<?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>Grounding an Agent Without a Vector Database: AgentCore Managed Knowledge Base</title>
      <dc:creator>Oliver Bühler</dc:creator>
      <pubDate>Tue, 01 Sep 2026 09:19:24 +0000</pubDate>
      <link>https://dev.to/olibhlr/grounding-an-agent-without-a-vector-database-agentcore-managed-knowledge-base-480e</link>
      <guid>https://dev.to/olibhlr/grounding-an-agent-without-a-vector-database-agentcore-managed-knowledge-base-480e</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I grounded this agent without setting up a vector database. One Bedrock Managed Knowledge Base takes documents from a private S3 bucket and three web-crawled sources. It is passed to the model as two additional tools used through the same Gateway as all other tools. The key point is that no Terraform provider includes a resource type for a Gateway connector target. So the managed-kb target is created by a small boto3 script inside a terraform_data resource. This workaround works but loses drift detection and requires manual order specification.&lt;/p&gt;

&lt;p&gt;The sixth article in the "Building a Partner Sales Agent on Amazon Bedrock AgentCore" series, based on a real-world project, focuses on developing a chat-based agent that integrates HubSpot CRM with AWS Partner Central for an AWS partner's sales team. This particular article focuses on the grounding layer: it makes use of one Bedrock Managed Knowledge Base, which is fed information from an S3 bucket and three web crawler data sources, and this knowledge base is made available to the agent via the same Gateway that was introduced in article 4. &lt;/p&gt;

&lt;p&gt;The agent's answers didn't have to use a vector database; instead, two types of ingestion were directed at one Managed Knowledge Base.&lt;/p&gt;

&lt;p&gt;As the first article pointed out, this system has no database since HubSpot and Partner Central remain the official records. The only data the project owns is a folder of documents. This article focuses on that folder, the three public websites crawled using it, and one setup aspect no Terraform provider could represent at build time. It builds on the Gateway-target mechanism because retrieval is added as two additional tools within the same controlled environment. It also prepares for Article 7, where the live demo uses this foundation to answer competency and sales-case questions.&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%2Fj4uuot8kux3zzwfhxu2c.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%2Fj4uuot8kux3zzwfhxu2c.png" alt="KB Overview" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Managed, not classic KB plus a vector store?
&lt;/h2&gt;

&lt;p&gt;There are two configurations of Bedrock Knowledge Bases and they require very different levels of decision-making on your part [1]. The traditional option asks you to make the following choices: an embedding model ARN, a storage backend such as an OpenSearch Serverless collection or an Aurora PostgreSQL table together with pgvector, and a chunking strategy for each data source. These choices are important when the quality of your retrieval depends on them, and merely constitute the operational interface when it doesn't. The Managed option eliminates all of these choices: you select the type of managed knowledge base, assign it a service role, and the service then provides its own embedding model, chunking, and encryption defaults [1].&lt;/p&gt;

&lt;p&gt;In the Bedrock console, that reduces KB creation to a name and a role. One constraint hides in the role and is easy to trip over: its name must begin with &lt;code&gt;AmazonBedrockExecutionRoleForKnowledgeBase_&lt;/code&gt;. I found that requirement buried in the resource schema's own attribute description. The role itself needs &lt;code&gt;s3:GetObject&lt;/code&gt; and &lt;code&gt;s3:ListBucket&lt;/code&gt; on the documents bucket so ingestion can read what you upload, and nothing else to start with.&lt;/p&gt;

&lt;p&gt;In the automated build, the entire Knowledge Base is this:&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_bedrock_knowledge_base"&lt;/span&gt; &lt;span class="s2"&gt;"kb"&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-kb"&lt;/span&gt;
  &lt;span class="nx"&gt;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;kb_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="c1"&gt;# name must start with AmazonBedrockExecutionRoleForKnowledgeBase_&lt;/span&gt;

  &lt;span class="nx"&gt;knowledge_base_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MANAGED"&lt;/span&gt;
    &lt;span class="c1"&gt;# The empty object is the complete configuration: service-managed&lt;/span&gt;
    &lt;span class="c1"&gt;# embedding model, chunking, and encryption defaults.&lt;/span&gt;
    &lt;span class="nx"&gt;managed_knowledge_base_configuration&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty &lt;code&gt;managed_knowledge_base_configuration = {}&lt;/code&gt; is the whole point of the design. I checked the CloudFormation resource-type schema before trusting it: &lt;code&gt;ManagedKnowledgeBaseConfiguration&lt;/code&gt; declares no required fields at all, so &lt;code&gt;{}&lt;/code&gt; is a valid, complete configuration rather than a lucky one. One automation footnote a console reader never sees: &lt;code&gt;hashicorp/aws&lt;/code&gt; (6.54.0 at build time) has no &lt;code&gt;managed_knowledge_base_configuration&lt;/code&gt; block on its knowledge base resource, a gap that was tracked upstream and has since closed (the fix merged in July 2026 for provider v6.56.0) [2], which is why every KB resource in this build uses the Cloud-Control-backed &lt;code&gt;hashicorp/awscc&lt;/code&gt; provider instead.&lt;/p&gt;

&lt;p&gt;The trade has limits, and a reader with different requirements should know where they sit. A Managed KB offers no tunable chunking strategy and no choice of embedding model. If you need a specific embedding model, a custom chunk size, or an index shared across knowledge bases, the classic path is the right call and this article's zero-config trade stops being available. This project's corpus, AWS Partner Central documentation plus Tallence sales-enablement material, needed none of that, so the trade went the other way. Article 1's "no independent system of record" framing already positioned the KB this way: HubSpot, Partner Central, and this KB are the only data sources the project touches, and the KB is the one that isn't a live external system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source one: the S3 documents bucket
&lt;/h2&gt;

&lt;p&gt;The first data source is a private S3 bucket holding AWS Partner Central and Tallence sales-enablement documents that ground the demo's answers. Public access is blocked, objects are encrypted at rest with SSE-S3. Documents are uploaded directly via console.&lt;/p&gt;

&lt;p&gt;The key operational property is that dropping a file into this bucket triggers ingestion. An S3 &lt;code&gt;ObjectCreated&lt;/code&gt; event invokes a small ingestion Lambda that calls &lt;code&gt;StartIngestionJob&lt;/code&gt;. There is no console button to press or sync script to run afterward. The full mechanism and its one gotcha are explained under "If you are reproducing this" below.&lt;/p&gt;

&lt;p&gt;In the Bedrock console, attaching the bucket is a step on the Knowledge Base's data-source tab: add an S3 data source with a name, bucket, optional prefix, and bucket owner's account ID, which the console derives for same-account buckets. Attaching the same bucket via Terraform revealed a surprise unseen by console users: the connector's connection configuration requires &lt;code&gt;bucketOwnerAccountId&lt;/code&gt;. The first apply without it passed creation but failed asynchronous validation with &lt;code&gt;Member must not be null&lt;/code&gt;. The failure appeared later in the data source's &lt;code&gt;failureReasons&lt;/code&gt;, a pattern repeated at larger scale in this article.&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_bedrock_data_source"&lt;/span&gt; &lt;span class="s2"&gt;"kb_s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;knowledge_base_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;awscc_bedrock_knowledge_base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;knowledge_base_id&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;"s3-documents"&lt;/span&gt;

  &lt;span class="nx"&gt;data_source_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MANAGED_KNOWLEDGE_BASE_CONNECTOR"&lt;/span&gt;

    &lt;span class="nx"&gt;managed_knowledge_base_connector_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;connector_parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3"&lt;/span&gt;
        &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
        &lt;span class="nx"&gt;connectionConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;bucketName&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
          &lt;span class="nx"&gt;bucketOwnerAccountId&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_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt; &lt;span class="c1"&gt;# required, not optional&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;aclEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="nx"&gt;filterConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;maxFileSizeInMegaBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"500"&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;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0rjdv6fi93kjagz01hpt.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%2F0rjdv6fi93kjagz01hpt.png" alt="AWS AgentCore Managed Knowlegebase" width="799" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Source two: three web crawler data sources
&lt;/h2&gt;

&lt;p&gt;The corpus also needed current public content: the Partner Central documentation tree at &lt;code&gt;docs.aws.amazon.com/partner-central/&lt;/code&gt;, an APN blog post on applying for an AWS Competency, and the APN partner program pages. The requirement was a deep crawl, depth 10, restricted tightly to each seed's own URL path.&lt;/p&gt;

&lt;p&gt;Whether that requirement is expressible depends on the type of KB created, because AWS provides two structurally distinct web crawler schemas. The classic KB's crawler has no crawl-depth field; it offers a page limit, rate limit, and a scope enum of &lt;code&gt;HOST_ONLY&lt;/code&gt; or &lt;code&gt;SUBDOMAINS&lt;/code&gt; [3]. The Managed KB's connector schema is richer: &lt;code&gt;crawlConfiguration.crawlDepth&lt;/code&gt; accepts 0 to 10 with a default of 2, and &lt;code&gt;syncScope = "PATH_SPECIFIC"&lt;/code&gt; restricts the crawl to the seed URL's host and path prefix [4]. Depth 10 was achievable exactly as requested, but only because the parent KB was Managed. On a classic KB, the design would have required compromise from the start.&lt;/p&gt;

&lt;p&gt;In the console, each crawler is added from that same data-source tab as a data source of the web crawler type, and the fields that follow are the ones its form asks for: a name, a seed URL, the sync scope, the crawl depth, and inclusion patterns. Per data source, the fields that matter: a seed URL, &lt;code&gt;authType = "NO_AUTH"&lt;/code&gt; for a public site, the crawl depth, the sync scope, &lt;code&gt;maxCrawledUrlsPerMinute&lt;/code&gt; (50 here), &lt;code&gt;crawlAttachments = false&lt;/code&gt;, and an inclusion regex. There are three data sources rather than one with three seeds for a schema reason: &lt;code&gt;inclusionPatterns&lt;/code&gt; applies to the whole data source, not per seed [4]. Two of the three seeds sit on the broad &lt;code&gt;aws.amazon.com&lt;/code&gt; marketing host, so each needed its own path-prefix regex as scope reduction, and each regex needed its own resource. &lt;/p&gt;

&lt;p&gt;One of the three, condensed to the fields above:&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_bedrock_data_source"&lt;/span&gt; &lt;span class="s2"&gt;"kb_web_partner_central_docs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;knowledge_base_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;awscc_bedrock_knowledge_base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;knowledge_base_id&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;"web-partner-central-docs"&lt;/span&gt;

  &lt;span class="nx"&gt;data_source_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MANAGED_KNOWLEDGE_BASE_CONNECTOR"&lt;/span&gt;

    &lt;span class="nx"&gt;managed_knowledge_base_connector_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;connector_parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"WEB"&lt;/span&gt;
        &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
        &lt;span class="nx"&gt;connectionConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;seedUrls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://docs.aws.amazon.com/partner-central/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NO_AUTH"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;crawlConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;crawlDepth&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
          &lt;span class="nx"&gt;maxCrawledUrlsPerMinute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
          &lt;span class="nx"&gt;syncScope&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PATH_SPECIFIC"&lt;/span&gt;
          &lt;span class="nx"&gt;crawlAttachments&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;filterConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;inclusionPatterns&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^https://docs&lt;/span&gt;&lt;span class="err"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.aws&lt;/span&gt;&lt;span class="err"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.amazon&lt;/span&gt;&lt;span class="err"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.com/partner-central/.*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nx"&gt;maxFileSizeInMegaBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"500"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;aclEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two smaller seeds reached ingestion &lt;code&gt;COMPLETE&lt;/code&gt; quickly, with zero failures. The Partner Central docs seed took roughly 1 hour 50 minutes and ended at 1,271 documents scanned: 615 new and 36 modified documents indexed, 619 skipped as out of scope, and exactly 1 failure, a rate of 0.08%.&lt;/p&gt;

&lt;p&gt;Before running your own crawl, note this counter behavior: &lt;code&gt;numberOfDocumentsScanned&lt;/code&gt; kept rising long after &lt;code&gt;numberOfNewDocumentsIndexed&lt;/code&gt; plateaued [5]. This is not a stall. &lt;code&gt;crawlDepth&lt;/code&gt; limits how far the crawler may wander from the seed but says nothing about elapsed time. Every discovered link, including out-of-scope ones, must be visited before being confirmed out of scope and marked skipped. Thus, a seed on a large host drags a link frontier much larger than its indexed corpus. In this crawl, 619 of 1,271 scanned documents existed only to be confirmed skippable.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Scanned
    Scanned --&amp;gt; InScope: matches syncScope + inclusionPatterns
    Scanned --&amp;gt; OutOfScope: fails scope or inclusion check
    InScope --&amp;gt; Indexed: chunk + embed succeed
    InScope --&amp;gt; Failed: ingestion error (cause not exposed)
    OutOfScope --&amp;gt; Skipped
    Indexed --&amp;gt; [*]
    Skipped --&amp;gt; [*]
    Failed --&amp;gt; [*]: only a top-level failureReasons message, no per-document detail&lt;/code&gt;&lt;/pre&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%2Fa2c3cezb23bhn4426vlh.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%2Fa2c3cezb23bhn4426vlh.png" alt="Crawler Detail" width="800" height="979"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The managed-kb Gateway target: an escape hatch, not a resource
&lt;/h2&gt;

&lt;p&gt;Ingestion fills the Knowledge Base. A Gateway connector target is what puts it in the agent's hands: it turns an existing AWS resource into MCP tools directly, with no Lambda in between, unlike the five Lambda-backed targets article 4 walked through [6].&lt;/p&gt;

&lt;p&gt;In the AgentCore console, this is an ordinary target. Open the Gateway, add a target, choose the Knowledge Base connector (connector ID &lt;code&gt;bedrock-knowledge-bases&lt;/code&gt;), point it at the KB, and select the Gateway's own IAM role as the credential provider [6]. Nothing about the console path hints at anything unusual; the escape hatch this section is named after is strictly an automation-path finding, and a console-only reader can stop worrying about it here.&lt;/p&gt;

&lt;p&gt;The target, named &lt;code&gt;managed-kb&lt;/code&gt;, exposes two tools under the same &lt;code&gt;pc&lt;/code&gt; tool block as every other target: &lt;code&gt;Retrieve&lt;/code&gt;, a single basic search, and &lt;code&gt;AgenticRetrieveStream&lt;/code&gt;, a multi-step agentic retrieval that this project's system prompt routes competency and sales-case questions to. On the harness allowlist they appear as &lt;code&gt;@pc/managed-kb___Retrieve&lt;/code&gt; and &lt;code&gt;@pc/managed-kb___AgenticRetrieveStream&lt;/code&gt;. One configuration choice to copy: &lt;code&gt;generateResponse = false&lt;/code&gt;, so the tool returns retrieved passages instead of synthesizing its own answer text, and the agent's system prompt formats the end-of-answer "Sources:" citations itself. One citation format across every tool beats a second answer-generator inside a tool.&lt;/p&gt;

&lt;p&gt;Now the automation gap. Neither &lt;code&gt;hashicorp/aws&lt;/code&gt; (6.54.0) nor &lt;code&gt;hashicorp/awscc&lt;/code&gt; (1.92.0) ships any resource type for a Gateway connector target [7]; if you search this build's Terraform for a sixth gateway-target resource, you will not find one. The workaround is a &lt;code&gt;terraform_data&lt;/code&gt; resource [8] whose create and destroy provisioners call a checked-in Python script, which calls &lt;code&gt;create_gateway_target&lt;/code&gt; through boto3:&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;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"kb_gateway_target"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;triggers_replace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&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_id&lt;/span&gt;
    &lt;span class="nx"&gt;kb_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;awscc_bedrock_knowledge_base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;knowledge_base_id&lt;/span&gt;
    &lt;span class="nx"&gt;target_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"managed-kb"&lt;/span&gt;
    &lt;span class="nx"&gt;region&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;aws_region&lt;/span&gt; &lt;span class="c1"&gt;# destroy-time provisioners may only reference self&lt;/span&gt;
    &lt;span class="c1"&gt;# Forces recreation if the retriever/citation config changes.&lt;/span&gt;
    &lt;span class="nx"&gt;config_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* retrievers, agenticRetrieveConfiguration, generateResponse */&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python ${path.module}/scripts/gateway_kb_target.py --action create --gateway-id ${aws_bedrockagentcore_gateway.agent.gateway_id} --kb-id ${awscc_bedrock_knowledge_base.kb.knowledge_base_id} --name managed-kb --region ${var.aws_region}"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;when&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;destroy&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python ${path.module}/scripts/gateway_kb_target.py --action destroy --gateway-id ${self.triggers_replace["&lt;/span&gt;&lt;span class="nx"&gt;gateway_id&lt;/span&gt;&lt;span class="s2"&gt;"]} --name ${self.triggers_replace["&lt;/span&gt;&lt;span class="nx"&gt;target_name&lt;/span&gt;&lt;span class="s2"&gt;"]} --region ${self.triggers_replace["&lt;/span&gt;&lt;span class="nx"&gt;region&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why a Python script instead of shelling out to the AWS CLI, which would need no file at all? A version discovery I now look for routinely: the installed AWS CLI (2.27.57) bundled a botocore that did not model the &lt;code&gt;connector&lt;/code&gt; member of the target configuration's tagged union at all. Its help listed only &lt;code&gt;openApiSchema&lt;/code&gt;, &lt;code&gt;smithyModel&lt;/code&gt;, &lt;code&gt;lambda&lt;/code&gt;, &lt;code&gt;mcpServer&lt;/code&gt;, and &lt;code&gt;apiGateway&lt;/code&gt;, which looks exactly like "the API doesn't support this yet." The pip-installed boto3 already in the project (1.43.36) modeled &lt;code&gt;mcp.connector&lt;/code&gt; fully. The CLI and your Python dependencies ship separate botocore copies on separate release cadences.&lt;/p&gt;

&lt;p&gt;The payload had one correction to teach. I had &lt;code&gt;generateResponse&lt;/code&gt; nested inside &lt;code&gt;agenticRetrieveConfiguration&lt;/code&gt;, and the live call rejected it with a &lt;code&gt;ValidationException&lt;/code&gt; naming that exact path. The devguide places it as a sibling of &lt;code&gt;retrievers&lt;/code&gt; and &lt;code&gt;agenticRetrieveConfiguration&lt;/code&gt;, directly under &lt;code&gt;parameterValues&lt;/code&gt; [6][9]. Don't count on client-side validation to catch this class of mistake: &lt;code&gt;parameterValues&lt;/code&gt; is an untyped Document shape in botocore, so nothing validates its contents before the call, and the asynchronous READY poll is the only feedback loop that catches a malformed payload. With the field moved, the target reached &lt;code&gt;READY&lt;/code&gt; on the next attempt in about 7 seconds.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_target_configuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kb_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp&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;connector&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;source&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;connectorId&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;bedrock-knowledge-bases&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;configurations&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;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;AgenticRetrieveStream&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;parameterValues&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;retrievers&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;description&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;AWS Partner Central competencies + Tallence sales-enablement corpus&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;configuration&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;knowledgeBase&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;knowledgeBaseId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kb_id&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;agenticRetrieveConfiguration&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;foundationModelType&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;MANAGED&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;rerankingModelType&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;MANAGED&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="c1"&gt;# Sibling of retrievers, NOT nested inside
&lt;/span&gt;                            &lt;span class="c1"&gt;# agenticRetrieveConfiguration -- the live call
&lt;/span&gt;                            &lt;span class="c1"&gt;# rejects the nested shape by exact path.
&lt;/span&gt;                            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generateResponse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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="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;Retrieve&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;parameterValues&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;knowledgeBaseId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kb_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="p"&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;create_gateway_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gatewayIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gateway_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;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;managed-kb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;targetConfiguration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_target_configuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kb_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;credentialProviderConfigurations&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;credentialProviderType&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;GATEWAY_IAM_ROLE&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="c1"&gt;# ...then poll get_gateway_target until READY or FAILED. This poll is the
# only validation feedback the connector target gives you.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two limits of the escape hatch, stated plainly. &lt;code&gt;terraform_data&lt;/code&gt; has no read or refresh, so there is no drift detection: a change made directly against the AWS API will never appear in a plan, and only the fields in &lt;code&gt;triggers_replace&lt;/code&gt; force recreation. And ordering needed one explicit &lt;code&gt;depends_on&lt;/code&gt;: the script's READY poll requires &lt;code&gt;bedrock:GetKnowledgeBase&lt;/code&gt; on the Gateway role, and nothing else in the graph ordered that policy before the target. &lt;/p&gt;

&lt;p&gt;The connector's IAM lives on the Gateway's service role, in three statements. &lt;code&gt;bedrock:GetKnowledgeBase&lt;/code&gt; (which the READY validation needs) and &lt;code&gt;bedrock:Retrieve&lt;/code&gt; are both scoped to the one Knowledge Base's ARN. The third cannot be:&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;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AgenticRetrieveStream"&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:AgenticRetrieveStream"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS does not support scoping &lt;code&gt;bedrock:AgenticRetrieveStream&lt;/code&gt; to a single Knowledge Base [10]. &lt;code&gt;Resource = "*"&lt;/code&gt; is the only grant that works: the action is read-only retrieval, and it is the only broad grant on that role. The generalizable rule sits: accept a wildcard where AWS forces it, and write down why it couldn't be narrower, right next to the grant.&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%2F11wsm93tlh8704uk2sk6.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%2F11wsm93tlh8704uk2sk6.png" alt="Ciation in the Chat" width="800" height="1042"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Six habits from this subsystem hold beyond this demo to any AgentCore build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed KB as a provider-selection heuristic.&lt;/strong&gt; If a knowledge source needs no custom chunking strategy and no specific embedding model, the empty &lt;code&gt;managed_knowledge_base_configuration&lt;/code&gt; object is the fastest, lowest-operational-surface way to stand up retrieval. Save the classic path for when a real requirement forces a specific index or embedding choice, and name that requirement before paying for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One data source per seed URL.&lt;/strong&gt; Because &lt;code&gt;inclusionPatterns&lt;/code&gt; scopes to a whole data source rather than a single seed, a project crawling N independent sites needs N data source resources, each having its own inclusion regex. Plan for that from the start, rather than discovering it mid-build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify-then-import beats retry on a Cloud Control false negative.&lt;/strong&gt; When a &lt;code&gt;CreateResource&lt;/code&gt; waiter reports &lt;code&gt;InternalFailure&lt;/code&gt; but the resource plausibly finished, check the underlying service API directly before touching Terraform again. A blind retry collides with the resource that already exists; a verified &lt;code&gt;terraform import&lt;/code&gt; doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform_data&lt;/code&gt; plus a checked-in script is the escape hatch, not a hack.&lt;/strong&gt; When no provider resource exists yet for an AWS capability, wrapping a direct boto3 call in &lt;code&gt;terraform_data&lt;/code&gt; keeps the action inside Terraform's apply graph and state instead of in a runbook, without waiting months for provider coverage. Know what you give up: no refresh, no drift detection, and ordering you must declare yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept &lt;code&gt;Resource = "*"&lt;/code&gt; only where AWS forces it, and say so.&lt;/strong&gt; &lt;code&gt;AgenticRetrieveStream&lt;/code&gt; is the one place in this project's IAM where scoping to a single ARN isn't possible. Granting the wildcard is fine; granting it without a comment explaining why it couldn't be narrower isn't.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Two things will cost you time here, and neither is a defect.&lt;/p&gt;

&lt;p&gt;Uploading a document to the KB bucket is the entire ingestion trigger. In this build, the upload's S3 &lt;code&gt;ObjectCreated&lt;/code&gt; event invokes an ingestion Lambda that calls &lt;code&gt;StartIngestionJob&lt;/code&gt; for you, so there is no separate manual "start ingestion" step. If the upload succeeded, the job already started; watch its progress in the Bedrock console's data source view or with &lt;code&gt;aws bedrock-agent get-ingestion-job&lt;/code&gt;. If you rebuild without that event wiring, the equivalent is one &lt;code&gt;StartIngestionJob&lt;/code&gt; call after each upload, and nothing more.&lt;/p&gt;

&lt;p&gt;Budget hours, not minutes, for a broad seed at &lt;code&gt;crawlDepth = 10&lt;/code&gt;. The crawler visits every discovered out-of-scope link to confirm it is out of scope before marking it skipped, so the scanned count keeps rising well after new indexed documents stop appearing, and a documentation tree the size of Partner Central's runs close to two hours end to end. That is expected behavior, not a stall, and it is not a rebuild step to run twenty minutes before an audience arrives.&lt;/p&gt;

&lt;p&gt;That closes the last building block. &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 Knowledge Bases: knowledge base types and configuration, covering the managed type against the classic/vector path's embedding-model, vector-store, and chunking requirements. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-build-managed.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/kb-build-managed.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-2"&gt;&lt;/a&gt;[2] hashicorp/terraform-provider-aws issue #48744: no &lt;code&gt;managed_knowledge_base_configuration&lt;/code&gt; support on the knowledge base resource at build time (provider 6.54.0); the gap closed when PR #48904 merged on 2026-07-16, milestoned for v6.56.0. &lt;a href="https://github.com/hashicorp/terraform-provider-aws/issues/48744" rel="noopener noreferrer"&gt;https://github.com/hashicorp/terraform-provider-aws/issues/48744&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-3"&gt;&lt;/a&gt;[3] Amazon Bedrock web crawler connector for classic Knowledge Bases: page limits, rate limit, and the HOST_ONLY/SUBDOMAINS scope enum, with no crawl-depth field. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/webcrawl-data-source-connector.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/webcrawl-data-source-connector.html&lt;/a&gt;; &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_WebCrawlerConfiguration.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_WebCrawlerConfiguration.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-4"&gt;&lt;/a&gt;[4] Managed Knowledge Base web connector parameters: &lt;code&gt;crawlConfiguration.crawlDepth&lt;/code&gt; (0-10, default 2), &lt;code&gt;syncScope&lt;/code&gt; values including PATH_SPECIFIC, and per-data-source &lt;code&gt;inclusionPatterns&lt;/code&gt;. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-ds-webcrawler.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/kb-managed-ds-webcrawler.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-5"&gt;&lt;/a&gt;[5] Amazon Bedrock &lt;code&gt;GetIngestionJob&lt;/code&gt; API reference: ingestion job statistics fields (&lt;code&gt;numberOfDocumentsScanned&lt;/code&gt;, &lt;code&gt;numberOfNewDocumentsIndexed&lt;/code&gt;, &lt;code&gt;numberOfModifiedDocumentsIndexed&lt;/code&gt;, &lt;code&gt;numberOfDocumentsFailed&lt;/code&gt;) and top-level &lt;code&gt;failureReasons&lt;/code&gt;. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_GetIngestionJob.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_GetIngestionJob.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: the managed knowledge base Gateway connector target, including the administrator-set fields &lt;code&gt;retrievers&lt;/code&gt;, &lt;code&gt;agenticRetrieveConfiguration&lt;/code&gt;, and &lt;code&gt;generateResponse&lt;/code&gt;. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-managed-kb.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-managed-kb.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-7"&gt;&lt;/a&gt;[7] Terraform provider coverage for AgentCore Gateway connector targets. &lt;a href="https://github.com/hashicorp/terraform-provider-aws/issues/48503" rel="noopener noreferrer"&gt;https://github.com/hashicorp/terraform-provider-aws/issues/48503&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-8"&gt;&lt;/a&gt;[8] Terraform documentation: the &lt;code&gt;terraform_data&lt;/code&gt; managed resource. &lt;a href="https://developer.hashicorp.com/terraform/language/resources/terraform-data" rel="noopener noreferrer"&gt;https://developer.hashicorp.com/terraform/language/resources/terraform-data&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-9"&gt;&lt;/a&gt;[9] Amazon Bedrock AgentCore developer guide: Gateway target configuration shapes for API targets. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-add-target-api-target-config.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-add-target-api-target-config.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-10"&gt;&lt;/a&gt;[10] Service Authorization Reference for Amazon Bedrock: the &lt;code&gt;bedrock:AgenticRetrieveStream&lt;/code&gt; action and its resource-scoping. &lt;a href="https://docs.aws.amazon.com/service-authorization/latest/reference/list_bedrock.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/service-authorization/latest/reference/list_bedrock.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>agentcore</category>
      <category>bedrock</category>
      <category>rag</category>
    </item>
    <item>
      <title>A Deterministic Backstop for Your Agent: AgentCore Policy and Cedar</title>
      <dc:creator>Oliver Bühler</dc:creator>
      <pubDate>Wed, 26 Aug 2026 20:25:02 +0000</pubDate>
      <link>https://dev.to/olibhlr/a-deterministic-backstop-for-your-agent-agentcore-policy-and-cedar-2h3a</link>
      <guid>https://dev.to/olibhlr/a-deterministic-backstop-for-your-agent-agentcore-policy-and-cedar-2h3a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I bound a Cedar policy engine to the Gateway from the last article and shipped two real guards: one rejects a get_opportunity call with no opportunity_id, the other rejects a HubSpot write unless confirmed_by_presenter is present and true. Both are enforced outside the model’s own reasoning so that no prompt injection can talk its way past them. The honest limit matters as much: Cedar only ever sees one call’s own parameters, never conversation history, so the write guard verifies that the approval flag is set, not that a human actually gave the approval it claims to represent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part 5 of the series “Building a Partner Sales Agent on Amazon Bedrock AgentCore”, built around one real project: a dialog-based agent that connects HubSpot CRM and AWS Partner Central for an AWS Partner’s sales team. My last article opened the Gateway that every external call passes through; this article covers the Cedar policy engine bound to that Gateway, what it deterministically blocks and, just as precisely, what it cannot see.&lt;/p&gt;

&lt;p&gt;A policy engine is precisely the type of feature that often leads to overstatements about system security. When terms like “Cedar” and “deterministic enforcement” appear together, it is easy for readers to infer that the agent is comprehensively protected against all forms of misuse, including prompt injection and model errors. However, this is not the case. To clarify the technical boundaries: AgentCore Policy in this project enforces two specific guardrails that are immune to prompt injection, but its scope is tightly limited. The engine evaluates individual tool calls in isolation and has no access to the broader conversation context or history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AgentCore Policy is
&lt;/h2&gt;

&lt;p&gt;AgentCore Policy is a Cedar-language authorization engine that attaches to an AgentCore Gateway, not to the harness. Every tool call the harness sends through the Gateway is evaluated against the engine’s policies before it reaches a target. The evaluation happens entirely outside the agent’s own code, after the model has already decided to make the call. That placement is the property that matters: a prompt-injected or plain misbehaving model cannot reason its way around a check it never participates in.&lt;/p&gt;

&lt;p&gt;This project ships two real policies. One forbids &lt;code&gt;get_opportunity&lt;/code&gt; calls that arrive without an &lt;code&gt;opportunity_id&lt;/code&gt;, a plain input guard on the Partner Central read path. The other forbids &lt;code&gt;upsert_contact&lt;/code&gt; calls, the single write path in the whole system, unless &lt;code&gt;confirmed_by_presenter&lt;/code&gt; is present and true; the deployed field name keeps the build-time word for the sales rep driving the demo.&lt;/p&gt;

&lt;p&gt;Thirty seconds on how Cedar evaluates a request in general, before this project’s specifics, because one detail of it produced the build’s biggest surprise. A Cedar request is allowed only when at least one &lt;code&gt;permit&lt;/code&gt; policy matches it and no forbid policy does. An explicit &lt;code&gt;forbid&lt;/code&gt; always beats any &lt;code&gt;permit&lt;/code&gt;. Also a request that no policy speaks about at all gets the engine’s default answer, which is deny [1]. Hold on to that last clause: the next section shows what it did to a live &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the AgentCore console, Policy has its own page under Build, next to Harness, Gateway, and Memory. Creating an engine takes a name and a description; this project’s is &lt;code&gt;partner_growth_agent_policy&lt;/code&gt;. The policies live inside the engine, each is a name, a description, and a Cedar statement entered directly. Nothing on the engine itself points at a Gateway. The association is configured on the Gateway, as a policy engine reference plus an enforcement mode [2] and that mode field holds more importance than its size suggests; the “Watching it work” section is built around it.&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%2F1rtnwis13l5a0ecivizg.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%2F1rtnwis13l5a0ecivizg.png" alt="project's policy engine (partner_growth_agent_policy)" width="800" height="821"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why add a policy engine to a demo at all, rather than describe one? Three reasons decided it here. It was already fully modeled in the pinned Terraform provider (hashicorp/aws 6.54.0). It adds a control that lives in reviewable configuration rather than in prompt text, which extends article 1’s blast-radius argument one layer deeper. Lastly it holds on the day the model’s judgment fails or becomes manipulated, which, as the numbers later in this article show, is not a hypothetical day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default-deny and the baseline permit it forces
&lt;/h2&gt;

&lt;p&gt;The plan was modest: associate the engine with the Gateway and ship one forbid rule, the &lt;code&gt;get_opportunity&lt;/code&gt; input guard. The first apply did not get past creating the policy. It failed with this, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Overly Restrictive: Policy Engine will deny every request for the specified principal/action/resource combination if the policy is added.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a create-time analysis, not a runtime failure and it fired for both principal types named in the error (IamEntity and OAuthUser). The service worked out what the engine’s policy set would decide with only a forbid in it, concluded the answer was “deny everything”, and refused to create the policy at all. The logic follows directly from Cedar’s default answer: with no permit anywhere. The forbid was beside the point, because every call on every target was already headed for the default deny. At the time this Gateway had four targets (&lt;code&gt;list-opps&lt;/code&gt;, &lt;code&gt;get-opp&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;managed-kb&lt;/code&gt;), and associating a forbid-only engine in enforcing mode would have silenced all four, not just the malformed calls the forbid was aimed at.&lt;/p&gt;

&lt;p&gt;The fix is a baseline permit covering every target on the Gateway. Here is the shipped statement, condensed to the Cedar block; it lists six actions today because the two HubSpot targets from article 4 joined the list when they were wired in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permit (
  principal,
  action in [
    AgentCore::Action::"list-opps",
    AgentCore::Action::"get-opp",
    AgentCore::Action::"reason",
    AgentCore::Action::"managed-kb",
    AgentCore::Action::"hs-search",
    AgentCore::Action::"hs-write"
  ],
  resource == AgentCore::Gateway::"&amp;lt;gateway_arn&amp;gt;"
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action names are Gateway target names, not tool names: a target-level action works as an action group covering every tool registered under that target, and since Cedar has no wildcard action, one group per target is the documented way to keep the list short [3]. And principal is left bare on purpose. This Gateway authenticates callers with &lt;code&gt;AWS_IAM&lt;/code&gt;, and the only role granted &lt;code&gt;bedrock-agentcore:InvokeGateway&lt;/code&gt; on it is the harness’s execution role, so IAM has already decided who can show up here. A Cedar principal condition would restate that decision without restricting anything.&lt;/p&gt;

&lt;p&gt;For the automation path, ordering is the finding. Nothing in the forbid resource references the permit resource, so Terraform’s graph has no implicit edge between them, and a parallel apply can race the forbid’s create-time validation ahead of the permit and land in the exact failure quoted above. My shipped demo code pins the order with an explicit &lt;code&gt;depends_on = [aws_bedrockagentcore_policy.pc_permit_existing_targets]&lt;/code&gt; on each forbid. A console reader gets the same rule in simpler form: create the baseline permit before any forbid, because the server runs the same analysis no matter how the policy arrives.&lt;/p&gt;

&lt;p&gt;Associating the engine with the Gateway gave my a surprise. &lt;code&gt;UpdateGateway&lt;/code&gt;, the operation the association rides on, failed five separate times with five distinct missing grants on the Gateway’s own execution role, discovered one live AccessDenied at a time: &lt;code&gt;bedrock-agentcore:GetPolicyEngine&lt;/code&gt; on the policy-engine ARN, then &lt;code&gt;AuthorizeAction&lt;/code&gt; and &lt;code&gt;PartiallyAuthorizeActions&lt;/code&gt;, each needed twice, once against the policy-engine ARN and once against the Gateway’s own ARN. None of this surfaces in &lt;code&gt;terraform validate&lt;/code&gt;. Two additional &lt;code&gt;CREATE_FAILED&lt;/code&gt; attempts on &lt;code&gt;PartiallyAuthorizeActions&lt;/code&gt; turned out to be IAM propagation delay rather than missing grants; the identical, already-attached policy applied cleanly on a later attempt.&lt;/p&gt;

&lt;p&gt;Default-deny also leaves a standing rule. Any new target added to this Gateway must be added to the baseline permit in the same change, or it is denied the moment it exists. When the HubSpot targets arrived, the permit was extended first and the new forbid added after it, repeating the same permit-plus-forbid pairing and the same &lt;code&gt;depends_on&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cedar decision flow
&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%2Fmbpusq9noxjw3a1pb0u8.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%2Fmbpusq9noxjw3a1pb0u8.png" alt="decision flow" width="800" height="1221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reading top to bottom: a tool call arrives at the Gateway carrying the harness’s principal, and in enforcing mode nothing routes onward until the engine has answered. If no permit matches, evaluation is effectively over: deny, before any forbid is consulted. Inside the space the permit opens, each forbid is tested against the call’s own &lt;code&gt;context.input&lt;/code&gt;. A firing forbid produces an explicit deny that names the specific policy. A call that survives both is routed to its target Lambda or the knowledge-base connector.&lt;/p&gt;

&lt;p&gt;One precision note so the diagram doesn’t overstate itself: Cedar formally evaluates the whole policy set and combines the results, allowing a request only when some permit matches and no forbid does [1]. For a policy set of one permit and two forbids, the sequential rendering above is decision-equivalent. It makes the load-bearing fact visible: a forbid can only narrow what a permit already allowed. The baseline permit’s presence and scope set the ceiling before any forbid gets a say. That is why the permit is not boilerplate. It is the half of the policy set that decides whether the Gateway works at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cedar can and cannot see
&lt;/h2&gt;

&lt;p&gt;A policy in this engine receives exactly two things about a request: the calling principal, and &lt;code&gt;context.input&lt;/code&gt;, the current call’s own parameters [4]. The principal carries no information here, and I mean that literally. This Gateway authenticates with &lt;code&gt;AWS_IAM&lt;/code&gt;, every call is signed by the same harness execution role, and so every request arrives as the same principal regardless of who is chatting or what the conversation is doing. That leaves &lt;code&gt;context.input&lt;/code&gt; as the only discriminating signal a policy can act on: one call, its own arguments, nothing else.&lt;/p&gt;

&lt;p&gt;Not conversation history. Not which turn or flow a call belongs to. Not whether a human approved anything. I ran into the shape of this limit directly: a first-draft policy referenced a &lt;code&gt;context.request_type&lt;/code&gt; attribute, meaning “is this call happening during a BANT (budget, authority, need, timeline) qualification flow”, and it turned out simply not writable against the real Cedar schema. No such attribute exists and no policy syntax supports one.&lt;/p&gt;

&lt;p&gt;That kills the most tempting use of the engine. Live sampling during the build measured the model calling &lt;code&gt;reason_about&lt;/code&gt; during BANT qualification despite a system prompt telling it never to: 2 of 6 sampled runs (roughly 33 percent) under the initial version single-paragraph instruction, and 1 of 9 (roughly 11 percent) after the prompt was restructured into short, isolated, repeated rules. The restructuring did all of that work. Cedar contributed nothing to the reduction, and it cannot close the remaining 11 percent, because the violating call is a well-formed, legitimate-looking &lt;code&gt;reason_about&lt;/code&gt; invocation, indistinguishable at the Gateway from a sanctioned use of the same tool one turn later. A blanket forbid on &lt;code&gt;reason_about&lt;/code&gt; would take the legitimate open-ended-reasoning capability down with it. “This call is part of a BANT flow” represents conversational state, as conversational state never reaches the engine. The 11 percent stays a documented, accepted prompt-level residual, and I would rather write that sentence than let a policy engine imply coverage it does not have.&lt;/p&gt;

&lt;p&gt;The write guard deserves the same scrutiny. Here are both shipped forbid rules together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forbid (
  principal,
  action == AgentCore::Action::"get-opp___get_opportunity",
  resource == AgentCore::Gateway::"&amp;lt;gateway_arn&amp;gt;"
) unless { context.input has opportunity_id };

forbid (
  principal,
  action == AgentCore::Action::"hs-write___upsert_contact",
  resource == AgentCore::Gateway::"&amp;lt;gateway_arn&amp;gt;"
) unless {
  context.input has confirmed_by_presenter &amp;amp;&amp;amp;
  context.input.confirmed_by_presenter == true
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action names use the &lt;code&gt;target___tool&lt;/code&gt; convention from last article’s story. The two rules both originated as &lt;code&gt;forbid ... unless { context.input has &amp;lt;field&amp;gt; }&lt;/code&gt; statements. It became clear during code review that the construct only checks for the presence of a key, not its value [1]. As a result, the initial write guard would permit calls where &lt;code&gt;confirmed_by_presenter&lt;/code&gt; was set to false. Although the model's prompt did not direct it to submit false, and such scenarios were considered unreachable in standard operation, the rationale for updating the policy was to ensure its independence from any assumptions about model behavior. Given that the purpose of this backstop is to enforce guarantees independent of the model’s instructions or actions, it was necessary to amend the policy so that the field not only must be present, but specifically must be true. &lt;/p&gt;

&lt;p&gt;Now the honest limit of the fixed version: the guard verifies the flag, not the approval. Cedar sees that &lt;code&gt;confirmed_by_presenter=true&lt;/code&gt; is on the call. It cannot see whether a sales rep actually typed an approval two turns earlier, because that is conversation, and conversation sits on the wrong side of the boundary. A model that attached the flag without any real approval behind it would pass this guard. The approval flow itself is prompt-level; Cedar’s contribution is narrower and sharper. Writes without the flag cannot happen, deterministically, no matter what the model narrates. Writes with a falsely attached flag are out of its reach. &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%2Fmflvrk9hquc4h7gws1ck.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%2Fmflvrk9hquc4h7gws1ck.png" alt="Whhat is visible" width="799" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it work
&lt;/h2&gt;

&lt;p&gt;A claim about enforcement should be tested at the enforcement boundary, so I verified both modes with direct MCP calls against the Gateway’s own endpoint, no model anywhere in the loop. That was a deliberate choice, not a shortcut. Persuading a model into producing a malformed tool call proves that the model can be compromised on that particular day; it proves nothing about the policy. A signed call I construct myself isolates the one variable the test is about.&lt;/p&gt;

&lt;p&gt;The Gateway-level association supports two modes, &lt;code&gt;LOG_ONLY&lt;/code&gt; and &lt;code&gt;ENFORCE&lt;/code&gt; [2]: the first evaluates and records, the second evaluates and blocks. Under &lt;code&gt;LOG_ONLY&lt;/code&gt;, a hand-built &lt;code&gt;get_opportunity&lt;/code&gt; call with no &lt;code&gt;opportunity_id&lt;/code&gt; still reached the Lambda while the evaluation recorded a &lt;code&gt;DenyDecisions&lt;/code&gt; count of 1. That is the contract: the denial exists only as a metric while the call still goes through, which makes &lt;code&gt;LOG_ONLY&lt;/code&gt; the safe first gear for a policy set you have not yet watched against live traffic. After that smoke pass, the mode was flipped. In the console this is the enforcement mode on the Gateway’s policy engine association; in the automated build it is one attribute:&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;policy_engine_configuration&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="nx"&gt;aws_bedrockagentcore_policy_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy_engine_arn&lt;/span&gt;
  &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ENFORCE"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;code&gt;ENFORCE&lt;/code&gt;, the identical malformed call was rejected before the Lambda ever ran, zero Lambda invocations logged, with an explicit MCP error naming the policy that fired, while a well-formed call in the same setup still reached Partner Central’s real API.&lt;/p&gt;

&lt;p&gt;The write guard skipped its own &lt;code&gt;LOG_ONLY&lt;/code&gt; period, and the reason is an automation-path finding rather than a preference. A per-policy enforcement mode exists in the imperative API, but the Terraform provider does not expose it (only &lt;code&gt;validation_mode&lt;/code&gt; appears in the resource schema), so the Gateway-level mode is this build’s only staged-rollout knob. By the time the write guard landed, that mode was already &lt;code&gt;ENFORCE&lt;/code&gt; and protecting the live &lt;code&gt;get_opportunity&lt;/code&gt; guard. The guard was verified under &lt;code&gt;ENFORCE&lt;/code&gt; instead: a direct tools/call to &lt;code&gt;hs-write___upsert_contact&lt;/code&gt; omitting &lt;code&gt;confirmed_by_presenter&lt;/code&gt;, bypassing the model entirely, came back denied with &lt;code&gt;pc_hubspot_upsert_contact_requires_confirmation&lt;/code&gt; named in the error.&lt;/p&gt;

&lt;p&gt;Know where to watch: the signal is metrics, not logs. Policy decisions surface in the &lt;code&gt;AWS/Bedrock-AgentCore&lt;/code&gt; CloudWatch metrics namespace as &lt;code&gt;DenyDecisions&lt;/code&gt; and &lt;code&gt;AllowDecisions&lt;/code&gt;, with Policy, Mode, and TargetResource dimensions [6]. No policy-engine-named log group exists to search; I looked and no such log group is ever populated. Span-level detail on individual denials would require Gateway tracing.&lt;/p&gt;

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

&lt;p&gt;Five habits from this subsystem generalize to any AgentCore build, whether console-managed or automated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default-deny forces permit-before-forbid, explicitly ordered.&lt;/strong&gt; Associating a default-deny engine with only your intended forbid rules blocks every call on every target, and the service refuses the policy set at create time. Write a baseline permit for every existing target first. In Terraform, order it with an explicit &lt;code&gt;depends_on&lt;/code&gt;, because no attribute reference exists between a forbid and the permit for the graph to infer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State what a control cannot see, in the same document that introduces it.&lt;/strong&gt; Cedar has no cross-turn conversational state. Naming that limit plainly costs one paragraph and saves a reader from assuming a policy engine closes every gap a system prompt leaves open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify a policy by bypassing the model.&lt;/strong&gt; A direct call against the Gateway’s own endpoint proves an enforcement decision. A model-driven test proves only the model’s behavior on that day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for metrics, not a policy-named log group.&lt;/strong&gt; The live signal is &lt;code&gt;DenyDecisions&lt;/code&gt;/&lt;code&gt;AllowDecisions&lt;/code&gt; in the &lt;code&gt;AWS/Bedrock-AgentCore&lt;/code&gt; namespace. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  For readers seeking to replicate this subsystem, the following guidance builds on the design details and implementation strategies discussed throughout the preceding sections.
&lt;/h2&gt;

&lt;p&gt;Four concrete notes for standing this subsystem up yourself, all from this build’s own history.&lt;/p&gt;

&lt;p&gt;First, create the baseline permit before any forbid, whichever path you take. The create-time analysis rejects a forbid-only policy set server-side, so the ordering rule applies in the console just as it does in Terraform; automation only adds the extra step of pinning the order with depends_on, since the two policy resources share no attribute reference.&lt;/p&gt;

&lt;p&gt;Second, grant the Gateway’s execution role the complete set of policy-engine permissions at the outset. The required IAM actions and their corresponding resources are summarized in the following table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;IAM Action&lt;/th&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bedrock-agentcore:GetPolicyEngine&lt;/td&gt;
&lt;td&gt;Policy Engine ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bedrock-agentcore:AuthorizeAction&lt;/td&gt;
&lt;td&gt;Policy Engine ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bedrock-agentcore:AuthorizeAction&lt;/td&gt;
&lt;td&gt;Gateway ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bedrock-agentcore:PartiallyAuthorizeActions&lt;/td&gt;
&lt;td&gt;Policy Engine ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bedrock-agentcore:PartiallyAuthorizeActions&lt;/td&gt;
&lt;td&gt;Gateway ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;During implementation, missing these permissions led to five distinct live failures that were only resolved by granting all five actions in advance . When specifying the Gateway-scoped ARN, construct it from the account ID and the static Gateway name with a wildcard suffix; avoid references to the resource attribute to prevent dependency cycles, and do not rely on a literal ARN captured from a previous apply since this may break upon resource re-creation. If PartiallyAuthorizeActions fails immediately after the permissions are granted, allow additional time before troubleshooting further, as two of the observed failures were attributable to IAM propagation delays rather than missing grants.&lt;/p&gt;

&lt;p&gt;Third, treat the permit’s action list as a hand-maintained contract. The strings in the Cedar statement are separated from the name attribute on each Gateway target, so renaming a target silently drops it from the permit, and under default-deny that means every call to the renamed target fails, discoverable only live, never by &lt;code&gt;terraform plan&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Fourth, plan your verification around metrics and a direct call, as described above. If you need per-denial detail beyond the metric dimensions, budget for enabling Gateway tracing; the policy engine will not give it to you otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implications for Subsequent Articles
&lt;/h2&gt;

&lt;p&gt;In summary, the current analysis has established both the strengths and inherent limitations of AgentCore Policy and Cedar within the agentic-AI framework. This discussion sets a clear foundation for the focus of future articles in the series. Article 6 will explore the integration of the Managed Knowledge Base, demonstrating how retrieval is subject to the same policy-driven boundaries as CRM write operations. The last Article will offer a comprehensive, live demonstration incorporating the full policy stack discussed here, including the approval workflow and its deterministic safeguards. By articulating the specific guarantees and constraints of the policy engine, this article provides critical insights that inform the scope and direction of the remaining installments. Future contributions will build upon these findings, ensuring that ongoing discourse remains grounded in the transparent capabilities and limitations of the current approach.&lt;/p&gt;

&lt;p&gt;The honest remark about AgentCore Policy is the sales pitch and the disclaimer in one: two guards no prompt can bypass, judging exactly one call at a time, blind to everything the conversation knows.&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;p&gt;Sources&lt;/p&gt;

&lt;p&gt;&lt;a id="source-1"&gt;&lt;/a&gt;[1] Cedar policy language documentation: authorization semantics (a request is allowed only if at least one permit matches and no forbid does; an explicit forbid overrides any permit; the default decision is deny) and the has operator’s key-presence-only semantics. &lt;a href="https://docs.cedarpolicy.com/" rel="noopener noreferrer"&gt;https://docs.cedarpolicy.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-2"&gt;&lt;/a&gt;[2] Amazon Bedrock AgentCore Developer Guide, Policy enforcement modes: the Gateway policy-engine association’s &lt;code&gt;LOG_ONLY&lt;/code&gt; and &lt;code&gt;ENFORCE&lt;/code&gt; enforcement modes. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-enforcement-modes.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-enforcement-modes.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, example policies: worked examples pairing input-guard forbid rules with baseline permits, and target-level actions grouping the tools registered under a Gateway target. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/example-policies.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/example-policies.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-4"&gt;&lt;/a&gt;[4] Amazon Bedrock AgentCore Developer Guide, Policy core concepts: what a policy evaluates (the calling principal and the tool call’s input) and the default-deny behavior. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-core-concepts.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-core-concepts.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-5"&gt;&lt;/a&gt;[5] AWS IAM User Guide, “The confused deputy problem”, including the &lt;code&gt;aws:SourceAccount&lt;/code&gt; mitigation for cross-service trust. &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.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, Policy observability data: &lt;code&gt;AllowDecisions&lt;/code&gt; / &lt;code&gt;DenyDecisions&lt;/code&gt; invocation metrics in the &lt;code&gt;AWS/Bedrock-AgentCore&lt;/code&gt; CloudWatch namespace, with &lt;code&gt;Policy&lt;/code&gt;, &lt;code&gt;Mode&lt;/code&gt;, and &lt;code&gt;TargetResource&lt;/code&gt; dimensions. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-policy-metrics.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-policy-metrics.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>agentcore</category>
      <category>cedar</category>
      <category>bedrock</category>
    </item>
    <item>
      <title>One Gateway, Six Tools: AgentCore Gateway as Your Agent's Only Way Out</title>
      <dc:creator>Oliver Bühler</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:45:52 +0000</pubDate>
      <link>https://dev.to/olibhlr/one-gateway-six-tools-agentcore-gateway-as-your-agents-only-way-out-gdp</link>
      <guid>https://dev.to/olibhlr/one-gateway-six-tools-agentcore-gateway-as-your-agents-only-way-out-gdp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I opened up the single AgentCore Gateway this agent calls every tool through: six targets behind one MCP endpoint, backed by three Lambdas and a managed knowledge base connector, with both HubSpot and Partner Central wrapped in Lambda instead of connected as MCP servers because of an auth incompatibility and a transport mismatch. The sharpest finding was the allowed_tools glob syntax: get it wrong and the model doesn't error, it just narrates fake tool calls while the Lambda logs stay empty. The only way I caught it was watching Converse token counts sit flat at 411 tokens no matter what I typed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part 4 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. The last article configured the harness runtime; this article opens the single Gateway that runtime calls every tool through. Everything described here is deployed and running (and every setup step is described as console configuration first).&lt;/p&gt;

&lt;p&gt;The most expensive line in this project's Terraform is a list of seven strings. &lt;code&gt;allowed_tools&lt;/code&gt;, the harness-side filter that decides which Gateway tools the model is allowed to see, uses a glob syntax documented in the AgentCore developer guide and nowhere in the Terraform provider docs. Until I found the right page, the deployed agent answered every request by narrating tool calls that never happened with an invented parameter name. Nothing threw an error. The Lambda logs stayed empty.&lt;/p&gt;

&lt;p&gt;This article walks you through the tool plane that line gates: one Gateway with six targets, the two external systems that both ship their own MCP servers yet still ended up behind Lambda functions, the input envelope those Lambdas actually receive, and the permissions the Gateway's own IAM role demanded (that no &lt;code&gt;terraform validate&lt;/code&gt; linted). The first article promised a single chokepoint through which every external effect passes. Here it is, field by field.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Gateway, six targets
&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%2Fk8l20tbe3rxn6jwreb7j.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%2Fk8l20tbe3rxn6jwreb7j.png" alt="AgentCore to Gateway Architecture" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Gateway in the AgentCore console is a short form: a name, a protocol type, an authorizer type, an execution role. This project's only Gateway is &lt;code&gt;partner-growth-agent-gateway&lt;/code&gt;, with &lt;code&gt;protocol_type = "MCP"&lt;/code&gt; and &lt;code&gt;authorizer_type = "AWS_IAM"&lt;/code&gt; (the other option is &lt;code&gt;CUSTOM_JWT&lt;/code&gt;). Those two values carry most of the design. The MCP protocol type means the Gateway presents itself to the harness as a standard MCP server: the harness enumerates tools with &lt;code&gt;tools/list&lt;/code&gt; and invokes them with &lt;code&gt;tools/call&lt;/code&gt;. The IAM authorizer type means this endpoint has no end-user-facing authentication at all. Its only caller is the harness, signing every request with its execution role's SigV4 credentials through the Gateway tool block described in article 1 and opened in full below. End users never touch this endpoint; they authenticate against Cognito at a separate frontend application. Two authentication planes, kept apart on purpose: humans at the API boundary, one IAM principal at the tool boundary.&lt;/p&gt;

&lt;p&gt;In the automated build, the Gateway resource is correspondingly small:&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_gateway"&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-gateway"&lt;/span&gt;
  &lt;span class="nx"&gt;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;pc_gateway&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;authorizer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS_IAM"&lt;/span&gt;
  &lt;span class="nx"&gt;protocol_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MCP"&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&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_iam_role_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc_gateway_read_policy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;policy_engine_configuration&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="nx"&gt;aws_bedrockagentcore_policy_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy_engine_arn&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ENFORCE"&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 &lt;code&gt;policy_engine_configuration&lt;/code&gt; block attaches the next article's Cedar engine in ENFORCE mode. Note what is absent: no JWT issuer, no allowed clients, no OAuth anything.&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%2Finkcuz14sp1arsbiyncw.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%2Finkcuz14sp1arsbiyncw.png" alt="AgentCore Gateway Taget List" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Behind that one endpoint sit the six 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;Tool the model sees&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 (Python 3.13)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list_matching_opportunities&lt;/code&gt;&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;&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;&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;&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;&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;, &lt;code&gt;AgenticRetrieveStream&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six targets carry seven tools, because &lt;code&gt;managed-kb&lt;/code&gt; exposes two retrieval tools and the other five expose one each. The one-tool-per-target shape follows the provider: the provider docs I checked at build time model exactly one &lt;code&gt;inline_payload&lt;/code&gt; tool schema per target, so the two Partner Central opportunity tools became two targets on the same Gateway rather than one target holding two schemas.&lt;/p&gt;

&lt;p&gt;Two more things about that table before we go deeper into the sections. Every target name is terse because each one gets qualified into an &lt;code&gt;allowed_tools&lt;/code&gt; glob that AWS caps at 64 characters, and the original descriptive names blew the cap the moment they were qualified. The &lt;code&gt;managed-kb&lt;/code&gt; target is configured differently from its five neighbors. In the console it is an ordinary connector target on the same Gateway. In the automated build it is a &lt;code&gt;terraform_data&lt;/code&gt; resource running &lt;code&gt;local-exec&lt;/code&gt;, because neither the &lt;code&gt;hashicorp/aws&lt;/code&gt; nor the &lt;code&gt;hashicorp/awscc&lt;/code&gt; provider models a Gateway connector target yet. A console reader never sees this gap; it exists only on the automation path. Article 6 covers the knowledge base behind it.&lt;/p&gt;

&lt;p&gt;Creating a Lambda target takes five values, console or code: a target name, a description, the Lambda function ARN, a credential provider and a tool schema. Meaning the tool's name, a description the model will read and typed input and output properties. The Terraform for the &lt;code&gt;get-opp&lt;/code&gt; target, condensed to the parts that matter (next article's Cedar guard references exactly this target):&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_gateway_target"&lt;/span&gt; &lt;span class="s2"&gt;"get_opportunity"&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;"get-opp"&lt;/span&gt;
  &lt;span class="nx"&gt;gateway_identifier&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_id&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Fetch one Partner Central opportunity by id, including its BANT source-field view"&lt;/span&gt;

  &lt;span class="nx"&gt;credential_provider_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_iam_role&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;target_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mcp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lambda_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partnercentral&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;tool_schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;inline_payload&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;"get_opportunity"&lt;/span&gt;

            &lt;span class="nx"&gt;input_schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"object"&lt;/span&gt;

              &lt;span class="nx"&gt;property&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;"opportunity_id"&lt;/span&gt;
                &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;
                &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The opportunity id returned by list_matching_opportunities (a 'matched' status result)."&lt;/span&gt;
                &lt;span class="nx"&gt;required&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nx"&gt;output_schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"object"&lt;/span&gt;

              &lt;span class="nx"&gt;property&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;"Identifier"&lt;/span&gt;
                &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;
                &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="nx"&gt;property&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;"bant_source_fields"&lt;/span&gt;
                &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"object"&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="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;I condensed one field away that deserves a sentence: the tool's description string in the full version carries part of the behavior contract, telling the model that any missing BANT (budget, authority, need, timeline) field renders the literal phrase "Not stated in opportunity data" rather than a guess. The model reads schema descriptions like documentation. A tool description is configuration and prompt at the same time and it lives here in the target. The contract is stated at both layers and in the live BANT round-trip the model followed the prompt's copy.&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%2F1nv1vebkxvj2oeze5vdo.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%2F1nv1vebkxvj2oeze5vdo.png" alt="AgentCore Gateway Targets configured" width="800" height="1021"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Lambda targets, when both external systems ship MCP servers?
&lt;/h2&gt;

&lt;p&gt;The table above should raise a question. HubSpot ships a remote MCP server. AWS Partner Central ships a remote MCP server. AgentCore Gateway supports MCP-server targets. So why is every target in this project a Lambda? Two separate reasons, each sufficient on its own and all three generalize to other builds.&lt;/p&gt;

&lt;p&gt;Reason one is HubSpot's auth model. HubSpot's MCP server at &lt;code&gt;mcp.hubspot.com&lt;/code&gt; is real and speaks standard Streamable HTTP, but it mandates OAuth Authorization Code with PKCE [1]. On the AgentCore side, that flow would run through AgentCore Identity as a custom OAuth2 credential provider and at build time that path was blocked by an open bug in the AgentCore Python SDK for exactly this case, &lt;code&gt;aws/bedrock-agentcore-sdk-python&lt;/code&gt; issue #158, still open at the time of writing in August 2026 [2]. &lt;/p&gt;

&lt;p&gt;Reason two is Partner Central's transport. Partner Central's MCP server lives at &lt;code&gt;https://partnercentral-agents-mcp.us-east-1.api.aws/mcp&lt;/code&gt;, signed with SigV4 for service name &lt;code&gt;partnercentral-agents-mcp&lt;/code&gt; in us-east-1 (one of the two regions its endpoint is published in [3]). While the MCP exposes only the two tools &lt;code&gt;sendMessage&lt;/code&gt; and &lt;code&gt;getSession&lt;/code&gt;, the conversation is handled by a session ID insight the tool argument [4]. That mechanism does not match the session-ID handshake the AgentCore Gateway's MCP-server target type performs. This is a transport mismatch, not a policy choice; no configuration bridges it. The solution  is a Lambda target and the Lambda uses botocore's &lt;code&gt;AWSRequest&lt;/code&gt;. The Gateway's role in this hop is routing only. One hop, no protocol translation in the middle.&lt;/p&gt;

&lt;p&gt;A reader who has followed the series closely might propose a third path: skip the Gateway for Partner Central and use the harness's own &lt;code&gt;remote_mcp&lt;/code&gt; tool type, which connects a harness directly to any MCP endpoint by URL, no Gateway involved [6]. I checked that too. Its configuration is a URL plus an optional map of sensitive headers and its documented auth options are exactly three: no auth, a static bearer-token header or a header value resolved from an AgentCore Identity API-key credential provider [6]. There is no SigV4 signing option. Partner Central's endpoint requires a cross-account IAM role and SigV4-signed requests, which &lt;code&gt;remote_mcp&lt;/code&gt; structurally cannot produce, whatever the transport looks like. For a third-party MCP server that authenticates with a plain API-key header, &lt;code&gt;remote_mcp&lt;/code&gt; is a legitimate and much shorter path; this project just does not have one of those.&lt;/p&gt;

&lt;p&gt;Where all of this lands: every target on this Gateway is a Lambda or the managed KB connector and the project contains no OAuth configuration and no AgentCore Identity resource anywhere. &lt;/p&gt;

&lt;h2&gt;
  
  
  The allowed_tools glob syntax
&lt;/h2&gt;

&lt;p&gt;For a reader new to MCP: &lt;code&gt;tools/list&lt;/code&gt; is the JSON-RPC method an MCP client calls to enumerate a server's tools before invoking any of them. The harness performs that discovery against the Gateway, then filters the result through &lt;code&gt;allowed_tools&lt;/code&gt; before the model ever sees a tool schema. A tool that survives the filter has its schema attached to the model's request. A tool that does not is invisible. &lt;/p&gt;

&lt;p&gt;Which is why the failure mode, when the filter matches nothing, is so quiet. After every execution-role IAM gap from article 3 was fixed, the harness deployed cleanly and the chat looked alive. Asked about an opportunity, the model announced it was calling &lt;code&gt;list_matching_opportunities&lt;/code&gt;, printed a plausible invocation with a parameter named &lt;code&gt;company_name&lt;/code&gt; and summarized results it had invented. The real tool's only parameter is &lt;code&gt;spoken_name&lt;/code&gt;. The Lambda's CloudWatch log group recorded zero invocations. There was no error anywhere, because from the model's point of view nothing was wrong: it had been asked about opportunities, given no tools and produced its best imitation of an agent at work.&lt;/p&gt;

&lt;p&gt;I tried several plausible ways. Bare tool names. The Gateway's namespaced MCP names in the &lt;code&gt;&amp;lt;target&amp;gt;___&amp;lt;tool&amp;gt;&lt;/code&gt; form, which I confirmed by making a direct SigV4-signed &lt;code&gt;tools/list&lt;/code&gt; call against the Gateway's MCP endpoint. An explicit empty list. A freshly recreated harness with the field never set at all. &lt;code&gt;GetHarness&lt;/code&gt; echoed back &lt;code&gt;allowedTools: []&lt;/code&gt; in every case. The final answer: Converse input token counts were byte-identical across every attempt (a constant 411 tokens per request). Tool schemas are part of the model's input. If any schema had been attached, that number would have moved.&lt;/p&gt;

&lt;p&gt;The mechanism is documented on the AgentCore developer guide's "Tools" page, not in the Terraform provider docs, which do not describe the pattern at all [6]. &lt;code&gt;allowedTools&lt;/code&gt; entries are glob patterns with a small grammar: &lt;code&gt;@server&lt;/code&gt;, &lt;code&gt;@server/tool&lt;/code&gt;, &lt;code&gt;@server/glob&lt;/code&gt;, &lt;code&gt;@*/tool&lt;/code&gt; [6]. A bare name such as &lt;code&gt;shell&lt;/code&gt; only ever matches a builtin tool (&lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;file_operations&lt;/code&gt;). Anything discovered through a Gateway or MCP server requires the &lt;code&gt;@&lt;/code&gt; form, where &lt;code&gt;server&lt;/code&gt; is the name of the harness's own tool block and the tool is the Gateway's namespaced name. For this project that is &lt;code&gt;@pc/get-opp___get_opportunity&lt;/code&gt;: &lt;code&gt;pc&lt;/code&gt; from the harness tool block, &lt;code&gt;get-opp___get_opportunity&lt;/code&gt; from the Gateway's own &lt;code&gt;tools/list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I applied the corrected globs and the very next invocation produced a real &lt;code&gt;contentBlockStart.start.toolUse&lt;/code&gt; block with &lt;code&gt;stopReason: "tool_use"&lt;/code&gt; and the correct &lt;code&gt;spoken_name&lt;/code&gt; argument. Converse input tokens jumped above 411. &lt;/p&gt;

&lt;p&gt;One constraint remained. AWS caps each glob string at 64 characters, rejecting longer ones with a &lt;code&gt;ValidationException&lt;/code&gt;. The project's original names were individually reasonable: a tool block named &lt;code&gt;partner_central_opportunities&lt;/code&gt;, targets named &lt;code&gt;list-matching-opportunities&lt;/code&gt; and &lt;code&gt;get-opportunity&lt;/code&gt;. Qualified, the listing tool's glob would have run to 88 characters. Everything got shortened to fit (&lt;code&gt;pc&lt;/code&gt;, &lt;code&gt;list-opps&lt;/code&gt;, &lt;code&gt;get-opp&lt;/code&gt;), and every target added since was named short from day one. &lt;/p&gt;

&lt;p&gt;The declared configuration as shipped:&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;tool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"agentcore_gateway"&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;"pc"&lt;/span&gt;

  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;agentcore_gateway&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="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="nx"&gt;outbound_auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;aws_iam&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;allowed_tools&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/list-opps___list_matching_opportunities"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/get-opp___get_opportunity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/reason___reason_about"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/managed-kb___Retrieve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/managed-kb___AgenticRetrieveStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/hs-search___search_contact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"@pc/hs-write___upsert_contact"&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 two knowledge-base tools live under the same &lt;code&gt;@pc/&lt;/code&gt; prefix as everything else. The &lt;code&gt;managed-kb&lt;/code&gt; connector target hangs off the same Gateway and the same tool block, so a separate &lt;code&gt;@kb/...&lt;/code&gt; prefix would silently match zero tools, which is the exact trap this section exists to document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lambda input envelope
&lt;/h2&gt;

&lt;p&gt;The Lambda's log group finally showed invocations. Every one of them failed with &lt;code&gt;ValueError: Unknown tool name: None&lt;/code&gt;. Since I wrote the initial lambda handler before fixing the schema, I could not check if the configuration was correct. Well every piece was wrong. &lt;/p&gt;

&lt;p&gt;The real contract, per the developer guide's Lambda input-format page [7]: &lt;code&gt;event&lt;/code&gt; is the flat map of the tool's &lt;code&gt;inputSchema&lt;/code&gt; properties, directly, with no wrapper key of any kind. A &lt;code&gt;get_opportunity&lt;/code&gt; call arrives as &lt;code&gt;{"opportunity_id": "..."}&lt;/code&gt; and nothing else. The tool name is not in &lt;code&gt;event&lt;/code&gt; at all. It travels in the Lambda context object, at &lt;code&gt;context.client_context.custom['bedrockAgentCoreToolName']&lt;/code&gt;, formatted as &lt;code&gt;&amp;lt;target&amp;gt;___&amp;lt;tool&amp;gt;&lt;/code&gt; and the developer guide warns that the prefix "will need to be manually stripped" [7]. The corrected dispatch, which is the entire integration surface between the Gateway and this Lambda:&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;_TOOL_NAME_DELIMITER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;___&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_tool_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw_tool_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrockAgentCoreToolName&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="n"&gt;_TOOL_NAME_DELIMITER&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw_tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw_tool_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_TOOL_NAME_DELIMITER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw_tool_name&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_json_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_json_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;_json_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&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;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_tool_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_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;list_matching_opportunities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list_matching_opportunities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&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;spoken_name&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="n"&gt;tool_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;get_opportunity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_opportunity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&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;opportunity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&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;ValueError&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;Unknown tool name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_json_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_json_safe&lt;/code&gt; in that snippet is this section's live-data lesson. &lt;code&gt;list_matching_opportunities&lt;/code&gt; worked end to end immediately. &lt;code&gt;get_opportunity&lt;/code&gt; did not: &lt;code&gt;Runtime.MarshalError: Unable to marshal response: Object of type datetime is not JSON serializable&lt;/code&gt;. boto3 deserializes Partner Central's timestamp fields, &lt;code&gt;LifeCycle.TargetCloseDate&lt;/code&gt; and &lt;code&gt;LastModifiedDate&lt;/code&gt; among them, into native Python &lt;code&gt;datetime&lt;/code&gt; objects, and the Lambda runtime cannot JSON-marshal those in a return value. The unit-test fixtures had used plain date strings, so the suite was green the whole time. The fix is the recursive converter above, applied to the entire return value so it catches a timestamp wherever one sits, plus a regression test that injects real &lt;code&gt;date&lt;/code&gt; and &lt;code&gt;datetime&lt;/code&gt; objects into the mocked response and asserts &lt;code&gt;json.dumps&lt;/code&gt; round-trips. Fixtures built from what you assume an API returns will not catch this bug class. One live call against real data did.&lt;/p&gt;

&lt;p&gt;One porting note: &lt;code&gt;_json_safe&lt;/code&gt; was not copied into the reasoning Lambda. That Lambda parses raw HTTP JSON with &lt;code&gt;json.loads&lt;/code&gt;, so its values are already strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IAM the Gateway itself needed
&lt;/h2&gt;

&lt;p&gt;The previous article told this story for the harness's execution role: every new capability surfaced its IAM needs as a live failure. The Gateway repeats the pattern with its own execution role and in this project it did so the moment the Cedar policy engine was associated with it.&lt;/p&gt;

&lt;p&gt;The association itself is one console action or the &lt;code&gt;policy_engine_configuration&lt;/code&gt; block shown in the first section. What it triggered was a chain of five service-side AccessDenied failures on &lt;code&gt;UpdateGateway&lt;/code&gt;, one per apply, each naming a permission the Gateway's execution role lacked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;bedrock-agentcore:GetPolicyEngine&lt;/code&gt; on the policy engine's ARN. The error reads "Access denied while calling GetPolicyEngine on Policy Engine ... with Gateway role": the Gateway role must be able to read the engine it is being associated with.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bedrock-agentcore:AuthorizeAction&lt;/code&gt; on the policy engine's ARN. Reading the engine's metadata is not enough; the role must also be allowed to invoke its authorization decision at call time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bedrock-agentcore:AuthorizeAction&lt;/code&gt; again, this time scoped to the Gateway's own ARN. Cedar's evaluation runs against the Gateway resource, so the same action needs a second grant on a second ARN type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bedrock-agentcore:PartiallyAuthorizeActions&lt;/code&gt; on the policy engine's ARN. &lt;code&gt;UpdateGateway&lt;/code&gt;'s own validation step, visible in the failure as an assumed role carrying the suffix &lt;code&gt;GenesisPolicyEngineCheck&lt;/code&gt;, calls this as a distinct action that &lt;code&gt;AuthorizeAction&lt;/code&gt; does not imply.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bedrock-agentcore:PartiallyAuthorizeActions&lt;/code&gt; on the Gateway's own ARN, completing the same two-ARN pattern as step 3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three actions, two ARN types, five grants demanded live. &lt;code&gt;GetPolicyEngine&lt;/code&gt; was the only action never demanded against the Gateway's own ARN, which is why the deployed policy carries five action-to-ARN grants, packed into two IAM statements. If you are planning to build the same, grant the full permissions up front anyway and skip the discovery loop; each of these cost one apply-fail-diagnose cycle and none of them is visible before the live &lt;code&gt;UpdateGateway&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Two implementation notes from the automated path. The gateway-scoped statements use an ARN pattern anchored to the statically known Gateway name with a wildcard suffix, &lt;code&gt;arn:aws:bedrock-agentcore:eu-central-1:&amp;lt;account&amp;gt;:gateway/partner-growth-agent-gateway-*&lt;/code&gt;, rather than the literal ID captured from one historical apply. The pattern is knowable at plan time and survives a teardown and recreate; a hardcoded suffix does not.&lt;/p&gt;

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

&lt;p&gt;Five things from this Gateway generalize beyond this demo to any AgentCore build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 64-character &lt;code&gt;allowed_tools&lt;/code&gt; cap is a naming rule.&lt;/strong&gt; Pick short Gateway target names before you know how many prefixes will stack on top of them. A name that fits comfortably on its own can blow the cap once qualified with the tool-block prefix and the tool-name suffix and renaming a target after a demo depends on it is far more disruptive than naming it short from the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare names match builtins only.&lt;/strong&gt; Any Gateway or MCP tool needs the &lt;code&gt;@&amp;lt;block&amp;gt;/&amp;lt;target&amp;gt;___&amp;lt;tool&amp;gt;&lt;/code&gt; glob form, documented on the developer guide's "Tools" page and absent from the Terraform provider docs. If your model narrates tool calls instead of making them, check this before anything else, and check it with token counts, not with prose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap an external MCP server in a Lambda target when its transport or auth does not match the Gateway.&lt;/strong&gt; HubSpot's OAuth-plus-PKCE mandate and Partner Central's &lt;code&gt;sendMessage&lt;/code&gt;/&lt;code&gt;getSession&lt;/code&gt; handshake are two different mismatches with the same fix: a Lambda that speaks the external system's real protocol on one side and exposes an ordinary &lt;code&gt;inline_payload&lt;/code&gt; schema on the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marshal datetimes before live data arrives.&lt;/strong&gt; Any Lambda tool wrapping a boto3 client needs a recursive datetime-to-ISO conversion on its whole return value, because test fixtures written as plain strings cannot surface the &lt;code&gt;Runtime.MarshalError&lt;/code&gt; that real responses produce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify tool exposure with a direct SigV4 &lt;code&gt;tools/list&lt;/code&gt; probe.&lt;/strong&gt; Bypassing the model and the harness to ask the Gateway what it exposes separates "the Gateway does not expose this tool" from "the model cannot see it" in one request. It is faster and more certain than debugging through a model's tool-selection behavior.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Three notes for a reader standing this subsystem up. All three come from failures described above.&lt;/p&gt;

&lt;p&gt;First, before associating a policy engine with a Gateway, put the policy-engine permissions on the Gateway's execution role up front: &lt;code&gt;GetPolicyEngine&lt;/code&gt;, &lt;code&gt;AuthorizeAction&lt;/code&gt;, and &lt;code&gt;PartiallyAuthorizeActions&lt;/code&gt;, scoped to the policy engine's ARN and to the Gateway's own ARN pattern. &lt;/p&gt;

&lt;p&gt;Second, keep every Gateway target name short before it gets qualified into an &lt;code&gt;allowed_tools&lt;/code&gt; glob. The budget is 64 characters for &lt;code&gt;@&amp;lt;block&amp;gt;/&amp;lt;target&amp;gt;___&amp;lt;tool&amp;gt;&lt;/code&gt; in total. Spend them on the tool name, which the model reads, not on the target name, which only routing sees.&lt;/p&gt;

&lt;p&gt;Third, before writing any code against an external MCP server, check its session handshake and its auth model against the Gateway's supported target types and the outbound-auth matrix. Both of this project's mismatches were visible in public documentation before any Lambda code existed. A mismatch found after the Lambda is built means rewriting the integration, not just the code around it.&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] HubSpot developer documentation for the HubSpot MCP server (&lt;code&gt;mcp.hubspot.com&lt;/code&gt;): Streamable HTTP transport and the OAuth Authorization Code + PKCE requirement. &lt;a href="https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server" rel="noopener noreferrer"&gt;https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-2"&gt;&lt;/a&gt;[2] 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;/p&gt;

&lt;p&gt;&lt;a id="source-3"&gt;&lt;/a&gt;[3] AWS Partner Central developer guide, MCP configuration reference: endpoint URL, SigV4 service name &lt;code&gt;partnercentral-agents-mcp&lt;/code&gt;, us-east-1. &lt;a href="https://docs.aws.amazon.com/partner-central/latest/developer-guide/mcp-configuration-reference.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/partner-central/latest/developer-guide/mcp-configuration-reference.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="source-4"&gt;&lt;/a&gt;[4] AWS Partner Central developer guide, MCP tools reference: the single &lt;code&gt;tools/call&lt;/code&gt; method carrying &lt;code&gt;sendMessage&lt;/code&gt;/&lt;code&gt;getSession&lt;/code&gt;, and the error-handling section documenting &lt;code&gt;-32004 LIMIT_EXCEEDED&lt;/code&gt;. &lt;a href="https://docs.aws.amazon.com/partner-central/latest/developer-guide/mcp-tools-reference.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/partner-central/latest/developer-guide/mcp-tools-reference.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: Gateway target types and the outbound-auth support matrix (Lambda targets support the Gateway service role only; API key and OAuth variants unsupported for that target type). &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-outbound-auth.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-outbound-auth.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, "Tools": the &lt;code&gt;allowedTools&lt;/code&gt; glob patterns (&lt;code&gt;@server&lt;/code&gt;, &lt;code&gt;@server/tool&lt;/code&gt;, &lt;code&gt;@server/glob&lt;/code&gt;, &lt;code&gt;@*/tool&lt;/code&gt;), bare names matching builtin tools only, and the &lt;code&gt;remote_mcp&lt;/code&gt; tool type's URL-plus-headers configuration. &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-7"&gt;&lt;/a&gt;[7] Amazon Bedrock AgentCore developer guide, Lambda function input format for Gateway targets: &lt;code&gt;event&lt;/code&gt; as the flat &lt;code&gt;inputSchema&lt;/code&gt; property map, and the tool name in &lt;code&gt;context.client_context.custom['bedrockAgentCoreToolName']&lt;/code&gt; with the target prefix to strip. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-add-target-lambda.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-add-target-lambda.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>mcp</category>
    </item>
    <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;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;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;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>
