<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oliver Bühler</title>
    <description>The latest articles on DEV Community by Oliver Bühler (@olibhlr).</description>
    <link>https://dev.to/olibhlr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013321%2F00ad002b-7746-4f94-a22e-14ec867a033f.jpg</url>
      <title>DEV Community: Oliver Bühler</title>
      <link>https://dev.to/olibhlr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olibhlr"/>
    <language>en</language>
    <item>
      <title>Configuring AgentCore Runtime: Session Lifecycle, Container Backing, Memory</title>
      <dc:creator>Oliver Bühler</dc:creator>
      <pubDate>Wed, 12 Aug 2026 16:00:36 +0000</pubDate>
      <link>https://dev.to/olibhlr/configuring-agentcore-runtime-session-lifecycle-container-backing-memory-421c</link>
      <guid>https://dev.to/olibhlr/configuring-agentcore-runtime-session-lifecycle-container-backing-memory-421c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I configured AgentCore Runtime under this project's harness one block at a time, session lifecycle, execution role, model resolution, container backing, memory attachment and network mode, and almost every setting's real behavior only showed up on a live invoke, not in a schema or a docs page. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part 3 of the series "Building a Partner Sales Agent on Amazon Bedrock AgentCore", built around one real project: a conversational agent that connects HubSpot CRM and AWS Partner Central for an AWS Partner's sales team. Article 2 argued why this project hands its agent loop to a managed harness at all; this article configures that harness against the runtime that executes it, block by block. Every setting can be reproduced from the AWS Management Console. My Terraform build is a automation bonus, including the places where automation hit problems a console reader never sees. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/olibhlr/two-systems-one-sales-motion-an-aws-partner-agent-on-amazon-agentcore-harness-3733"&gt;Article 1: Two Systems, One Sales Motion: An AWS Partner Agent on Amazon AgentCore Harness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/olibhlr/why-i-didnt-write-my-own-agent-loop-the-case-for-agentcore-harness-34n1"&gt;Article 2: Why I Didn't Write My Own Agent Loop: The Case for AgentCore Harness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Article 3: Configuring AgentCore Runtime: Session Lifecycle, Container Backing, Memory&lt;/li&gt;
&lt;li&gt;Article 4: One Gateway, Six Tools: AgentCore Gateway as Your Agent's Only Way Out &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 5: A Deterministic Backstop for Your Agent: AgentCore Policy and Cedar &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 6: Grounding an Agent Without a Vector Database: AgentCore Managed Knowledge Base &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 7: From Company Name to CRM Record: One AgentCore Conversation, End to End &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A harness needs exactly two values at creation time: a name and an execution role. Everything else this article covers (the model reference, the memory binding, the container image, the skill, the session limits, the network mode) can be attached later. I have build the agent iterative and got surprises on nearly step in the way. So I walk the resource from the outside in and hold every block to the same question: what does this actually do at runtime and how do I know? Where the answer came from a live failure instead of documentation, I say so. That covers most of the article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgpbhelg0d8q8l1se83i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgpbhelg0d8q8l1se83i.png" alt="AgentCore Harness Configuration" width="800" height="1179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Session lifecycle: how long a conversation stays warm
&lt;/h2&gt;

&lt;p&gt;A harness session opens on the first invoke and stays warm as long as calls keep landing. Two limits bound it: an idle timeout of 15 minutes and a maximum lifetime of 8 hours [1]. Each &lt;code&gt;InvokeHarness&lt;/code&gt; call restarts the idle counter from zero. What ends a session early is a gap between calls, never a count of turns: forty short exchanges inside ten minutes leave the counter at zero, while one sixteen-minute pause terminates the session. The 8-hour ceiling is the other bound, and it holds regardless of activity.&lt;/p&gt;

&lt;p&gt;In the console, these limits live in the harness's runtime-environment settings, which share their parameters with AgentCore Runtime itself: idle session timeout, maximum lifetime, network configuration, filesystem configuration [1]. A harness is backed by an AgentCore Runtime resource under the hood, witch you can see also twice on the IAM grant and network section. In my demo I run with the default values. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2xx8tsgo7vqst5nys52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2xx8tsgo7vqst5nys52.png" alt="Sequence Diagram" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a live demo, the numbers translate directly. A sales representative can pause mid-conversation for up to 15 minutes, take audience questions, and resume with full session state. A longer gap or the 8-hour ceiling, means the next invoke opens a fresh session silently. There is no error and no warning. There is just no continuity and an agent that suddenly asks which opportunity you meant.&lt;/p&gt;

&lt;p&gt;The operational risk for a workshop is therefore specific: watch the gap between the last invoke and the next one. A Q&amp;amp;A block that runs 20 minutes resets state mid-presentation. If you ask your self how to resolve this issue, the answer is a long-term memory, that survives session boundaries by design. &lt;/p&gt;

&lt;h2&gt;
  
  
  Execution role: one identity for everything the agent does
&lt;/h2&gt;

&lt;p&gt;Everything the agent does at runtime, it does as one IAM role. &lt;code&gt;harness_exec&lt;/code&gt; is the harness's execution role, and it is the identity behind the Bedrock model calls, the Memory reads and writes, and the SigV4-signed calls to the Gateway, plus the ECR image pulls and the observability writes that keep the runtime diagnosable.&lt;/p&gt;

&lt;p&gt;AWS documents the baseline fan-out for a harness execution role across four services: AgentCore Identity for workload access tokens, X-Ray for sampling rules and traces, CloudWatch for log groups and streams and ECR for image and layer access [2]. This project grants three of the four and skips Identity entirely. The harness's outbound authentication toward the Gateway is a tagged union of three variants: &lt;code&gt;aws_iam&lt;/code&gt; (a boolean), &lt;code&gt;none&lt;/code&gt; (a boolean) or &lt;code&gt;oauth&lt;/code&gt; with a credential-provider ARN and scopes. Only the &lt;code&gt;oauth&lt;/code&gt; variant consumes an AgentCore Identity resource. &lt;code&gt;aws_iam = true&lt;/code&gt;, which this project sets, is a self-contained SigV4-signing mode: the harness signs Gateway calls with its own execution-role credentials and needs no Identity resource anywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpj9blzf8vjvd5llqhjqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpj9blzf8vjvd5llqhjqx.png" alt="Harness Execution Role" width="800" height="1169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the console, the execution role is a single field on the harness form. I have scoped a policy per capability: &lt;/p&gt;

&lt;p&gt;each grant is its own policy document and role policy, listing only the actions one capability needs, against one ARN. No wildcard resource appears anywhere except where a service's own IAM model demands it: &lt;code&gt;ecr:GetAuthorizationToken&lt;/code&gt; is a registry-level action that ECR refuses to scope to a repository [3], the X-Ray write actions take no resource ARN, and &lt;code&gt;cloudwatch:PutMetricData&lt;/code&gt; is condition-scoped to the &lt;code&gt;bedrock-agentcore&lt;/code&gt; namespace instead. Across the whole project there is exactly one further documented exception, the Knowledge Base's &lt;code&gt;bedrock:AgenticRetrieveStream&lt;/code&gt; grant, held on the Gateway's service role, which AWS does not support scoping to a single Knowledge Base; article 6 covers it.&lt;/p&gt;

&lt;p&gt;Here is what one of those pairs looks like in the automated build. This is the Memory grant and its action list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_memory"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock-agentcore:CreateEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock-agentcore:ListEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock-agentcore:GetEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock-agentcore:ListSessions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock-agentcore:ListActors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock-agentcore:RetrieveMemoryRecords"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;awscc_bedrockagentcore_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memory_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_memory"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"agentcore-memory"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy did not exist on day one. My first live smoke test failed with an &lt;code&gt;AccessDeniedException&lt;/code&gt; on &lt;code&gt;bedrock-agentcore:ListEvents&lt;/code&gt; against the attached Memory resource's ARN, because the role only carried the model-invoke actions at that point. &lt;code&gt;bedrock-agentcore:*&lt;/code&gt; is a distinct IAM namespace from &lt;code&gt;bedrock:*&lt;/code&gt; and nothing in the toolchain flags its absence: &lt;code&gt;terraform validate&lt;/code&gt; passed, the plan was clean, the apply succeeded, and the harness itself created without complaint. The gap surfaced at the first real invoke, as a 403.&lt;/p&gt;

&lt;p&gt;Once tools were attached, the harness stopped calling the model through &lt;code&gt;InvokeModel&lt;/code&gt; and switched to Bedrock's &lt;code&gt;Converse&lt;/code&gt; and &lt;code&gt;ConverseStream&lt;/code&gt; operations, two separate IAM actions the role had never been granted: another live &lt;code&gt;AccessDeniedException&lt;/code&gt;. Both actions folded into the same policy that already carried the plain &lt;code&gt;InvokeModel*&lt;/code&gt; grant, against the same ARNs (the profile-versus-region ARN split is Model resolution's subject, below):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_bedrock_invoke"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock:InvokeModelWithResponseStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock:Converse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock:ConverseStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-central-1:${data.aws_caller_identity.current.account_id}:inference-profile/eu.anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-central-1::foundation-model/anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-north-1::foundation-model/anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-south-1::foundation-model/anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-south-2::foundation-model/anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-3::foundation-model/anthropic.claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock-invoke"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec_bedrock_invoke&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same switch triggered Bedrock's marketplace-subscription check for third-party models. With a new resource in one of the most recent AWS terraform provider releases, I could enable the model as well via Terraform (there is also an option in the AWS Management Console):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_marketplace"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"aws-marketplace:ViewSubscriptions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"aws-marketplace:Subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws-marketplace:ProductId"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prod-4ezhkeia6k2cs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Claude Sonnet 5's marketplace product id&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_marketplace"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"marketplace-model-subscription"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec_marketplace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AgentCore Gateway call needed &lt;code&gt;bedrock-agentcore:InvokeGateway&lt;/code&gt; on the Gateway's ARN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_invoke_gateway"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:InvokeGateway"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_bedrockagentcore_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateway_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_invoke_gateway"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"invoke-gateway"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec_invoke_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initial I had some trouble finding out why the harness was not writing any CloudWatch logs until I figured out what permissions where missing. This is the one policy in the set with more than one statement, because logs, X-Ray, and the metrics namespace each carry their own scoping rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_observability"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"logs:CreateLogGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"logs:DescribeLogStreams"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:eu-central-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"logs:DescribeLogGroups"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:eu-central-1:${data.aws_caller_identity.current.account_id}:log-group:*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"logs:CreateLogStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"logs:PutLogEvents"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:eu-central-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"xray:PutTraceSegments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"xray:PutTelemetryRecords"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"xray:GetSamplingRules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"xray:GetSamplingTargets"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# X-Ray's write and sampling actions take no resource ARN&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch:PutMetricData"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudwatch:namespace"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_observability"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"agentcore-observability"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec_observability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five of the six policies here added during troubleshooting (RTFM). Only the ECR grant was added before it could fail, because the developer guide states the requirement plainly [3]. For the container I added the following permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_ecr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ecr:GetDownloadUrlForLayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecr:BatchGetImage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ecr:GetAuthorizationToken"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"harness_exec_ecr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecr-image-pull"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec_ecr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Model resolution: the profile id, not the model id
&lt;/h2&gt;

&lt;p&gt;The harness's model block takes a single id string. This project runs Claude Sonnet 5 through &lt;code&gt;eu.anthropic.claude-sonnet-5&lt;/code&gt;, a cross-region inference profile: Bedrock's routing layer that spreads one model's traffic across a set of regional endpoints under a single addressable id [4]. This profile routes across six regions: eu-central-1, eu-north-1, eu-south-1, eu-south-2, eu-west-1, and eu-west-3.&lt;/p&gt;

&lt;p&gt;The bare model id fails and I confirmed the mechanism directly: &lt;code&gt;aws bedrock get-foundation-model&lt;/code&gt; reports the bare id's &lt;code&gt;inferenceTypesSupported&lt;/code&gt; as &lt;code&gt;["INFERENCE_PROFILE"]&lt;/code&gt; and nothing else. There is no on-demand single-region invocation for this model. The config field would accept the bare id, nothing validates it at Terraform plan time and every invocation would then fail to reach the model. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr78aynek1brgj1umxmjg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr78aynek1brgj1umxmjg.png" alt="AgentCore Harness Configuration (Model)" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the console: pick the inference profile, not the foundation model, wherever the harness asks for a model id. The Bedrock console's cross-region inference page lists the profiles available in your region, with their status and routing targets. &lt;/p&gt;

&lt;p&gt;The IAM consequence follows in the same way, because the routing layer changes what gets authorized. Bedrock checks both the profile call and the regional endpoint the call lands on, so the execution role's &lt;code&gt;bedrock:InvokeModel*&lt;/code&gt; grant scopes to seven ARNs, not one: the inference-profile ARN, which is account-scoped, plus the six underlying per-region foundation-model ARNs, which carry no account segment because foundation models are AWS-global resources. Scope to the profile alone and the invocation fails in whichever region the router picks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container backing and why it exists here
&lt;/h2&gt;

&lt;p&gt;Skills are filesystem-path artifacts. A purely declarative harness has no filesystem to put them on. That single sentence is the entire reason this project's harness is container-backed: the BANT-qualification Skill (budget, authority, need, timeline) ships baked into an image and nothing else about the harness needed a container at all.&lt;/p&gt;

&lt;p&gt;The default harness environment gives the agent Python and Bash, which is enough for most configurations [5]. A custom environment is an ECR image built for &lt;code&gt;linux/arm64&lt;/code&gt; and it comes with one behavior that changes how you think about the image: the harness overrides the container's ENTRYPOINT and CMD, so your startup command never runs [5]. The container is a filesystem layer, nothing more. The harness's own managed agent process, the model, the tool blocks, the system prompt, runs unaffected by the conversion; your installed software, filesystem, and environment variables are simply available to it. My Dockerfile is accordingly two lines of substance, a &lt;code&gt;FROM&lt;/code&gt; and a &lt;code&gt;COPY&lt;/code&gt;, with no ENTRYPOINT and no application code. The one contract that matters is that the skill's configured path be byte-identical to the COPY destination [6]:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;environment_artifact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;container_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container_uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_ecr_repository.harness.repository_url}:${var.image_tag}"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;skill&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;".agents/skills/sales-methodology-implementer"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console path is: create the ECR repository -&amp;gt; push the image -&amp;gt; then set the image URI and the skill path on the harness. &lt;/p&gt;

&lt;h2&gt;
  
  
  Memory attachment
&lt;/h2&gt;

&lt;p&gt;AgentCore Memory gives the harness two layers: short-term session events and long-term strategies that extract durable records from those events. In the console, the Memory resource is its own thing under Build, created with a name, an event-retention period and a set of strategies. The harness then binds it by ARN. This project's Memory resource carries all four built-in long-term strategies: semantic, summary, user preference and episodic with a reflection configuration (I wanted to try everything).&lt;/p&gt;

&lt;p&gt;Console readers configure this in forms. Automation readers hit a wall first: &lt;code&gt;hashicorp/aws&lt;/code&gt;'s &lt;code&gt;aws_bedrockagentcore_memory&lt;/code&gt; resource has no memory-strategy block anywhere in its schema. Only &lt;code&gt;hashicorp/awscc&lt;/code&gt;'s Cloud-Control-backed &lt;code&gt;awscc_bedrockagentcore_memory&lt;/code&gt; models the four strategies.  Here it is, condensed to the shape that matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"awscc_bedrockagentcore_memory"&lt;/span&gt; &lt;span class="s2"&gt;"agent"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"partner_growth_agent_memory"&lt;/span&gt;
  &lt;span class="nx"&gt;event_expiry_duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

  &lt;span class="nx"&gt;memory_strategies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;semantic_memory_strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"semantic"&lt;/span&gt;
        &lt;span class="nx"&gt;namespace_templates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/strategies/{memoryStrategyId}/actors/{actorId}/semantic"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;summary_memory_strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"summary"&lt;/span&gt;
        &lt;span class="nx"&gt;namespace_templates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/strategies/{memoryStrategyId}/actors/{actorId}/sessions/{sessionId}/summary"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user_preference_memory_strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"user_preference"&lt;/span&gt;
        &lt;span class="nx"&gt;namespace_templates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/strategies/{memoryStrategyId}/actors/{actorId}/preferences"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;episodic_memory_strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"episodic"&lt;/span&gt;
        &lt;span class="nx"&gt;namespace_templates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/strategies/{memoryStrategyId}/actors/{actorId}/sessions/{sessionId}/episodes"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;reflection_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;# must be the same as, or a prefix of, the episodic namespace above&lt;/span&gt;
          &lt;span class="nx"&gt;namespace_templates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/strategies/{memoryStrategyId}/actors/{actorId}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# The awscc provider cannot flatten memory_strategies back into state;&lt;/span&gt;
  &lt;span class="c1"&gt;# without this, every plan re-proposes the same phantom change forever.&lt;/span&gt;
  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;memory_strategies&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The namespace templates carry the design decision. Look at which strategies scope to &lt;code&gt;{actorId}&lt;/code&gt; and which to &lt;code&gt;{sessionId}&lt;/code&gt;. Semantic facts, user preferences and episodic reflections are statements about the actor, so they scope to &lt;code&gt;{actorId}&lt;/code&gt; alone: that is what gives the fixed demo actor true cross-session recall, where the sales rep closes the browser, opens a new session tomorrow, and the agent still knows what it learned. Summaries and raw episodes are per-conversation artifacts, so they keep &lt;code&gt;{sessionId}&lt;/code&gt; in their path and die with the conversation's relevance. This split is a reasoning step, not a default; the templates ship empty and force you to make it.&lt;/p&gt;

&lt;p&gt;Two constraints are documented nowhere I could find, not in the &lt;code&gt;CreateMemory&lt;/code&gt; API reference and not in the CloudFormation resource schema, and both surfaced only as live &lt;code&gt;ValidationException&lt;/code&gt;s [8]: &lt;code&gt;namespace_templates&lt;/code&gt; is actually required per strategy despite being marked optional (AWS's own worked memory examples all set it explicitly, which corroborates the finding [9]) and the episodic strategy's reflection namespace must be the same as, or a prefix of, the episodic namespace itself. My first attempt used a sibling path and the service rejected it with an error message.&lt;/p&gt;

&lt;p&gt;On the actorId I had a important learning: I fix defined this id in Terraform. On the deployed system every Harness invocation passed per-user a &lt;code&gt;actorId&lt;/code&gt; (derives from the caller's verified Cognito identity and that per-request value, not this default is what isolates users' memory from each other). &lt;/p&gt;

&lt;p&gt;On the validation of this feature: To check cross-session recall I created a session and stated a fictional example the agent should remember. I then opend a new session a few minutes later and asked to recall that fact. It worked really well.&lt;/p&gt;

&lt;p&gt;The same Memory resource also backs the chat UI's session-history feature, which reads and deletes conversations through its own separately scoped policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network mode: PUBLIC by omission
&lt;/h2&gt;

&lt;p&gt;Here is a configuration finding I have not seen written down anywhere: in the installed Terraform provider, there is no &lt;code&gt;network_mode&lt;/code&gt; attribute on the harness resource at all. PUBLIC network mode is expressed by omitting the entire &lt;code&gt;environment&lt;/code&gt;/&lt;code&gt;network_configuration&lt;/code&gt; block. There is no enum to set; the block simply stays unwritten. And you should not try to write the VPC variant casually either: setting a VPC &lt;code&gt;network_configuration&lt;/code&gt; triggers a permanent taint loop in the installed provider, an open issue at the time of writing [11].&lt;/p&gt;

&lt;p&gt;A console reader never meets this. The console presents network mode as a visible choice between public and VPC and choosing public is unremarkable. In the automated build, the same choice looks like something is missing: grepping the Terraform for a network block and finding nothing is the configuration, not evidence that a step was skipped. The harness resource's overall shape makes the point best, with the absence marked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagentcore_harness"&lt;/span&gt; &lt;span class="s2"&gt;"agent"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;harness_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_name&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harness_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;model&lt;/span&gt;                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# model resolution, above&lt;/span&gt;
  &lt;span class="nx"&gt;environment_artifact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# container backing, above&lt;/span&gt;
  &lt;span class="nx"&gt;skill&lt;/span&gt;                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;               &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# memory attachment, above&lt;/span&gt;
  &lt;span class="nx"&gt;tool&lt;/span&gt;                 &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# the Gateway binding, article 4&lt;/span&gt;

  &lt;span class="c1"&gt;# Deliberately no environment / network_configuration block.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two modes on offer, public and VPC, are AgentCore Runtime's own, the runtime backing showing through one last time [12]. I had no use-case for the VPC mode, but you could used it to access private resources, internal APIs and services that run non-serverless. &lt;/p&gt;

&lt;h2&gt;
  
  
  Transferable patterns
&lt;/h2&gt;

&lt;p&gt;Five habits from this configuration work generalize beyond the demo to any AgentCore build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PUBLIC-by-omission.&lt;/strong&gt; Network mode is not a value; it is the absence of a block. Before assuming a resource type needs an explicit off setting, check whether the provider models the default as an omitted block instead of an enum. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The awscc-versus-aws provider check.&lt;/strong&gt; &lt;code&gt;hashicorp/aws&lt;/code&gt; has no memory-strategy support on its Memory resource; only &lt;code&gt;hashicorp/awscc&lt;/code&gt; reaches AgentCore's Cloud-Control-backed long-term-memory API. When a resource looks incompletely modeled in one provider, check the sibling Cloud-Control provider's schema before concluding the feature is unsupported in Terraform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One scoped policy per capability, not one role-wide policy.&lt;/strong&gt; Each policy-document-and-role-policy pair grants exactly the actions one capability needs, against one ARN. It costs more files up front and pays back the first time you need to reason about the blast radius of a single service or to delete a capability cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM surfaces one 403 at a time, and &lt;code&gt;terraform validate&lt;/code&gt; catches none of them.&lt;/strong&gt; A clean plan and a clean apply prove nothing about runtime authorization in a service-specific namespace like &lt;code&gt;bedrock-agentcore:*&lt;/code&gt;. Budget a live-invoke debugging pass after every apply that touches a new capability. In this build that pass found five of the execution role's six policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern under all of it
&lt;/h2&gt;

&lt;p&gt;Step back from the sections and one shape repeats. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network mode: I assumed a setting existed and the truth was an absent block. &lt;/li&gt;
&lt;li&gt;Memory strategies: I assumed the standard provider covered the resource and the truth was a second provider by necessity. &lt;/li&gt;
&lt;li&gt;The execution role: I assumed the documented actions were the whole list, and the truth arrived one live 403 at a time, five policies' worth. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PUBLIC-by-omission, awscc-by-necessity, and IAM found one 403 at a time are the same finding wearing three different names: the straightforward assumption failed and the live apply is what caught it. None of these surfaced in validation, planning, or documentation review. Every one surfaced when the deployed system was made to actually do the thing.&lt;/p&gt;

&lt;p&gt;That is no argument against the harness. When the runtime is configured, it runs reliable. I can only suggest to get your hands dirty and try it out yourself. &lt;/p&gt;

&lt;p&gt;I hope this article was useful for you. I would love to receive feedback on what you liked and disliked, so that I can improve any future article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;&lt;a id="source-1"&gt;&lt;/a&gt;[1] Amazon Bedrock AgentCore developer guide: harness/runtime session lifecycle, &lt;code&gt;idleRuntimeSessionTimeout&lt;/code&gt; and &lt;code&gt;maxLifetime&lt;/code&gt;, and the runtime-environment parameters shared with AgentCore Runtime. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-lifecycle-settings.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-lifecycle-settings.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-2"&gt;&lt;/a&gt;[2] Amazon Bedrock AgentCore developer guide: baseline execution-role permissions for the runtime (AgentCore Identity, X-Ray, CloudWatch, ECR). &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-3"&gt;&lt;/a&gt;[3] Amazon Bedrock AgentCore developer guide: private ECR access for custom container images, including the &lt;code&gt;ecr:GetAuthorizationToken&lt;/code&gt; registry-level grant. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-security.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-security.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-4"&gt;&lt;/a&gt;[4] Amazon Bedrock user guide: cross-region inference and inference profiles. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-5"&gt;&lt;/a&gt;[5] Amazon Bedrock AgentCore developer guide: custom environment (container images); the default Python-and-Bash environment, the &lt;code&gt;linux/arm64&lt;/code&gt; requirement, and the ENTRYPOINT/CMD override. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-environment.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-environment.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-6"&gt;&lt;/a&gt;[6] Amazon Bedrock AgentCore developer guide: filesystem-path skills and baking skill directories into a container image. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-skills.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-skills.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-7"&gt;&lt;/a&gt;[7] Terraform CLI documentation: resource targeting (&lt;code&gt;-target&lt;/code&gt;) as an exceptional-use mechanism for ordering constraints. &lt;a href="https://developer.hashicorp.com/terraform/cli/commands/plan#resource-targeting" rel="noopener noreferrer"&gt;https://developer.hashicorp.com/terraform/cli/commands/plan#resource-targeting&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-8"&gt;&lt;/a&gt;[8] Amazon Bedrock AgentCore API reference: &lt;code&gt;CreateMemory&lt;/code&gt; and its memory-strategy input, where &lt;code&gt;namespaceTemplates&lt;/code&gt; is marked optional. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateMemory.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateMemory.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-9"&gt;&lt;/a&gt;[9] AWS Machine Learning Blog: "Amazon Bedrock AgentCore Memory: Building context-aware agents", whose worked examples all set namespace templates explicitly. &lt;a href="https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-agentcore-memory-building-context-aware-agents/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-agentcore-memory-building-context-aware-agents/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-10"&gt;&lt;/a&gt;[10] GitHub, hashicorp/terraform-provider-aws issue #48496: the harness resource crashes plan/apply when the &lt;code&gt;memory&lt;/code&gt; block is omitted and the harness defaults to managed memory. &lt;a href="https://github.com/hashicorp/terraform-provider-aws/issues/48496" rel="noopener noreferrer"&gt;https://github.com/hashicorp/terraform-provider-aws/issues/48496&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-11"&gt;&lt;/a&gt;[11] GitHub, hashicorp/terraform-provider-aws issue #48159: setting a VPC &lt;code&gt;network_configuration&lt;/code&gt; on the harness causes a permanent taint loop. &lt;a href="https://github.com/hashicorp/terraform-provider-aws/issues/48159" rel="noopener noreferrer"&gt;https://github.com/hashicorp/terraform-provider-aws/issues/48159&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-12"&gt;&lt;/a&gt;[12] Amazon Bedrock AgentCore developer guide: network modes (public and VPC), and VPC mode's ENI-based attachment to your VPC via the AgentCore service-linked role. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-13"&gt;&lt;/a&gt;[13] Docker documentation: multi-platform builds with buildx. &lt;a href="https://docs.docker.com/build/building/multi-platform/" rel="noopener noreferrer"&gt;https://docs.docker.com/build/building/multi-platform/&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why I Didn't Write My Own Agent Loop: The Case for AgentCore Harness</title>
      <dc:creator>Oliver Bühler</dc:creator>
      <pubDate>Tue, 28 Jul 2026 21:00:50 +0000</pubDate>
      <link>https://dev.to/olibhlr/why-i-didnt-write-my-own-agent-loop-the-case-for-agentcore-harness-34n1</link>
      <guid>https://dev.to/olibhlr/why-i-didnt-write-my-own-agent-loop-the-case-for-agentcore-harness-34n1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I skipped writing my own agent loop and put this project on AWS Bedrock AgentCore Harness instead, because a hand-rolled loop would have cost me setup work, slow iteration and tool-integration plumbing that has nothing to do with qualifying a sale. A harness needs only two required parameters, a name and an execution role, and every other block (model, prompt, tools, skills, memory) attaches later and updates in place in about 20 seconds. The sharpest finding: container-backing a harness for a filesystem Skill does not have to mean granting the model shell access and I kept those two decisions separate on purpose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part 2 of the series "Building a Partner Sales Agent on Amazon Bedrock AgentCore", built around one real project: a conversational agent that connects HubSpot CRM and AWS Partner Central for an AWS Partner's sales team. Article 1 walked the whole architecture while this article argues for the piece at its center, the managed agent loop, before anything gets configured. Everything described here is deployed and running, and every setup appears console-first, with Terraform and boto3 behind my own build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/olibhlr/two-systems-one-sales-motion-an-aws-partner-agent-on-amazon-agentcore-harness-3733"&gt;Article 1: Two Systems, One Sales Motion: An AWS Partner Agent on Amazon AgentCore Harness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Article 2: Why I Didn't Write My Own Agent Loop: The Case for AgentCore Harness&lt;/li&gt;
&lt;li&gt;Article 3: Configuring AgentCore Runtime: Session Lifecycle, Container Backing, Memory &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 4: One Gateway, Six Tools: AgentCore Gateway as Your Agent's Only Way Out &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 5: A Deterministic Backstop for Your Agent: AgentCore Policy and Cedar &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 6: Grounding an Agent Without a Vector Database: AgentCore Managed Knowledge Base &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 7: From Company Name to CRM Record: One AgentCore Conversation, End to End &lt;em&gt;(coming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem a harness solves
&lt;/h2&gt;

&lt;p&gt;The agent loop itself is a weekend project. Send the conversation plus the tool definitions to a model, read back either text or a tool call, execute the tool, append the result, loop until the model stops asking for tools. I have written that loop before. If that were the whole job, this article would not exist.&lt;/p&gt;

&lt;p&gt;The cost sits in everything around the loop. When I priced out what this demo would need without a managed runtime, the list split into three buckets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;. Before the loop runs once, you need model wiring, an orchestration framework, a runtime environment to host the loop, a deployment path for that runtime, and memory integration on top. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iteration speed&lt;/strong&gt;. A hand-rolled loop bakes its configuration into a service you own, so every change is a redeploy of that service: a new tool, a reworded system prompt, a different model. This build reworked its system prompt repeatedly against live test results and because the prompt is harness configuration rather than application code, each rework was an update apply that completed in about 20 seconds with nothing redeployed. That difference compounds. Prompt tuning at 20 seconds per attempt is a different activity from prompt tuning at one container build per attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool integration&lt;/strong&gt;. Memory, retrieval, browsers, code execution, and external APIs all attach differently and each source brings its own transport and auth story. There is no common connector layer you get for free in a hand-rolled loop. This one demo connects six Gateway-fronted tools across four targets, a filesystem Skill, and a memory store. Every one of those integrations is configuration on the harness rather than code I maintain and the code that does exist lives behind the Gateway, never inside the loop.&lt;/p&gt;

&lt;p&gt;Concretely: without a harness, this project would have needed its own agent loop, its own tool-dispatch logic, its own session and memory wiring, and its own container orchestration before the agent answered a single BANT (budget, authority, need, timeline) qualification question. The demo's value lives in its tools, its guardrails, and its prompt. None of it lives in the loop.&lt;/p&gt;

&lt;p&gt;Those three buckets are also the reusable takeaway. Run them as a checklist against your own planned build. If you have one tool, no memory requirement, and an existing service that can host the loop, hand-rolling is defensible and keeps you in full control of the loop's internals. This build failed the checklist on all three counts, which is what put AgentCore Harness, now generally available [1], at the center of my first article's architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a harness actually is
&lt;/h2&gt;

&lt;p&gt;A harness is a fully managed agent loop. You hand AgentCore a configuration: which model, which system prompt, which tools and skills, which memory. AgentCore runs the loop that decides when to answer and when to call a tool [2]. There is no framework code to write and no service of your own to keep alive.&lt;/p&gt;

&lt;p&gt;In the AWS Management Console, the harness lives in the Amazon Bedrock AgentCore console under Build, alongside Runtime, Gateways, Memory, Policy, and Identity; a separate Test section holds the harness playground used later in this article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwb8x2m2jicscw4i86fps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwb8x2m2jicscw4i86fps.png" alt="AgentCore Harness" width="800" height="1179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The API asks for exactly two things, a harness name and an execution role ARN, and the console's create form mirrors that split. Everything else on that form is optional: model, system prompt, tools, skills, memory, timeouts, environment settings. All of it can be added later on an update, and most of it can even be overridden per invocation [2]. In the Management console AWS takes care of the execution role creation based on the harness name. You actually only the name of the harness is needed to get started. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx2pyjnstfg64ea1iorh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx2pyjnstfg64ea1iorh.png" alt="AgentCore Harness Creation" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two-parameter claim holds outside the console too. The Terraform provider's schema for &lt;code&gt;aws_bedrockagentcore_harness&lt;/code&gt; marks just two top-level attributes as required, &lt;code&gt;harness_name&lt;/code&gt; and &lt;code&gt;execution_role_arn&lt;/code&gt;and a boto3 call is as simple as this [2]:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore-control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;harness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;harness_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyFirstAgentWithHarness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;executionRoleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your_harness_execution_role&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fuller shape as boto3 eqivilant, with the optional blocks this project uses, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;harness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;harness_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyFirstAgentWithHarness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;executionRoleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your_harness_execution_role&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrockModelConfig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modelId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu.anthropic.claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;path_to_skill&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentcore_gateway&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mygatewaytools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentCoreGateway&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;gateway_arn&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model id there is the one this project deploys: &lt;code&gt;eu.anthropic.claude-sonnet-5&lt;/code&gt;, the EU cross-region inference profile rather than the bare model id. My shipped harness ended up using most of the major optional blocks this API offers: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a model block&lt;/li&gt;
&lt;li&gt;a long rule-numbered system prompt&lt;/li&gt;
&lt;li&gt;a Gateway tool binding&lt;/li&gt;
&lt;li&gt;a Skill&lt;/li&gt;
&lt;li&gt;a memory attachment&lt;/li&gt;
&lt;li&gt; explicit tool allowlist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not one of those was required to get the first harness running. I started near the two-parameter shape and grew the configuration one apply at a time and I would recommend that order: a name and a role on the first deploy, then each optional block only when the build demands it. There is also a CLI middle path: &lt;code&gt;agentcore create&lt;/code&gt; scaffolds a new agent project, with a harness project as one of the types its wizard offers, creatable non-interactively with flags [3].&lt;/p&gt;

&lt;h2&gt;
  
  
  Control plane versus data plane
&lt;/h2&gt;

&lt;p&gt;AgentCore splits the harness API in two. Five control-plane operations manage the harness as a resource: &lt;code&gt;CreateHarness&lt;/code&gt;, &lt;code&gt;GetHarness&lt;/code&gt;, &lt;code&gt;ListHarnesses&lt;/code&gt;, &lt;code&gt;UpdateHarness&lt;/code&gt;, &lt;code&gt;DeleteHarness&lt;/code&gt;. One data-plane operation runs it: &lt;code&gt;InvokeHarness&lt;/code&gt; [4]. Boto3 ships the split as two separate clients, &lt;code&gt;bedrock-agentcore-control&lt;/code&gt; and &lt;code&gt;bedrock-agentcore&lt;/code&gt;. I mapped this at the start of the build by listing the installed SDK's available services (boto3 1.43.56) instead of trusting documentation, because the service was new enough that the SDK and the docs could disagree.&lt;/p&gt;

&lt;p&gt;The split shows up physically in my build. Terraform's &lt;code&gt;aws_bedrockagentcore_harness&lt;/code&gt; resource wraps the control plane: plan and apply translate to Create, Get, and Update calls. The streaming relay Lambda from article 1's Dynamic content speaks only the data plane, through the JavaScript SDK's &lt;code&gt;InvokeHarnessCommand&lt;/code&gt;, at request time. Two SDK surfaces, two clients, no overlap.&lt;/p&gt;

&lt;p&gt;That separation buys a security property. The relay's execution role carries the data-plane invoke grant and nothing from the control plane, so the component that runs the agent for end users cannot reconfigure the agent. Terraform holds the control-plane credentials and never runs at request time. Configuring a harness and running a harness are two distinct operations against two distinct APIs, which is why the Terraform code and the relay Lambda never touch each other's calls and more usefully, why they cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  From configuration to a running agent
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft6buns0igqfpl1n1vgef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft6buns0igqfpl1n1vgef.png" alt="AgentCore Harness, it's configuration and capabilitie" width="799" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read it left to right. The agent configuration holds four rows: the model, the system prompt, tools and skills, and everything else (timeouts, iteration caps, environment). Six kinds of tool source can feed the tools row: a remote MCP server, an AgentCore Gateway, the managed Browser, the managed Code Interpreter, an inline tool executed on the client side, and a Skill. The configured harness then draws on the platform capabilities on the right: Runtime hosts it, Memory persists across sessions, Identity handles inbound auth, Observability traces it. Underneath all of that sit the runtime features the harness inherits: long-running sessions, attachable storage, shell command execution, microVM isolation, and the option to bring your own container image [2].&lt;/p&gt;

&lt;p&gt;Mapping that diagram onto this project keeps it honest. Of the six tool sources, I use two: one Gateway binding that fronts all six of article 1's Gateway targets (article 4 opens that up), and one Skill. Of the capabilities column, Runtime and Memory are load-bearing (article 3 configures both), Observability supplied the tool-call traces I later used to measure a prompt-compliance problem (article 5 puts a number on that story), and Identity goes unused: article 1's inbound-auth investigation explains why a token-validating relay beat it. And the bottom-row container option is in this build for one specific reason, not as a default.&lt;/p&gt;

&lt;p&gt;That reason is Skills. A Skill is a directory of instruction markdown and supporting files, sometimes including scripts, that the harness loads for the model on demand, and the &lt;code&gt;skills&lt;/code&gt; parameter, in the path form this project uses, points at content already on the harness filesystem. It uploads and installs nothing itself [5]. A purely declarative harness has no filesystem to point into, so the skill directory has to arrive some other way, and baking it into a custom container image is the way this project ships it. The whole Dockerfile is a &lt;code&gt;FROM&lt;/code&gt; and a &lt;code&gt;COPY&lt;/code&gt;; the image is purely a filesystem layer, never a running process of mine [6].&lt;/p&gt;

&lt;p&gt;The Skill itself is not my writing, and that belongs on the record: I vendored &lt;code&gt;sales-methodology-implementer&lt;/code&gt; verbatim from the MIT-licensed &lt;code&gt;github.com/OneWave-AI/claude-skills&lt;/code&gt; repository at a pinned commit [7]. Its files also cover MEDDIC, Sandler, Challenger, and SPIN, and I kept all of that in place. Restricting the demo to BANT is one instruction in the system prompt, not file surgery on someone else's skill.&lt;/p&gt;

&lt;p&gt;Attaching the container brought the first surprise. I expected converting a declarative harness to a container-backed one to force a destroy-and-recreate, and had planned around that blast radius. The live plan read &lt;code&gt;2 to add, 1 to change, 0 to destroy&lt;/code&gt;: adding the container artifact and the skill, changing the system prompt, all as an in-place update. The console's edit flow drives the same update API [4], so the in-place behavior is no Terraform quirk; my plan was simply wrong about the blast radius, in the good direction for once. Article 3 walks the conversion in configuration detail, plan output and build tripwires included.&lt;/p&gt;

&lt;p&gt;The harder edge came from the same Skill mechanism. A Skill's content is fetched onto the harness filesystem and injected into the model's context, and for a methodology Skill that is the entire job. But if a Skill bundles a script, the only AgentCore-native mechanism that runs it server-side is the harness's built-in &lt;code&gt;shell&lt;/code&gt; tool, which executes inside the harness's microVM with the harness's own execution-role credentials. There is no separate, scoped skill-execution role, and no "run only this one script" tool type [8]. I did not take that on faith. I built a second, read-only Skill, &lt;code&gt;partner-central-lookup&lt;/code&gt;, wired it as a second skill block, added its one dependency to the image, and gave the harness a narrowly scoped &lt;code&gt;sts:AssumeRole&lt;/code&gt; grant. Making it executable then required adding &lt;code&gt;shell&lt;/code&gt; to the harness's &lt;code&gt;allowed_tools&lt;/code&gt;, and that list turned out to be an allowlist in the strict sense, not a filter: because my harness had carried an explicit, restrictive allowlist from the first week, the built-in &lt;code&gt;shell&lt;/code&gt; and &lt;code&gt;file_operations&lt;/code&gt; tools had been silently excluded the entire time.&lt;/p&gt;

&lt;p&gt;Granting &lt;code&gt;shell&lt;/code&gt; would not have restored something already present; it would have added a new capability outright: arbitrary in-container command execution, on a harness that in the same session holds knowledge-base retrieval over crawled web content and a cross-account role into Partner Central. I chose to keep the capability permanently inert rather than switch it on, and in a later v1.1 cleanup I removed &lt;code&gt;partner-central-lookup&lt;/code&gt; entirely, since an inert, never-executed capability carries no ongoing value. The &lt;code&gt;sales-methodology-implementer&lt;/code&gt; Skill is unaffected, and it remains the one live reason this harness is container-backed at all.&lt;/p&gt;

&lt;p&gt;One adjacent warning from the same documentation page: the &lt;code&gt;InvokeAgentRuntimeCommand&lt;/code&gt; API executes commands in the harness microVM directly, without passing through the model or the &lt;code&gt;allowed_tools&lt;/code&gt; gate at all [8]. Nothing in this project's IAM grants it, anywhere.&lt;/p&gt;

&lt;p&gt;The decision rule I took away: before container-backing a harness for a Skill, decide whether the Skill needs to execute a bundled script at all, or whether the context-injection behavior alone does the job. For this project, context injection was the whole value, and &lt;code&gt;shell&lt;/code&gt; never earned its risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invoking it
&lt;/h2&gt;

&lt;p&gt;The invocation surface is small: a prompt goes in, the configured loop runs, an output comes back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwxqcmkzsgyksx0cabeh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwxqcmkzsgyksx0cabeh4.png" alt="Harness Invocation Flow" width="800" height="131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the console, the harness playground under Test drives this flow interactively: pick the harness, type a message, watch the response stream in. That playground was my second-fastest verification path throughout the build.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6ox4tmuawvam73c1qjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6ox4tmuawvam73c1qjq.png" alt="AgentCore Harness Playground" width="799" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fastest one is a script. Standing up the first live invocation taught me three things. The data-plane operation is named &lt;code&gt;InvokeHarness&lt;/code&gt;, not the shorter name I had assumed. It wants the harness's full ARN rather than a bare id and the response is an event stream (&lt;code&gt;messageStart&lt;/code&gt;, &lt;code&gt;contentBlockDelta&lt;/code&gt;, &lt;code&gt;contentBlockStop&lt;/code&gt;, and error events), not a single blob. Condensed from the shipped script, the call is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-central-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;harnessArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;harness_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# the full ARN, not a bare harness id
&lt;/span&gt;    &lt;span class="n"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# one uuid4 per conversation
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;text_chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;text_chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validationException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Harness validation error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;validationException&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes on what the condensation hides. The shipped script also surfaces &lt;code&gt;internalServerException&lt;/code&gt; and &lt;code&gt;runtimeClientError&lt;/code&gt; events as errors. It does not hardcode &lt;code&gt;invoke_harness&lt;/code&gt; at all: it resolves the operation name from the installed client's own method map, because the data-plane operation name has shifted across SDK revisions.&lt;/p&gt;

&lt;p&gt;That very first smoke test failed, with an &lt;code&gt;AccessDeniedException&lt;/code&gt; on &lt;code&gt;bedrock-agentcore:ListEvents&lt;/code&gt;: the execution role could call the model but not the harness's own attached Memory. The second attempt returned a non-empty Claude Sonnet 5 response from the live harness, which proved the entire walking skeleton in one command: Terraform-created harness, execution role, boto3 invoke, streamed model output. The whole invoke path really is as simple as the diagram claims. &lt;/p&gt;

&lt;p&gt;One more property of the invoke API deserves its own diagram: per-invocation overrides. The stored configuration is the baseline and a single call can override the model, the prompt, tools, or memory settings for that call only [4].&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztxlyhslwsh7m34eajyr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztxlyhslwsh7m34eajyr.png" alt="Harness State diagram" width="800" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The overrides carry one sharp edge I hit live: passing &lt;code&gt;tools=[...]&lt;/code&gt; at invoke time replaces the harness's entire configured toolset for that call. It does not add to it. When I tried injecting an approval tool per invocation, every Gateway tool vanished for that call, and the model attempted a bare tool name and got "Unknown tool" back. If you need a temporary extra tool alongside the configured ones, invoke-time injection is not the mechanism.&lt;/p&gt;

&lt;p&gt;The next article opens the box this section treated as opaque: session lifecycle and its idle limits, the container backing in configuration detail, memory attachment, and the execution-role IAM the harness needs at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving from Bedrock Agents
&lt;/h2&gt;

&lt;p&gt;If you run agents on Amazon Bedrock Agents today, AWS positions the harness as the path forward: the same fully managed loop idea, with more flexibility on tool integration, memory, and skills [9]. I did not migrate anything in this project, so I have no migration story to offer. What this build proofes: six tool source types, per-invocation overrides, filesystem skills and container backing covered everything this agent needed without a single loop-level workaround. If you are starting fresh rather than migrating, skip the comparison entirely and run the three-bucket checklist from the top of this article instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transferable patterns
&lt;/h2&gt;

&lt;p&gt;Three habits from this article generalize beyond this demo to any AgentCore build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two mandatory parameters, then grow.&lt;/strong&gt; Start a harness with &lt;code&gt;harness_name&lt;/code&gt; and &lt;code&gt;executionRoleArn&lt;/code&gt; alone and add model, tools, skills, and memory incrementally, as each becomes necessary. Every optional block you skip on day one is a block you cannot misconfigure on day one and updates land in place in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed loop versus hand-rolled loop is a decision, not a default.&lt;/strong&gt; Price your build against the three cost buckets: setup, iteration speed, tool integration. Reach for the harness when those costs dominate; keep writing your own loop when you need behavior the harness's configuration surface does not expose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container-back only for filesystem artifacts and treat execution as a separate decision.&lt;/strong&gt; A declarative harness covers model, prompt, tools, and memory with no container anywhere. Reach for a custom image specifically when a Skill must exist as files on the harness's filesystem, and treat &lt;code&gt;shell&lt;/code&gt; (or any execution-granting tool) as its own security review, never as a bundled default that container backing drags in. My build shipped the container and declined the execution, and both halves of that were deliberate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you are reproducing this
&lt;/h2&gt;

&lt;p&gt;Console first: create the harness under Build with a name via Quick Create (the execution role is added automatically), add the optional blocks as you need them and test in the playground under Test. A console reader building a declarative harness never touches Docker and can skip the rest of this section.&lt;/p&gt;

&lt;p&gt;The container-backed variant has two tripwires, both from my own build history. First, the image must be &lt;code&gt;linux/arm64&lt;/code&gt; [6]. My local Docker Desktop daemon is &lt;code&gt;linux/amd64&lt;/code&gt; and cannot build that natively; the one-step fix is &lt;code&gt;docker buildx build --platform linux/arm64 --push -t &amp;lt;ecr-repo-url&amp;gt;:&amp;lt;tag&amp;gt; .&lt;/code&gt;, which cross-builds through QEMU and pushes in the same command, with no daemon reconfiguration. Second, the image must exist in ECR before the harness that references it. In the console the ordering is natural (push, then paste the image URI into the form), while my Terraform build needed a two-pass apply with an explicit &lt;code&gt;-target&lt;/code&gt; on the ECR repository first. The constraint is the same on both paths; automation just makes it easier to get wrong in one shot.&lt;/p&gt;

&lt;p&gt;Then use the harness playground and perform a smoke test directly on the harness, bypassing API Gateway and Cognito entirely, so a failure isolates to the harness and its execution role rather than somewhere in a five-component request chain. I learned to run this first the cheap way, by debugging the long way once.&lt;/p&gt;

&lt;p&gt;I hope this article was useful for you. I would love to receive feedback on what you liked and disliked, so that I can improve any future article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;&lt;a id="source-1"&gt;&lt;/a&gt;[1] Amazon Bedrock AgentCore Harness general availability. &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-bedrock-agentcore-harness-generally-available/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-bedrock-agentcore-harness-generally-available/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-2"&gt;&lt;/a&gt;[2] AWS Bedrock AgentCore developer guide: harness concept and CreateHarness configuration parameters, including the two required parameters and per-invocation override support. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-get-started.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-get-started.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-3"&gt;&lt;/a&gt;[3] AWS Bedrock AgentCore developer guide: &lt;code&gt;agentcore create&lt;/code&gt; scaffolds a new agent project; harness projects are created interactively or non-interactively with flags. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/develop-agents.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/develop-agents.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-4"&gt;&lt;/a&gt;[4] AWS Bedrock AgentCore API reference: control-plane operations (CreateHarness, GetHarness, ListHarnesses, UpdateHarness, DeleteHarness) and the data-plane InvokeHarness operation with invoke-time configuration overrides. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateHarness.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateHarness.html&lt;/a&gt;; &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_InvokeHarness.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_InvokeHarness.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-5"&gt;&lt;/a&gt;[5] AWS Bedrock AgentCore developer guide, harness Skills: filesystem-path skills reference content already on the harness filesystem, baked into the container image or installed at session start. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-skills.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-skills.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-6"&gt;&lt;/a&gt;[6] AWS Bedrock AgentCore developer guide, harness custom environment (container images): the &lt;code&gt;linux/arm64&lt;/code&gt; requirement, and the harness overriding the container's ENTRYPOINT and CMD. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-environment.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-environment.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-7"&gt;&lt;/a&gt;[7] GitHub, OneWave-AI/claude-skills: upstream source of the vendored &lt;code&gt;sales-methodology-implementer&lt;/code&gt; Skill, MIT license. &lt;a href="https://github.com/OneWave-AI/claude-skills" rel="noopener noreferrer"&gt;https://github.com/OneWave-AI/claude-skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-8"&gt;&lt;/a&gt;[8] AWS Bedrock AgentCore developer guide, harness tools: the built-in &lt;code&gt;shell&lt;/code&gt; and &lt;code&gt;file_operations&lt;/code&gt; tools, &lt;code&gt;allowedTools&lt;/code&gt; as the restriction mechanism, and the warning that &lt;code&gt;InvokeAgentRuntimeCommand&lt;/code&gt; executes without passing the allowedTools gate. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-tools.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-tools.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-9"&gt;&lt;/a&gt;[9] AWS positioning of the harness relative to Amazon Bedrock Agents. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/agents-classic-maintenance-mode.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/agents-classic-maintenance-mode.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Two Systems, One Sales Motion: An AWS Partner Agent on Amazon AgentCore Harness</title>
      <dc:creator>Oliver Bühler</dc:creator>
      <pubDate>Thu, 23 Jul 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/olibhlr/two-systems-one-sales-motion-an-aws-partner-agent-on-amazon-agentcore-harness-3733</link>
      <guid>https://dev.to/olibhlr/two-systems-one-sales-motion-an-aws-partner-agent-on-amazon-agentcore-harness-3733</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a chat agent on AWS Bedrock AgentCore Harness (running Claude Sonnet 5) that connects HubSpot and AWS Partner Central so sales reps stop manually re-typing opportunity data between the two systems — a rep types a company name, the agent finds the matching Partner Central opportunity, qualifies it against BANT criteria, answers questions from a knowledge base and after explicit human approval writes a contact back to HubSpot. Every tool call routes through one Gateway with Cedar policy enforcement, so the model's reach into either live system stays reviewable rather than baked into a prompt. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part 1 of the series "Building a Partner Sales Agent on Amazon Bedrock AgentCore", built around one real project: a conversational agent that connects HubSpot CRM and AWS Partner Central for an AWS Partner's sales team. Everything described here is deployed and running. The build behind it is automated with Terraform, but nothing in this series requires that: every capability shown can be set up from the AWS Management Console. This article focuses on the general architecture of the demo. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Article 1: Two Systems, One Sales Motion: An AWS Partner Agent on Amazon  AgentCore Harness&lt;/li&gt;
&lt;li&gt;Article 2: Why I Didn't Write My Own Agent Loop: The Case for AgentCore Harness &lt;em&gt;(comming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 3: Configuring AgentCore Runtime: Session Lifecycle, Container Backing, Memory &lt;em&gt;(comming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 4: One Gateway, Six Tools: AgentCore Gateway as Your Agent's Only Way Out &lt;em&gt;(comming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 5: A Deterministic Backstop for Your Agent: AgentCore Policy and Cedar &lt;em&gt;(comming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 6: Grounding an Agent Without a Vector Database: AgentCore Managed Knowledge Base &lt;em&gt;(comming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Article 7: From Company Name to CRM Record: One AgentCore Conversation, End to End &lt;em&gt;(comming soon)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As company working in IT products, professional services and managed services, you need central customer relationship management (CRM) that is connected to your marketing and sales processes. This information usually covers contacts, deals, notes, follow-up tasks, marketing campaigns and much more. The entire sales motion lives here. A new lead arrives through a campaign, gets a contact record, moves through deal stages and every touchpoint lands in the same CRM the marketing team reports from. CRM systems are tailored to the company’s own processes and vary if you compare two similar companies. There are several well-established CRM tools in the market and some you might have heard about: Salesforce, Microsoft Dynamics, HubSpot. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I’m currently using HubSpot, so you will see curing this article series practical examples for HubSpot and it is also referred equivalent to any CRM. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you are an AWS Partner co-selling Amazon Web Services solutions, you see the long arm of the AWS CRM: AWS Partner Central. The portal is the main entry gate for AWS Marketing and Sales processes. AWS tracks the same customer relationship on its side as a Partner Central opportunity. That record carries the fields AWS cares about for co-selling: the customer, the workload, budget signals, a target timeline, and the AWS account team attached to the deal. For a partner in a co-sell motion, keeping that record current is not optional bookkeeping. It's how the AWS side knows the deal exists, how it gets routed to the right people and part of how the partnership itself gets measured.&lt;/p&gt;

&lt;p&gt;As an AWS Alliance Lead (caring about exactly the partnership with AWS) or a fellow Sales representative, keeping the company CRM world and AWS world in sync. Is a mandatory job for effective sales motion. With scaling organization and sales volume, this sync. can fast become a bottle neck. There were a few Connectors in the past (e.g. for Salesforce and Microsoft Dynamics), but they were barley usable and not flexible in case of heavy CRM customization. &lt;/p&gt;

&lt;p&gt;To give you a concrete example: A sales rep qualifying a AWS originated opportunity hold both the CRM and AWS Partner Central open. He reads opportunity fields in Partner Central, retype the relevant parts into HubSpot and copies the notes he last reported to the AWS side. Each manual hop is a chance for the records to drift apart. The cost isn't just double data entry. It's a co-sell process running on stale information and a rep who quietly stops updating the system that isn't their daily driver.&lt;/p&gt;

&lt;p&gt;Why solve this now and why this way? Partner Central Version 3 has been live since reinvent 2025 [1] and its opportunity data is reachable programmatically, including through a dedicated AWS Partner Central MCP server [2]. That is the integration surface this project builds on. The AWS side of the workflow stopped being a portal you log into and became something an agent can call.&lt;/p&gt;

&lt;p&gt;A note on who this series is for: cloud engineers and AWS partners evaluating Amazon Bedrock AgentCore against exactly this kind of integration problem. It assumes you know IAM and the general shape of an LLM tool-calling loop. It is not an introduction to what an AI agent is and it never argues that you should want one. What you should be able to do by the end of this article is look at your own pair of disconnected systems and judge whether it fits the same shape this architecture was built for: two live systems of record, one conversational workflow spanning both and a hard requirement that the model's access to each stays reviewable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I set out to prove
&lt;/h2&gt;

&lt;p&gt;One chat conversation should carry the whole motion. A sales rep types a company name. The agent finds the matching Partner Central opportunity, qualifies it against BANT criteria (budget, authority, need, timeline), answers competency questions from a document knowledge base that contains both AWS documents as well internal Sales enablement material, proposes a HubSpot contact, waits for an explicit human approval and writes the contact to HubSpot with a back-reference to the opportunity it came from.&lt;/p&gt;

&lt;p&gt;That conversation is the demo in article 7, run live against real HubSpot and Partner Central instances. The six articles before it are evidence for the claim, one building block at a time.&lt;/p&gt;

&lt;p&gt;The stack in short: The agent runs on AgentCore Harness, AWS's managed agent loop, now generally available [3]. The model is Claude Sonnet 5 running on AWS Bedrock. Everything runs as managed services in a single AWS account in eu-central-1: no Kubernetes, no self-managed inference and nothing that a reader with AWS Management Console access couldn't replicate. My own build automates the full setup with Terraform, which pays off in repeatability and change review, but treats that as a bonus rather than a prerequisite. Each article names the things to configure and the values that matter, so a console-only reader can follow every step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdvcj5qutfb6xwkdd2jo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdvcj5qutfb6xwkdd2jo.png" alt="The real application" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, top to bottom
&lt;/h2&gt;

&lt;p&gt;Now the best part: The architecture diagram. This is an overview of the whole system.  The rest of this article walks it region by region and the rest of this series zooms into it one region per article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fml036p1azy6dtlwuio1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fml036p1azy6dtlwuio1f.png" alt="An architecture Overview" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read it in four horizontal bands: static content delivery (CloudFront, S3), Dynamic content (Cognito, API Gateway, Lambda functions), AgentCore (Harness, Memory, Gateway, policy engine) and the tool plane (six targets, three Lambdas, one knowledge base, three external systems). A user request enters at the left and every external effect exits at the bottom, through exactly one Gateway. &lt;/p&gt;

&lt;h3&gt;
  
  
  Static hosting
&lt;/h3&gt;

&lt;p&gt;The frontend is a Next.js chat UI, statically exported. There is no Next.js server anywhere: the build produces plain files and configuration values like the Cognito client ID and the API endpoint are baked into the bundle at build time, because a static export has no server to read environment variables at request time. &lt;/p&gt;

&lt;p&gt;Those files sit in a private S3 bucket with all public access blocked, served exclusively through CloudFront with Origin Access Control. &lt;br&gt;
The bucket policy grants &lt;code&gt;s3:GetObject&lt;/code&gt; to the CloudFront service principal only, condition-scoped to this one distribution's ARN and it deliberately never grants &lt;code&gt;s3:ListBucket&lt;/code&gt;That omission has a visible side effect worth knowing about: a request for a missing object returns 403 rather than 404, because without list permission S3 refuses to confirm that an object doesn't exist. Both status codes map to &lt;code&gt;/index.html&lt;/code&gt; in the distribution's custom error responses, which is exactly what a single-page app needs for deep links to work.&lt;br&gt;
What this region proves: the frontend is a normal static site with no server-side rendering in it. Every dynamic call goes through the “Dynamic Content” described next. The series doesn't spend a dedicated article on this region because none of it is AgentCore-specific and a well established pattern. &lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Content: Cognito, API Gateway and a streaming relay
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0y9zr1kxzh91vy4mzor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0y9zr1kxzh91vy4mzor.png" alt="A piece of the content delivery architecture" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;End users authenticate against a Cognito user. The frontent app client is public with no client secret, because a browser SPA cannot keep a secret anyway. One configuration detail cost me a debugging session and is worth passing on: the client's enabled auth flows needed SRP added [4], because AWS Amplify's &lt;code&gt;signIn()&lt;/code&gt; defaults to the SRP flow and without that flow enabled the stock Amplify login path fails against an otherwise perfectly healthy pool.&lt;/p&gt;

&lt;p&gt;The chat itself is a single streaming POST endpoint on a REGIONAL REST API. In front of it sits a native &lt;code&gt;COGNITO_USER_POOLS&lt;/code&gt; authorizer that validates the caller's ID token before any compute runs. That placement buys a property I later confirmed in CloudWatch: a rejected or unauthenticated request produces zero billed Lambda invocations, because rejection happens at the authorizer, not inside application code you'd have to write, test and pay for.&lt;/p&gt;

&lt;p&gt;Behind the authorizer sits the streaming relay and it is the project's one deliberate exception to an otherwise Python-only Lambda rule: it runs on Node.js because AWS Lambda response streaming is available only on Node managed runtimes [5]. The integration uses the function's response-streaming invoke ARN with the transfer mode set to STREAM, so the harness's output reaches the browser as a progressive Server-Sent-Events stream instead of one buffered blob at the end. A 90-second function timeout absorbs the first-token latency of a turn where the model calls several tools before it starts answering. The relay's other job is identity and it's load-bearing enough that a whole section below is devoted to the alternative it beat.&lt;/p&gt;

&lt;p&gt;A second, non-streaming Lambda serves the supporting routes (&lt;code&gt;/sessions&lt;/code&gt;, &lt;code&gt;/preferences&lt;/code&gt;) behind the same authorizer, backed by a small DynamoDB table holding per-user memory preference flags.&lt;/p&gt;

&lt;p&gt;What this region proves: authentication is enforced at the API boundary, streaming is a first-class transport decision rather than an afterthought, and none of it required AgentCore-specific code.&lt;/p&gt;

&lt;p&gt;AWS's own reference architecture suggests a leaner architecture than mine: skip API Gateway and the relay Lambda entirely and let the browser call the harness directly [6]. The harness supports this through a native inbound JWT authorizer. You point it at an OIDC discovery URL, allow-list your Cognito app client and the browser invokes the harness over HTTPS with a plain Bearer token [7]; the AgentCore SDK even ships a real browser build with streaming support. I evaluated this and rejected it for one reason. The authorizer only gates the call: it validates the token's signature, issuer and audience, then lets the request through. It never binds any claim from that token to the request's &lt;code&gt;actorId&lt;/code&gt;, the field AgentCore Memory partitions users by. &lt;code&gt;actorId&lt;/code&gt; arrives as a plain payload field the client controls, so any authenticated user could send someone else's value and read that user's memory. AWS's own security guidance says as much: derive user identifiers from the authenticated principal, never from client-supplied values [8][9]. Closing the gap would mean verifying the JWT and deriving &lt;code&gt;actorId&lt;/code&gt; inside the agent code. The relay already does exactly that in one place: &lt;code&gt;deriveActorId()&lt;/code&gt; reads only the Cognito-verified sub claim, server-side and fails closed if it's missing. Deleting the relay wouldn't remove that logic; it would just move it somewhere with less separation from the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AgentCore block: Harness, Memory, Gateway, Policy
&lt;/h3&gt;

&lt;p&gt;Four managed pieces make up the agent itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexfc46skl7hcypdccc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexfc46skl7hcypdccc3.png" alt="AgentCore Harness, it's configuration and capabilities" width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The harness is the agent loop: you hand it a model, a system prompt, tools, skills and a memory binding and AgentCore runs the loop that decides when to answer and when to call a tool. This project's harness runs the LLM Model Claude Sonnet 5 through the &lt;code&gt;eu.anthropic.claude-sonnet-5&lt;/code&gt; cross-region inference profile on AWS Bedrock. The harness is container-backed for exactly one reason: Skills are filesystem-path artifacts and a purely declarative harness has no filesystem to put them on, so the project's BANT-qualification Skill ships baked into a container image in ECR. The two other options for hosting skills (on S3 or a Git repository) where not supported by the Terrafrom AWS provider at the time of the demo creation [11]. &lt;/p&gt;

&lt;p&gt;AgentCore Memory gives the loop short-term session events plus four long-term memory strategies, which is what lets a user close the browser, come back and continue where they left off. &lt;/p&gt;

&lt;p&gt;The Gateway is the single MCP endpoint the harness calls tools through and the Cedar policy engine (also used in Amazon Verified Permissions) is bound to that Gateway in ENFORCE mode, evaluating every single call before it reaches a target. &lt;/p&gt;

&lt;p&gt;The structural decision that shapes everything else in the diagram: every external side effect the model can produce routes through that one Gateway. Which tools the model can even see is an allowlist on the harness. Whether a given call passes at all is a Cedar rule. The goal with this implementation is to reduce the blast radius of a misbehaving or manipulated model and bind it to a reviewable configuration and not only a system prompt text. &lt;/p&gt;

&lt;p&gt;Wiring the harness to that Gateway is one small configuration step: on the harness, add a tool of type Gateway, point it at the Gateway, and set outbound authentication to IAM. &lt;/p&gt;

&lt;p&gt;Two details here preview later articles, and they apply identically whether you configure this in the console or in code. The tool name &lt;code&gt;pc&lt;/code&gt; is deliberately terse: AWS caps each tool-permission glob string at 64 characters and the tool name is part of every glob, so every character spent on a pretty name is a character taken from the tool names it has to match. A later article covers how I found that out, which involved the model narrating entirely fictional tool calls in prose while matching zero real tools. Outbound authentication set to IAM means the harness signs its own Gateway calls with its execution role's credentials. There is deliberately no OAuth configuration and no AgentCore Identity resource anywhere in this project, because it was not needed for this Demo architecture, but it can be useful in other setups. &lt;br&gt;
What this region proves: the model's judgment is not the only thing standing between a user's prompt and a live CRM write.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbx9cr9xgaaktiz3bq8dh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbx9cr9xgaaktiz3bq8dh.png" alt="AWS Management Console for AgentCore" width="799" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Six Gateway targets behind one door
&lt;/h3&gt;

&lt;p&gt;A "Single Gateway" does not mean "one tool". It means one path out into six governed targets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;What the model gets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-opps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partner Central Lambda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list_matching_opportunities&lt;/code&gt;: find opportunities by company name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get-opp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partner Central Lambda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_opportunity&lt;/code&gt;: the full record plus a BANT-source view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reasoning Lambda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reason_about&lt;/code&gt;: open-ended questions against Partner Central MCP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hs-search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HubSpot Lambda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;search_contact&lt;/code&gt;: look up a contact before proposing a write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hs-write&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HubSpot Lambda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;upsert_contact&lt;/code&gt;: the one write path in the whole system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;managed-kb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Knowledge Base connector&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Retrieve&lt;/code&gt; and &lt;code&gt;AgenticRetrieveStream&lt;/code&gt;: grounded document search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few of these carry details worth knowing even at map level. The &lt;code&gt;get-opp&lt;/code&gt; tool returns the opportunity plus a &lt;code&gt;bant_source_fields&lt;/code&gt; view in which any missing field renders as the literal string "Not stated in opportunity data": the qualification the model produces later must distinguish evidence from absence and that contract is enforced in the tool's own output rather than left to the model's discretion. The &lt;code&gt;hs-write&lt;/code&gt; target is guarded by a Cedar rule that rejects any call missing an explicit confirmation flag, which a later article dissects along with what that guard honestly does and does not verify. The &lt;code&gt;managed-kb&lt;/code&gt; target is an ordinary Gateway target from the console's point of view. &lt;br&gt;
The obvious architecture question: both HubSpot and Partner Central ship their own MCP servers, so why is every target a Lambda instead of a direct MCP connection? Two different blockers and naming them matters because they generalize. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; HubSpot's MCP server mandates OAuth Authorization-Code with PKCE, which at build time collided with an open AgentCore Identity bug for custom OAuth2 providers [10]&lt;/li&gt;
&lt;li&gt; Partner Central's MCP server speaks a &lt;code&gt;sendMessage&lt;/code&gt;/&lt;code&gt;getSession&lt;/code&gt; transport that doesn't match the Gateway's session handshake, a genuine protocol mismatch no configuration can bridge; so the reasoning Lambda makes the SigV4-signed MCP call itself, one hop and the Gateway routes to the Lambda.
A future article resolves both stories in full, including the outbound-auth support matrix that independently forces the same design.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What this region proves: a governed Lambda wrapper is not an admission that MCP integration failed. It's frequently the only path that satisfies auth, transport and review requirements at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The backing Lambdas
&lt;/h3&gt;

&lt;p&gt;Every tool call terminates in a piece of infrastructure with its own scoped IAM role, not in code embedded in the harness. That sentence sounds obvious and is the quiet backbone of the whole security story.&lt;/p&gt;

&lt;p&gt;Three Lambda functions back the five Gateway targets. The Partner Central Lambda holds two deterministic opportunity tools. The reasoning Lambda holds the open-ended &lt;code&gt;reason_about&lt;/code&gt; tool and is the only principal in the workload account that can assume a cross-account role into the PartnerCentral AWS account. The HubSpot Lambda holds both HubSpot tools and is the only principal that can read the HubSpot token stored in AWS Secrets Manager. This reduces the attack surface in case any other component is compromised. &lt;/p&gt;

&lt;p&gt;This is also where the harness's own identity boundary shows. The harness executes as one role, which covers model calls, Memory access and signing Gateway requests. It has no HubSpot scope, no Partner Central scope or no Secrets Manager scope. Capability lives with the tool, not with the agent, so granting the agent a new ability is a scoped change to one Lambda's role, reviewable on its own, whether you manage roles in the console or in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The external systems
&lt;/h3&gt;

&lt;p&gt;Two live systems sit at the bottom of the diagram and neither of them is a mock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HubSpot&lt;/strong&gt; is a real demo CRM instance, reached through its REST API with a Private App token holding exactly two scopes: contacts read and contacts write. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partner Central&lt;/strong&gt; is the real AWS-side system, reached through its MCP server. Its client is pinned to &lt;code&gt;us-east-1&lt;/code&gt; because Partner Central exposes no &lt;code&gt;eu-central-1&lt;/code&gt; endpoint, a mismatch with the rest of the demo stack's region. &lt;br&gt;
AWS cross-account access: the demo account's two Partner-Central-facing Lambda roles assume a single, read-only IAM role in the Partner Central AWS account. That role's permission set stays deliberately narrow and it carries no write actions of any kind: this agent can read and reason about opportunities and it structurally cannot create or modify them on the AWS side.&lt;/p&gt;

&lt;p&gt;What this region proves: the demo exercises two genuinely external systems with real credentials and real failure modes, which is where most of the build's surprises came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there is no database in that diagram
&lt;/h2&gt;

&lt;p&gt;Look at the diagram once more and notice what's absent. No Aurora. No OLTP database. No vector store. The only DynamoDB table holds boolean preference flags. For a system that reads, qualifies and writes sales data, that absence is the second-biggest architectural decision after the Gateway chokepoint.&lt;/p&gt;

&lt;p&gt;The reasoning: HubSpot and Partner Central are already the systems of record for their own data. Building a third store that mirrors them would recreate the exact synchronization problem the agent exists to remove and it would need schema, migrations, backups and a sync job to stay honest. The demo architecture reference is explicit that an independent system of record was ruled out of scope on purpose: the agent operates on the two live systems and the only data this project owns outright is a folder of documents.&lt;/p&gt;

&lt;p&gt;Those documents, AWS Partner Central material plus the partner's own sales-enablement content, ground the agent through a Bedrock Managed Knowledge Base. The managed variant reduces the entire setup to choosing the managed type and accepting the service's own defaults for embedding model, chunking, and encryption. So, there is no index to design and no retrieval code to write. It also brings a native Gateway connector, which means retrieval enters the agent through the same gateway as every other tool. A subsequent article shows the full setup, including the web crawler sources layered on top of the S3.&lt;/p&gt;

&lt;p&gt;Before reusing this pattern, ask the question it depends on: do I already have systems of record or am I about to make my agent's database the system of record? This architecture only stays this small because the answer here was the first one. A project without upstream systems of record needs a real data layer and no Gateway diagram makes that requirement go away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The road I didn't take: calling the harness straight from the browser
&lt;/h2&gt;

&lt;p&gt;Every article in this series names at least one path I evaluated and rejected, with the specific reason. Here is the first.&lt;br&gt;
During the implementation, the relay Lambda started to look like too formal setup. AgentCore Harness supports a native inbound JWT authorizer: configure it with an OIDC discovery URL, allow-list of your Cognito client and a browser can call the harness directly over HTTPS with a plain Bearer token. No API Gateway, no relay Lambda, two fewer pieces of infrastructure. This is not an obscure option. The AgentCore SDK is published with a genuine browser build, streaming responses are supported on the direct call and AWS's own reference architectures wire frontends to agents exactly this way [6].&lt;/p&gt;

&lt;p&gt;I read the developer guide closely before deleting anything and one gap decided the question. The inbound authorizer authenticates the call. It verifies the token's signature, issuer and audience and then lets the request through. What it does not do is bind any claim from that token to the request's &lt;code&gt;actorId&lt;/code&gt;, the field that partitions AgentCore Memory between users. &lt;code&gt;actorId&lt;/code&gt; is a plain payload field, structurally identical however the caller authenticated and nothing server-side ties it to the identity that logged in. AWS's own security documentation is candid about the adjacent mechanism, describing the related user-ID header as an opaque identifier that is not verified against an authenticated identity and prescribing exactly the mitigation you'd expect: derive the user identifier from the authenticated principal, never from client-supplied values [8].&lt;/p&gt;

&lt;p&gt;Follow the consequence through. With direct browser calls, any authenticated user could put any &lt;code&gt;actorId&lt;/code&gt; they like in the payload and read the agent's memory of another user: their past sessions' context, their preferences, whatever the memory strategies have accumulated. The relay closes this by construction. Its &lt;code&gt;deriveActorId()&lt;/code&gt; reads only the Cognito-verified &lt;code&gt;sub&lt;/code&gt; claim from the validated token, server-side and fails closed if the claim is absent. The client is unaffected and has no surface to send a wrong &lt;code&gt;actorId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the lambda relay stays and the decision reframed what the relay is. It's not a middleman, it's a component that guarantees isolation. I verified the property live  and a second Cognito user replaying a stolen session ID receives none of the first user's memory context.&lt;/p&gt;

&lt;p&gt;The transferable lesson is the method, not the conclusion. The option was real, supported and documented by AWS itself. It still lost to a specific problem after reviewing the security. Sometimes a preference is not a decision you can make. &lt;/p&gt;

&lt;h2&gt;
  
  
  Transferable patterns
&lt;/h2&gt;

&lt;p&gt;Three habits from this article generalize beyond this demo to any agent build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single Gateway as a blast-radius control.&lt;/strong&gt; Route every external side effect the model can trigger through one governed endpoint, so the callable tool set, each tool's permissions and the per-call guard rules all live in reviewable configuration. The alternative, capability scattered across the agent's own code and prompt, has no configuration surface to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"No independent system of record" as a scoping heuristic.&lt;/strong&gt; Before building your agent a database, check whether the systems you're integrating are already the systems of record. If they are, a mirror store is scope you don't need and a consistency bug you haven't had yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate-and-reject as a habit.&lt;/strong&gt; For each major design choice, name the more idiomatic-looking alternative, check it against primary documentation and record the specific reason it lost. The Inbound Auth story above is the first in this series. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you are reproducing this
&lt;/h2&gt;

&lt;p&gt;Three things will bite you in the first hour if you stand this architecture up yourself, console or code. All three come from this project's own build history.&lt;/p&gt;

&lt;p&gt;First, two AWS accounts are involved and only one of them is yours to automate. Every resource in the diagram lives in the workload account. The Partner Central AWS account holds exactly one created, read-only IAM role that the workload account's two Partner-Central-facing Lambda roles assume into. Create that role first, in the Partner Central account's IAM console and list both Lambda execution role ARNs explicitly as trusted principals in its trust policy: granting &lt;code&gt;sts:AssumeRole&lt;/code&gt; on the workload side alone is not sufficient and the failure mode is a runtime AccessDenied long after setup looked complete.&lt;/p&gt;

&lt;p&gt;Second, model access. Claude Sonnet 5 is a marketplace-gated Bedrock model whose usage agreement must be accepted once per account before any tool-using invocation succeeds. I learned this from a live &lt;code&gt;AccessDeniedException&lt;/code&gt; referencing &lt;code&gt;aws-marketplace:Subscribe&lt;/code&gt; on a full-admin session, which ruled out an IAM misconfiguration and pointed at a genuinely unaccepted offer. Accept the agreement once (the Bedrock console's model-access page is the direct route), make sure the identity doing it carries &lt;code&gt;aws-marketplace:Subscribe&lt;/code&gt; and &lt;code&gt;aws-marketplace:ViewSubscriptions&lt;/code&gt; and note that the agreement machinery lives in us-east-1 regardless of your workload region. As a bonus for automation-minded readers: A recent update of the AWS terraform provider lets you reproduce this with a terraform resource. &lt;/p&gt;

&lt;p&gt;Third, ordering. A container-backed harness references a container image that must already exist in ECR before the harness itself can be created, which is a genuine chicken-and-egg on a fresh account: create the ECR repository first, build and push the image (&lt;code&gt;linux/arm64&lt;/code&gt; is required) and only then create the harness. I found this on the first live terraform apply. The same ordering applies in the console; automation just makes it easier to get wrong in one shot.&lt;/p&gt;

&lt;p&gt;I hope this article was use-full for you. I would love to receive feedback on what you liked and disliked, so that I can improve any future article. &lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;AWS Partner Central v3 availability. &lt;a href="https://aws.amazon.com/de/blogs/aws/aws-partner-central-now-available-in-aws-management-console/" rel="noopener noreferrer"&gt;https://aws.amazon.com/de/blogs/aws/aws-partner-central-now-available-in-aws-management-console/&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;AWS Partner Central agents now guide new partners from registration to ready-to-sell
. &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/aws-partner-central/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/06/aws-partner-central/&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Amazon Bedrock AgentCore Harness general availability. &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-bedrock-agentcore-harness-generally-available/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-bedrock-agentcore-harness-generally-available/&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;AWS Amplify documentation: &lt;code&gt;signIn()&lt;/code&gt; uses the SRP flow by default. &lt;a href="https://docs.amplify.aws/react/frontend/auth/switching-authentication-flows/" rel="noopener noreferrer"&gt;https://docs.amplify.aws/react/frontend/auth/switching-authentication-flows/&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;AWS Lambda documentation: response streaming is available on Node.js managed runtimes only. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS reference implementations of the direct browser-to-agent pattern: the aws-samples/sample-amazon-bedrock-agentcore-fullstack-webapp starter template (&lt;a href="https://github.com/aws-samples/sample-amazon-bedrock-agentcore-fullstack-webapp" rel="noopener noreferrer"&gt;https://github.com/aws-samples/sample-amazon-bedrock-agentcore-fullstack-webapp&lt;/a&gt;) and the AWS Builders article "Skip the Middleman: Connecting Your UI Directly to an AI Agent via WebSocket" (&lt;a href="https://dev.to/aws-builders/skip-the-middleman-connecting-your-ui-directly-to-an-ai-agent-via-websocket-29f7"&gt;https://dev.to/aws-builders/skip-the-middleman-connecting-your-ui-directly-to-an-ai-agent-via-websocket-29f7&lt;/a&gt;). &lt;/li&gt;
&lt;li&gt;AWS Bedrock AgentCore developer guide. "Configure inbound JWT authorizer": &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;AWS Bedrock AgentCore developer guide. "Authenticate and authorize with Inbound Auth and Outbound Auth": &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;AWS Bedrock AgentCore developer guide. "Get workload access token": &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/get-workload-access-token.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/get-workload-access-token.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub, &lt;code&gt;aws/bedrock-agentcore-sdk-python&lt;/code&gt; issue #158: open AgentCore Identity bug for custom OAuth2 providers. &lt;a href="https://github.com/aws/bedrock-agentcore-sdk-python/issues/158" rel="noopener noreferrer"&gt;https://github.com/aws/bedrock-agentcore-sdk-python/issues/158&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Missing skill support for S3 and Git as source. &lt;a href="https://github.com/hashicorp/terraform-provider-aws/issues/48540" rel="noopener noreferrer"&gt;https://github.com/hashicorp/terraform-provider-aws/issues/48540&lt;/a&gt; &lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
