1
0
mirror of synced 2026-08-05 11:07:15 +00:00

Harness GA Samples — AWS Skills, S3 Filesystem, Step Functions, Builder Agent (#1688)

* Add Harness GA samples: AWS Skills, S3 filesystem, Step Functions, builder agent

Four new samples under 01-features/01-harness for the Harness GA launch:

- 01-advanced-examples/08-aws-skills: enable native AWS Skills from the AWS
  Agent Toolkit via the awsSkills source (all / glob / specific / mixed modes)
- 01-advanced-examples/09-s3-filesystem: mount an S3 Files access point as the
  agent filesystem; includes s3_knowledge_base.py — a persistent LLM-wiki
  knowledge base (ingest / query / lint) on the S3 mount
- 01-advanced-examples/10-stepfunctions-orchestration: a Step Functions STANDARD
  state machine that drives the harness lifecycle (create -> poll READY ->
  invoke -> delete) via a Lambda task worker
- 02-use-cases/03-aws-builder-agent: harness + AWS Skills compose an AWS
  engineering agent that designs and scaffolds a serverless app

All samples use the GA boto3 SDK shapes (create_harness/invoke_harness) with no
internal endpoints or configuration. Updated the harness README index.

* Rename s3_knowledge_base.py to s3_llm_wiki.py to avoid Bedrock Knowledge Bases confusion

The LLM-wiki sample is a self-maintained markdown wiki on the agent's
filesystem, unrelated to the Amazon Bedrock Knowledge Bases feature. Rename the
file and scrub 'knowledge base' wording (mount default /mnt/wiki, pages/ subdir)
across the script and READMEs to prevent customer confusion.

* Fix S3 filesystem + Step Functions samples after end-to-end testing

Verified the samples against the live GA API and fixed real bugs found:

S3 filesystem (s3_filesystem.py, s3_llm_wiki.py):
- S3 Files mounts REQUIRE VPC network mode — add networkConfiguration
  (networkMode=VPC + subnets + security groups) to create_harness; add
  required --subnet-ids/--security-group-ids CLI args.
- Execution role needs s3files permissions (ListMountTargets/Get*/ClientMount/
  ClientWrite/ClientRootAccess), not plain s3:* — correct the attached policy.
- README: document VPC + mount-target prerequisites and the new args.

Step Functions (stepfunctions_orchestration.py):
- Lambda role also needs *AgentRuntime* actions (CreateHarness provisions an
  underlying AgentRuntime) — without them CreateHarness returns AccessDenied.
- CheckStatus Choice now matches CREATE_FAILED/UPDATE_FAILED so a failed
  creation fails fast instead of looping on the WaitForReady default forever.

* Rework Step Functions sample to native integration; fix S3 Files IAM/VPC guidance

Step Functions: replace the Lambda task-worker approach with the native
arn:aws:states:::bedrockagentcore:invokeHarness service integration — a single
Task state, no Lambda, no glue. The state machine's own role gets
bedrock-agentcore:InvokeHarness; output is Converse-shaped
(Output.Message.Content[].Text). Rewrote the README accordingly.

S3 filesystem: scope the execution-role IAM to what the runtime actually needs —
s3files:GetAccessPoint (unscoped, validated at harness create time) plus
s3files:ClientMount/ClientWrite (conditioned on the access point). Document that
S3 Files mounts require VPC network mode with private subnets that have egress
(route to a NAT gateway); public subnets do not connect. Same IAM fix applied to
the s3_llm_wiki.py sample.

* Remove Step Functions sample; renumber to avoid collision with merged samples

Upstream main now occupies advanced-examples 01-12 (incl. 06-async-step-function,
08-gemini-model-provider, 09-openai-model-provider). Renumber our two samples to
free slots and drop the Step Functions sample (upstream added its own):

- delete 10-stepfunctions-orchestration
- 08-aws-skills  -> 13-aws-skills
- 09-s3-filesystem -> 14-s3-filesystem

Updated all cross-references (harness index README, builder-agent note).
This commit is contained in:
Ed Fraga
2026-06-20 02:11:41 +10:00
committed by GitHub
parent 97e89df7b6
commit bb763db66c
8 changed files with 1758 additions and 3 deletions
@@ -0,0 +1,102 @@
# AWS Skills
| Information | Details |
|:--------------------|:-------------------------------------------------------------------------|
| Tutorial type | Advanced Example |
| Agent type | AWS engineering assistant |
| Agentic Framework | None (direct boto3) |
| LLM model | Anthropic Claude Haiku 4.5 |
| Tutorial components | AgentCore harness — `skills` parameter, native `awsSkills` source |
| Example complexity | Intermediate |
## Overview
Give a harness agent native **AWS Skills** — curated capability bundles from the
[AWS Agent Toolkit](https://github.com/aws/agent-toolkit-for-aws) that are baked
into the harness runtime image. You enable them declaratively in the `skills`
parameter; there is nothing to install on the VM.
This is the zero-install counterpart to [05-agent-skills](../05-agent-skills),
which hand-installs custom skills onto the VM with `npx`.
## What are AWS Skills?
Skills bundle instructions, reference docs, and code templates for a specific AWS
domain (serverless, CloudFormation, observability, cost management, ...). They are
especially valuable with smaller/cheaper models that don't carry deep AWS
knowledge — the skill supplies the patterns the model needs.
Each entry in the `skills` array is a union; the `awsSkills` member selects skills
from the toolkit:
```python
# Enable every AWS Skill
skills=[{"awsSkills": {}}]
# Enable a whole category by glob
skills=[{"awsSkills": {"paths": ["core-skills/*"]}}]
# Enable one specific skill
skills=[{"awsSkills": {"paths": [
"specialized-skills/operations-skills/troubleshooting-application-failures"]}}]
# Mix multiple AWS Skill selections (and other skill sources)
skills=[
{"awsSkills": {"paths": ["core-skills/aws-serverless"]}},
{"awsSkills": {"paths": ["core-skills/aws-cdk"]}},
]
```
`core-skills/*` includes domains like `amazon-bedrock`, `aws-cdk`,
`aws-serverless` (Lambda, API Gateway, Step Functions, SAM), `aws-observability`,
`aws-billing-and-cost-management`, and the language SDK usage skills. Skills can be
set on the harness resource (so they apply to every invocation) or passed per
`invoke_harness` call.
## Sample Prompts
**Prompt** (`--mode glob`, default): "What AWS skills do you have available? Give a short bulleted summary by category."
**Expected Behavior**: Agent lists the `core-skills/*` it loaded, grouped by category.
**Prompt** (`--mode mixed`): "Design a Step Functions state machine for order processing and outline the CDK stack."
**Expected Behavior**: Agent draws on the `aws-serverless` and `aws-cdk` skills to propose a design.
## Key Concepts
**Zero install**: Unlike custom skills, AWS Skills require no `npx`/VM step — set them on the harness and they're ready on first invocation.
**Selection modes**: `--mode all | glob | specific | mixed` map to the four `awsSkills` shapes above.
**Resource vs. per-call**: This sample sets skills at create time. You can also pass `skills=` on `invoke_harness` to apply them to a single call.
## Clean Up
```python
control.delete_harness(harnessId=harness_id)
from utils.iam import delete_harness_role
delete_harness_role()
```
The script deletes the harness automatically on exit (pass `--skip-cleanup` to keep it).
## Running the Python Scripts
```bash
pip install -r ../../requirements.txt
```
```bash
# Default — enable core-skills/* and summarize
python aws_skills.py
# Enable every AWS Skill
python aws_skills.py --mode all
# One named skill
python aws_skills.py --mode specific \
--skill-path specialized-skills/operations-skills/troubleshooting-application-failures
# Combine serverless + CDK skills with a build task
python aws_skills.py --mode mixed \
-m "Design a Step Functions state machine for order processing and outline the CDK stack."
```
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""
AWS Skills for Harness
This script shows how to give a Harness agent native **AWS Skills** — curated
capability bundles from the AWS Agent Toolkit (https://github.com/aws/agent-toolkit-for-aws)
that are baked into the Harness runtime image. Unlike custom skills that you
install onto the VM yourself, AWS Skills are enabled declaratively through the
`skills` parameter and are ready the moment the agent starts.
A skill bundles instructions, reference docs, and code templates for a specific
AWS domain (serverless, CloudFormation, observability, cost management, ...).
They are especially valuable with smaller/cheaper models that don't carry deep
AWS knowledge — the skill supplies the patterns the model needs to succeed.
Four ways to select AWS Skills (pick with --mode):
all Enable every AWS Skill in the toolkit
skills=[{"awsSkills": {}}]
glob Enable a whole category with a glob path (default)
skills=[{"awsSkills": {"paths": ["core-skills/*"]}}]
specific Enable one named skill
skills=[{"awsSkills": {"paths": [
"specialized-skills/operations-skills/troubleshooting-application-failures"]}}]
mixed Combine several AWS Skill selections (and you can add other skill
sources — path/S3 — in the same array)
skills=[
{"awsSkills": {"paths": ["core-skills/aws-cdk"]}},
{"awsSkills": {"paths": ["core-skills/aws-serverless"]}},
]
Skills can be set on the Harness resource (CreateHarness/UpdateHarness, so they
apply to every invocation) or passed per call on InvokeHarness. This sample sets
them on the resource at create time, then invokes.
Usage:
# Default — enable all of core-skills/* and ask the agent what it can do
python aws_skills.py
# Enable every AWS Skill
python aws_skills.py --mode all
# Enable a single named skill
python aws_skills.py --mode specific \\
--skill-path specialized-skills/operations-skills/troubleshooting-application-failures
# Combine serverless + CDK skills and ask the agent to design a workflow
python aws_skills.py --mode mixed \\
-m "Design a Step Functions state machine for order processing and outline the CDK stack."
# Keep the harness after the demo
python aws_skills.py --skip-cleanup
# See all options
python aws_skills.py --help
"""
import argparse
import json
import os
import sys
import time
import uuid
from pathlib import Path
import boto3
import botocore.exceptions
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from utils.iam import create_harness_role
from utils.client import get_agentcore_client, get_agentcore_control_client
REGION = os.getenv("AWS_DEFAULT_REGION")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEFAULT_MODEL = "global.anthropic.claude-haiku-4-5-20251001-v1:0"
DEFAULT_GLOB = "core-skills/*"
DEFAULT_SPECIFIC = "specialized-skills/operations-skills/troubleshooting-application-failures"
DEFAULT_PROMPT = "What AWS skills do you have available? Give a short bulleted summary by category."
HARNESS_POLL_INTERVAL = 5
HARNESS_POLL_TIMEOUT = 120
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
parser = argparse.ArgumentParser(
description="Enable native AWS Skills on a Harness and invoke the agent.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--mode",
choices=["all", "glob", "specific", "mixed"],
default="glob",
help="How to select AWS Skills (default: glob — enables core-skills/*)",
)
parser.add_argument(
"--skill-path",
default=DEFAULT_SPECIFIC,
metavar="PATH",
help=f"Skill path used by --mode specific (default: {DEFAULT_SPECIFIC})",
)
parser.add_argument(
"--glob-path",
default=DEFAULT_GLOB,
metavar="PATH",
help=f"Glob path used by --mode glob (default: {DEFAULT_GLOB})",
)
parser.add_argument(
"--model",
default=DEFAULT_MODEL,
metavar="MODEL_ID",
help=f"Bedrock model ID (default: {DEFAULT_MODEL})",
)
parser.add_argument(
"--message",
"-m",
default=DEFAULT_PROMPT,
help="Prompt to send to the agent",
)
parser.add_argument(
"--role-arn",
default=None,
metavar="ARN",
help="Use an existing IAM execution role ARN instead of creating one",
)
parser.add_argument(
"--skip-cleanup",
action="store_true",
help="Keep the harness after the demo",
)
parser.add_argument(
"--raw-events",
action="store_true",
help="Print raw JSON streaming events from invoke",
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def build_skills(args):
"""Translate the chosen --mode into a `skills` parameter value.
Each entry is a union; the `awsSkills` member selects skills from the AWS
Agent Toolkit. An empty `{}` means "all skills"; `paths` narrows it with
glob patterns or exact skill paths.
"""
if args.mode == "all":
return [{"awsSkills": {}}]
if args.mode == "glob":
return [{"awsSkills": {"paths": [args.glob_path]}}]
if args.mode == "specific":
return [{"awsSkills": {"paths": [args.skill_path]}}]
# mixed — combine two category selections; add {"path": ...} or S3 sources here too
return [
{"awsSkills": {"paths": ["core-skills/aws-serverless"]}},
{"awsSkills": {"paths": ["core-skills/aws-cdk"]}},
]
def poll_harness_status(control, harness_id, target_status="READY", timeout=HARNESS_POLL_TIMEOUT):
"""Poll until a Harness reaches the target status or times out."""
deadline = time.monotonic() + timeout
while True:
resp = control.get_harness(harnessId=harness_id)
status = resp["harness"]["status"]
print(f" Harness status: {status}")
if status == target_status:
return resp
if status in ("FAILED", "DELETE_FAILED"):
raise RuntimeError(f"Harness entered {status}")
if time.monotonic() > deadline:
raise TimeoutError(f"Harness not {target_status} after {timeout}s (current: {status})")
time.sleep(HARNESS_POLL_INTERVAL)
def stream_response(client, harness_arn, session_id, message, model_id, raw=False):
"""Invoke a Harness and stream the response to stdout."""
response = client.invoke_harness(
harnessArn=harness_arn,
runtimeSessionId=session_id,
messages=[{"role": "user", "content": [{"text": message}]}],
model={"bedrockModelConfig": {"modelId": model_id}},
)
full_text = ""
try:
for event in response["stream"]:
if raw:
print(json.dumps(event, default=str))
continue
if "contentBlockStart" in event:
start = event["contentBlockStart"].get("start", {})
if "toolUse" in start:
print(f"\n [Tool: {start['toolUse'].get('name', '?')}]", flush=True)
elif "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
if "text" in delta:
print(delta["text"], end="", flush=True)
full_text += delta["text"]
elif "messageStop" in event:
print()
elif "internalServerException" in event:
print(f"\n Error: {event['internalServerException']}")
except botocore.exceptions.EventStreamError:
# The stream may send an empty error event on close; safe to ignore
# if we already received content.
if not full_text:
raise
return full_text
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main(args=None):
if args is None:
args = parser.parse_args()
control = get_agentcore_control_client()
client = get_agentcore_client()
skills = build_skills(args)
harness_id = None
try:
# ── Step 0: IAM role ──────────────────────────────────────────
print("=" * 60)
print("Step 0: IAM execution role")
print("=" * 60)
if args.role_arn:
role_arn = args.role_arn
print(f" Using provided role: {role_arn}")
else:
role_arn = create_harness_role()
print(" Waiting for IAM propagation...")
time.sleep(10)
# ── Step 1: Create Harness with AWS Skills ────────────────────
print("\n" + "=" * 60)
print(f"Step 1: Create Harness with AWS Skills (mode: {args.mode})")
print("=" * 60)
print(f" skills = {json.dumps(skills)}")
harness_name = f"AwsSkills_{uuid.uuid4().hex[:8]}"
resp = control.create_harness(
harnessName=harness_name,
executionRoleArn=role_arn,
skills=skills,
# Smaller models benefit most from skills; allow the agent to use
# its default tools (fs/shell) so it can act on what the skill teaches.
systemPrompt=[{"text": "You are a helpful AWS engineering assistant."}],
)
harness_id = resp["harness"]["harnessId"]
harness_arn = resp["harness"]["arn"]
print(f" Harness ID: {harness_id}")
print(f" Harness ARN: {harness_arn}")
poll_harness_status(control, harness_id)
# ── Step 2: Invoke and observe the loaded skills ──────────────
print("\n" + "=" * 60)
print("Step 2: Invoke agent")
print("=" * 60)
session_id = str(uuid.uuid4()).upper()
print(f" Session ID: {session_id}")
print(f" Model: {args.model}")
print(f" Message: {args.message[:80]}{'...' if len(args.message) > 80 else ''}\n")
stream_response(client, harness_arn, session_id, args.message, args.model, raw=args.raw_events)
print("\n" + "=" * 60)
print("Done!")
print("=" * 60)
finally:
if not args.skip_cleanup and harness_id:
print("\nCleaning up...")
try:
control.delete_harness(harnessId=harness_id)
print(f" Deleted harness: {harness_id}")
except Exception as e:
print(f" Warning: failed to delete harness: {e}")
if __name__ == "__main__":
main()
@@ -0,0 +1,174 @@
# S3 Filesystem Mount
| Information | Details |
|:--------------------|:-------------------------------------------------------------------------|
| Tutorial type | Advanced Example |
| Agent type | Assistant with persistent storage |
| Agentic Framework | None (direct boto3) |
| LLM model | Anthropic Claude Haiku 4.5 |
| Tutorial components | AgentCore harness — `filesystemConfigurations`, S3 Files access point |
| Example complexity | Intermediate |
## Overview
A harness session runs in an isolated microVM with an **ephemeral** disk — when
the session ends, anything written to the VM is gone. Mount an **S3 Files access
point** into the VM and the agent gets a normal POSIX path (e.g. `/mnt/data`)
backed by S3, so artifacts persist past the session and are shared across
sessions.
## What's in this folder
| File | What it shows |
|---|---|
| [`s3_filesystem.py`](s3_filesystem.py) | **The mechanism.** Session A writes a file under the mount; Session B (a brand-new microVM) reads it back — only possible because the file lives in S3, not on the VM disk. |
| [`s3_llm_wiki.py`](s3_llm_wiki.py) | **The use case: a persistent LLM wiki.** The agent builds and maintains a compounding markdown wiki on the S3 mount across sessions (ingest → query → lint). |
The first script proves the persistence boundary; the second shows *why you'd
want it*.
## Configuration
An S3 Files mount requires the harness to run in **VPC network mode** — the
microVM reaches the access point's mount target over your VPC. So the
environment carries both a `networkConfiguration` and the `filesystemConfigurations`:
```python
environment={
"agentCoreRuntimeEnvironment": {
"networkConfiguration": {
"networkMode": "VPC",
"networkModeConfig": {
"subnets": ["subnet-0abc1234"],
"securityGroups": ["sg-0def5678"],
},
},
"filesystemConfigurations": [
{
"s3FilesAccessPoint": {
"accessPointArn": "arn:aws:s3files:us-west-2:111122223333:file-system/fs-abc/access-point/fsap-def",
"mountPath": "/mnt/data",
}
}
],
}
}
```
`mountPath` must look like `/mnt/<name>`. The execution role must be allowed to
mount the access point — when this script creates the role, it attaches the
required `s3files` permissions for you: `s3files:GetAccessPoint` (the runtime
validates this at create time, so it stays unscoped) plus `s3files:ClientMount`
and `s3files:ClientWrite` (scoped to the file system with an `AccessPointArn`
condition, used when the microVM mounts the access point).
## Prerequisites
- An **S3 Files access point** backed by a bucket, with a **mount target** in the
subnet you pass. Its ARN looks like:
`arn:aws:s3files:<region>:<account>:file-system/fs-xxxx/access-point/fsap-xxxx`
- The **subnet(s) and security group(s)** that reach the mount target. The Harness
must be in the **same VPC** as the mount target, the subnet(s) you pass must be
in an **Availability Zone that has a mount target**, and the security group(s)
must allow **NFS (port 2049)** between the Harness and the mount target (a
self-referencing security group is the simplest setup).
- **Use private subnets with egress** (a route to a NAT gateway). VPC-mode
Harnesses run in private networking; public subnets do not give the microVM the
connectivity it needs and the invoke will fail. See
[Configure AgentCore for VPC](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html).
- If you bring your own execution role (`--role-arn`), it must already have the
`s3files` mount permissions above.
## Sample Prompts
**Prompt (Session A)**: "Write a short markdown travel note about Amsterdam to /mnt/data/harness-note.md."
**Expected Behavior**: Agent writes the file under the mounted path and confirms the absolute path.
**Prompt (Session B, fresh VM)**: "Read the file /mnt/data/harness-note.md and show me its contents verbatim."
**Expected Behavior**: Agent reads back the note written in Session A — the S3-backed mount persisted it.
## Key Concepts
**Persistence boundary**: A different `session_id` means a different VM disk. Surviving that boundary is what proves the mount is S3-backed.
**Mount path format**: `mountPath` must match `/mnt/<name>` (validated by the script before the call).
**IAM scope**: The execution role only needs access to the single access point — the script attaches a narrowly scoped policy.
## Use case: a persistent LLM wiki
[`s3_llm_wiki.py`](s3_llm_wiki.py) turns the S3 mount into a
**persistent, compounding LLM wiki**, following the pattern Andrej Karpathy
describes in [this gist](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f):
rather than re-deriving answers from raw documents on every query (classic RAG),
the agent **builds and maintains a markdown wiki once and keeps it current**, so
knowledge becomes a compounding artifact.
> This is a self-maintained markdown wiki on the agent's filesystem — it is
> unrelated to the **Amazon Bedrock Knowledge Bases** feature.
The S3 mount is what makes this possible — the wiki must outlive any single
session and be shared across invocations. Three layers live under the mount:
```
/mnt/wiki/
sources/ raw, immutable inputs (the agent reads, never edits)
pages/ LLM-owned markdown: summaries, entity pages, concept pages ([[cross-linked]])
AGENTS.md the schema (how the wiki is organized)
index.md catalog of pages
log.md append-only history
```
Three operations, **each run in its own session** to prove the wiki persists
across the microVM boundary:
- **ingest** — read a raw source and integrate it across the wiki (create/update pages)
- **query** — answer from the wiki, then file the answer back as a new page so explorations compound
- **lint** — health-check: contradictions, stale claims, orphan pages, broken links
Re-run with `--op query` later and the wiki is still there in S3 — the agent
picks up exactly where it left off.
## Clean Up
```python
control.delete_harness(harnessId=harness_id)
from utils.iam import delete_harness_role
delete_harness_role()
```
The script deletes the harness on exit (pass `--skip-cleanup` to keep it). It
**leaves your S3 bucket and access point intact**.
## Running the Python Scripts
```bash
pip install -r ../../requirements.txt
```
```bash
# 1) The mechanism — prove persistence across sessions
python s3_filesystem.py \
--access-point-arn arn:aws:s3files:us-west-2:111122223333:file-system/fs-abc/access-point/fsap-def \
--subnet-ids subnet-0abc1234 \
--security-group-ids sg-0def5678
# Custom mount path + filename
python s3_filesystem.py \
--access-point-arn arn:aws:s3files:... \
--subnet-ids subnet-0abc1234 --security-group-ids sg-0def5678 \
--mount-path /mnt/shared \
--filename trip-notes.md
```
```bash
# 2) The LLM wiki — full demo (bootstrap, ingest, query, lint)
python s3_llm_wiki.py \
--access-point-arn arn:aws:s3files:us-west-2:111122223333:file-system/fs-abc/access-point/fsap-def \
--subnet-ids subnet-0abc1234 --security-group-ids sg-0def5678
# Query the existing wiki (it compounds — answers get filed back)
python s3_llm_wiki.py --access-point-arn arn:aws:s3files:... \
--subnet-ids subnet-0abc1234 --security-group-ids sg-0def5678 \
--op query -m "How does the LLM wiki pattern differ from RAG?"
```
@@ -0,0 +1,398 @@
#!/usr/bin/env python3
"""
Mount S3 as the Harness Filesystem
Every Harness session runs in an isolated microVM with its own ephemeral disk —
when the session ends, that disk is gone. To keep artifacts around, you can mount
an **S3 Files access point** into the VM. The agent then reads and writes a normal
POSIX path (e.g. /mnt/data) that is backed by S3, so files survive session
termination and are shared across sessions.
An S3 Files mount requires the Harness to run in **VPC network mode** — the
microVM reaches the access point's NFS mount target over your VPC. So the
environment carries both a `networkConfiguration` (VPC + subnets + security
groups) and the `filesystemConfigurations`:
environment={
"agentCoreRuntimeEnvironment": {
"networkConfiguration": {
"networkMode": "VPC",
"networkModeConfig": {
"subnets": ["subnet-..."],
"securityGroups": ["sg-..."],
},
},
"filesystemConfigurations": [
{
"s3FilesAccessPoint": {
"accessPointArn": "<S3 Files access point ARN>",
"mountPath": "/mnt/data",
}
}
],
}
}
This sample demonstrates persistence across the session boundary:
1. Create a Harness with the S3 mount (in your VPC)
2. Session A — ask the agent to WRITE a file under the mount path
3. Session B (fresh microVM) — ask the agent to READ that same file back
The file is still there because it lives in S3, not on the VM disk.
Prerequisites
-------------
* An S3 Files access point backed by a bucket, with a mount target in the subnet
you pass below. Its ARN looks like:
arn:aws:s3files:<region>:<account>:file-system/fs-xxxx/access-point/fsap-xxxx
Provide it with --access-point-arn.
* The subnet(s) and security group(s) that reach the mount target. The Harness
must be in the same VPC as the mount target, the subnet(s) you pass must be in an
Availability Zone that has a mount target, and the security group(s) must allow
NFS (port 2049). Use private subnets with egress (a route to a NAT gateway) —
VPC-mode Harnesses run in private networking and public subnets won't connect.
* The Harness execution role must be allowed to mount the access point. If this
script creates the role (the default), it attaches the required `s3files`
permissions for you. If you pass --role-arn, make sure it already has them.
Usage:
# Mount an existing S3 Files access point at /mnt/data and run the demo
python s3_filesystem.py \\
--access-point-arn arn:aws:s3files:us-west-2:111122223333:file-system/fs-abc/access-point/fsap-def \\
--subnet-ids subnet-0abc1234 \\
--security-group-ids sg-0def5678
# Choose a different mount path and filename
python s3_filesystem.py \\
--access-point-arn arn:aws:s3files:... \\
--subnet-ids subnet-0abc1234 --security-group-ids sg-0def5678 \\
--mount-path /mnt/shared \\
--filename trip-notes.md
# Keep the harness after the demo
python s3_filesystem.py --access-point-arn ... --subnet-ids ... --security-group-ids ... --skip-cleanup
# See all options
python s3_filesystem.py --help
"""
import argparse
import json
import os
import re
import sys
import time
import uuid
from pathlib import Path
import boto3
import botocore.exceptions
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from utils.iam import create_harness_role, ROLE_NAME
from utils.client import get_agentcore_client, get_agentcore_control_client
REGION = os.getenv("AWS_DEFAULT_REGION")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEFAULT_MODEL = "global.anthropic.claude-haiku-4-5-20251001-v1:0"
DEFAULT_MOUNT_PATH = "/mnt/data"
DEFAULT_FILENAME = "harness-note.md"
S3_FILES_POLICY_NAME = "HarnessS3FilesAccess"
# mountPath must match /mnt/<name> (see the service model: MountPath)
MOUNT_PATH_PATTERN = re.compile(r"^/mnt/[a-zA-Z0-9._-]+/?$")
HARNESS_POLL_INTERVAL = 5
HARNESS_POLL_TIMEOUT = 180
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
parser = argparse.ArgumentParser(
description="Mount an S3 Files access point into a Harness and prove artifacts persist across sessions.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--access-point-arn",
required=True,
metavar="ARN",
help="S3 Files access point ARN to mount (arn:aws:s3files:...:access-point/fsap-...)",
)
parser.add_argument(
"--subnet-ids",
required=True,
nargs="+",
metavar="SUBNET",
help="VPC subnet(s) that can reach the access point's mount target (NFS/2049)",
)
parser.add_argument(
"--security-group-ids",
required=True,
nargs="+",
metavar="SG",
help="Security group(s) allowing NFS (2049) to the mount target",
)
parser.add_argument(
"--mount-path",
default=DEFAULT_MOUNT_PATH,
metavar="PATH",
help=f"Where to mount it inside the VM (default: {DEFAULT_MOUNT_PATH})",
)
parser.add_argument(
"--filename",
default=DEFAULT_FILENAME,
metavar="NAME",
help=f"File the agent writes/reads under the mount (default: {DEFAULT_FILENAME})",
)
parser.add_argument(
"--model",
default=DEFAULT_MODEL,
metavar="MODEL_ID",
help=f"Bedrock model ID (default: {DEFAULT_MODEL})",
)
parser.add_argument(
"--role-arn",
default=None,
metavar="ARN",
help="Use an existing IAM execution role (must already allow the access point)",
)
parser.add_argument(
"--skip-cleanup",
action="store_true",
help="Keep the harness after the demo",
)
parser.add_argument(
"--raw-events",
action="store_true",
help="Print raw JSON streaming events from invoke",
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def attach_s3_files_policy(role_name, access_point_arn):
"""Allow the execution role to validate and mount the S3 Files access point.
* `s3files:GetAccessPoint` on `*` — the runtime validates this at harness
create time. Keep it unscoped; a scoped/conditioned form is rejected at
create with "Ensure the role has s3files:GetAccessPoint".
* `s3files:ClientMount`/`ClientWrite` — used when the microVM mounts the
access point; scoped to the file system with an AccessPointArn condition.
"""
fs_arn = access_point_arn.split("/access-point/")[0]
iam = boto3.client("iam")
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3FilesValidate",
"Effect": "Allow",
"Action": ["s3files:GetAccessPoint"],
"Resource": "*",
},
{
"Sid": "S3FilesClientMount",
"Effect": "Allow",
"Action": ["s3files:ClientMount", "s3files:ClientWrite"],
"Resource": fs_arn,
"Condition": {"ArnEquals": {"s3files:AccessPointArn": access_point_arn}},
},
],
}
iam.put_role_policy(
RoleName=role_name,
PolicyName=S3_FILES_POLICY_NAME,
PolicyDocument=json.dumps(policy),
)
print(f" Attached S3 Files access policy: {S3_FILES_POLICY_NAME}")
def poll_harness_status(control, harness_id, target_status="READY", timeout=HARNESS_POLL_TIMEOUT):
"""Poll until a Harness reaches the target status or times out."""
deadline = time.monotonic() + timeout
while True:
resp = control.get_harness(harnessId=harness_id)
status = resp["harness"]["status"]
print(f" Harness status: {status}")
if status == target_status:
return resp
if status in ("FAILED", "DELETE_FAILED"):
reason = resp["harness"].get("failureReason", "")
raise RuntimeError(f"Harness entered {status}. {reason}")
if time.monotonic() > deadline:
raise TimeoutError(f"Harness not {target_status} after {timeout}s (current: {status})")
time.sleep(HARNESS_POLL_INTERVAL)
def stream_response(client, harness_arn, session_id, message, model_id, raw=False):
"""Invoke a Harness and stream the response to stdout."""
response = client.invoke_harness(
harnessArn=harness_arn,
runtimeSessionId=session_id,
messages=[{"role": "user", "content": [{"text": message}]}],
model={"bedrockModelConfig": {"modelId": model_id}},
)
full_text = ""
try:
for event in response["stream"]:
if raw:
print(json.dumps(event, default=str))
continue
if "contentBlockStart" in event:
start = event["contentBlockStart"].get("start", {})
if "toolUse" in start:
print(f"\n [Tool: {start['toolUse'].get('name', '?')}]", flush=True)
elif "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
if "text" in delta:
print(delta["text"], end="", flush=True)
full_text += delta["text"]
elif "messageStop" in event:
print()
elif "internalServerException" in event:
print(f"\n Error: {event['internalServerException']}")
except botocore.exceptions.EventStreamError:
if not full_text:
raise
return full_text
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main(args=None):
if args is None:
args = parser.parse_args()
if not MOUNT_PATH_PATTERN.match(args.mount_path):
parser.error(f"--mount-path must look like /mnt/<name> (got: {args.mount_path})")
control = get_agentcore_control_client()
client = get_agentcore_client()
mount = args.mount_path.rstrip("/")
remote_file = f"{mount}/{args.filename}"
harness_id = None
try:
# ── Step 0: IAM role (with S3 access) ─────────────────────────
print("=" * 60)
print("Step 0: IAM execution role")
print("=" * 60)
if args.role_arn:
role_arn = args.role_arn
print(f" Using provided role: {role_arn}")
print(" (ensure it can access the S3 Files access point)")
else:
role_arn = create_harness_role()
attach_s3_files_policy(ROLE_NAME, args.access_point_arn)
print(" Waiting for IAM propagation...")
time.sleep(10)
# ── Step 1: Create Harness with S3 mount (VPC network mode) ───
print("\n" + "=" * 60)
print("Step 1: Create Harness with S3 mounted at " + mount)
print("=" * 60)
# S3 Files mounts require VPC network mode so the microVM can reach the
# access point's mount target over your VPC.
network = {
"networkMode": "VPC",
"networkModeConfig": {
"subnets": args.subnet_ids,
"securityGroups": args.security_group_ids,
},
}
filesystem = [
{
"s3FilesAccessPoint": {
"accessPointArn": args.access_point_arn,
"mountPath": mount,
}
}
]
print(f" networkConfiguration = {json.dumps(network)}")
print(f" filesystemConfigurations = {json.dumps(filesystem)}")
harness_name = f"S3Mount_{uuid.uuid4().hex[:8]}"
resp = control.create_harness(
harnessName=harness_name,
executionRoleArn=role_arn,
environment={
"agentCoreRuntimeEnvironment": {
"networkConfiguration": network,
"filesystemConfigurations": filesystem,
}
},
systemPrompt=[
{"text": f"You are a helpful assistant. A persistent S3-backed directory is mounted at {mount}."}
],
)
harness_id = resp["harness"]["harnessId"]
harness_arn = resp["harness"]["arn"]
print(f" Harness ID: {harness_id}")
print(f" Harness ARN: {harness_arn}")
poll_harness_status(control, harness_id)
# ── Step 2: Session A — write a file to the mount ─────────────
print("\n" + "=" * 60)
print("Step 2: Session A — write a file to the S3 mount")
print("=" * 60)
session_a = str(uuid.uuid4()).upper()
print(f" Session A: {session_a}\n")
stream_response(
client,
harness_arn,
session_a,
f"Write a short markdown travel note about Amsterdam to {remote_file}. "
f"Confirm the absolute path you saved it to.",
args.model,
raw=args.raw_events,
)
# Give the S3-backed write a moment to flush before the next session.
time.sleep(5)
# ── Step 3: Session B — read it back from a fresh VM ──────────
print("\n" + "=" * 60)
print("Step 3: Session B (fresh microVM) — read the file back")
print("=" * 60)
session_b = str(uuid.uuid4()).upper()
print(f" Session B: {session_b}")
print(" Different session = different VM disk. If the agent can still")
print(" read the file, it's because the mount is backed by S3.\n")
stream_response(
client,
harness_arn,
session_b,
f"Read the file {remote_file} and show me its contents verbatim.",
args.model,
raw=args.raw_events,
)
print("\n" + "=" * 60)
print("Done! The file written in Session A was readable in Session B.")
print("=" * 60)
finally:
if not args.skip_cleanup and harness_id:
print("\nCleaning up...")
try:
control.delete_harness(harnessId=harness_id)
print(f" Deleted harness: {harness_id}")
except Exception as e:
print(f" Warning: failed to delete harness: {e}")
print(" Note: the S3 bucket and access point are left intact.")
if __name__ == "__main__":
main()
@@ -0,0 +1,380 @@
#!/usr/bin/env python3
"""
S3-Backed LLM Wiki (a persistent, compounding markdown wiki)
A use case for the S3 filesystem mount: an agent that maintains its own
persistent markdown wiki. It implements the pattern Andrej Karpathy describes in
https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f :
instead of re-deriving answers from raw documents on every query (classic RAG),
an LLM agent **incrementally builds and maintains a persistent markdown wiki** —
knowledge is compiled once and kept current, becoming a compounding artifact.
(Note: this is a self-maintained markdown wiki on the agent's filesystem — it is
unrelated to the Amazon Bedrock Knowledge Bases feature.)
Why the harness S3 mount is a natural fit: the wiki must outlive any single
session and be shared across invocations. Mounting an S3 Files access point at
`/mnt/wiki` gives the agent a normal POSIX directory that is backed by S3, so the
wiki it writes in one session is still there in the next — and the agent picks up
exactly where it left off.
The three layers from the gist, mapped onto the mount:
/mnt/wiki/
sources/ raw, immutable inputs (the agent reads, never edits)
pages/ LLM-owned markdown: summaries, entity pages, concept pages
AGENTS.md the schema — tells the agent how the wiki is organized
index.md catalog of wiki pages
log.md append-only chronological record
Three operations, each a separate session to PROVE persistence across the
microVM boundary:
ingest read a raw source, integrate it across the wiki (create/update pages)
query answer a question from the wiki, filing the answer back as a page
lint health-check: find contradictions, stale claims, orphan pages
An S3 Files mount requires the Harness to run in **VPC network mode** (pass the
subnet(s) and security group(s) that can reach the access point's mount target).
Usage:
# Full demo: bootstrap schema, ingest two sources, query, lint
python s3_llm_wiki.py \\
--access-point-arn arn:aws:s3files:us-west-2:111122223333:file-system/fs-abc/access-point/fsap-def \\
--subnet-ids subnet-0abc1234 --security-group-ids sg-0def5678
# Run a single operation against an existing wiki harness/mount
python s3_llm_wiki.py --access-point-arn arn:aws:s3files:... \\
--subnet-ids subnet-0abc1234 --security-group-ids sg-0def5678 \\
--op query -m "What do we know about retrieval-augmented generation?"
# Custom mount path
python s3_llm_wiki.py --access-point-arn ... --subnet-ids ... --security-group-ids ... --mount-path /mnt/notes
# Keep the harness after the demo
python s3_llm_wiki.py --access-point-arn ... --subnet-ids ... --security-group-ids ... --skip-cleanup
# See all options
python s3_llm_wiki.py --help
"""
import argparse
import json
import os
import re
import sys
import time
import uuid
from pathlib import Path
import boto3
import botocore.exceptions
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from utils.iam import create_harness_role, ROLE_NAME
from utils.client import get_agentcore_client, get_agentcore_control_client
REGION = os.getenv("AWS_DEFAULT_REGION")
# ── Constants ───────────────────────────────────────────────────────────────
DEFAULT_MODEL = "global.anthropic.claude-haiku-4-5-20251001-v1:0"
DEFAULT_MOUNT_PATH = "/mnt/wiki"
S3_FILES_POLICY_NAME = "HarnessS3FilesAccess"
MOUNT_PATH_PATTERN = re.compile(r"^/mnt/[a-zA-Z0-9._-]+/?$")
HARNESS_POLL_INTERVAL = 5
HARNESS_POLL_TIMEOUT = 180
# Two tiny "raw sources" the agent ingests. In a real wiki these are papers,
# tickets, docs — here they're short so the demo runs fast.
SOURCES = {
"rag-overview.md": (
"# Retrieval-Augmented Generation (RAG)\n\n"
"RAG retrieves relevant document chunks at query time and feeds them to an "
"LLM as context. Strengths: fresh data, source attribution. Weaknesses: "
"re-derives understanding on every query, sensitive to chunking and "
"retrieval quality."
),
"wiki-pattern.md": (
"# The LLM Wiki Pattern\n\n"
"Instead of re-retrieving raw chunks per query, an LLM maintains a persistent "
"markdown wiki: summaries, entity pages, and concept pages with cross-links. "
"Knowledge is compiled once and kept current. Contrasts with RAG by making "
"knowledge a compounding artifact rather than a per-query computation."
),
}
# ── CLI ─────────────────────────────────────────────────────────────────────
parser = argparse.ArgumentParser(
description="Build a persistent, S3-backed LLM wiki with the harness.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--access-point-arn", required=True, metavar="ARN",
help="S3 Files access point ARN to mount (arn:aws:s3files:...:access-point/fsap-...)")
parser.add_argument("--subnet-ids", required=True, nargs="+", metavar="SUBNET",
help="VPC subnet(s) that can reach the access point's mount target (NFS/2049)")
parser.add_argument("--security-group-ids", required=True, nargs="+", metavar="SG",
help="Security group(s) allowing NFS (2049) to the mount target")
parser.add_argument("--mount-path", default=DEFAULT_MOUNT_PATH, metavar="PATH",
help=f"Where to mount the wiki inside the VM (default: {DEFAULT_MOUNT_PATH})")
parser.add_argument("--op", choices=["all", "ingest", "query", "lint"], default="all",
help="Which operation to run (default: all — bootstrap, ingest, query, lint)")
parser.add_argument("--message", "-m", default="How does the LLM wiki pattern differ from RAG?",
help="Question for the query operation")
parser.add_argument("--model", default=DEFAULT_MODEL, metavar="MODEL_ID",
help=f"Bedrock model ID (default: {DEFAULT_MODEL})")
parser.add_argument("--role-arn", default=None, metavar="ARN",
help="Use an existing IAM execution role (must already allow the access point)")
parser.add_argument("--skip-cleanup", action="store_true", help="Keep the harness after the demo")
parser.add_argument("--raw-events", action="store_true", help="Print raw JSON streaming events")
# ── Helpers ─────────────────────────────────────────────────────────────────
def attach_s3_files_policy(role_name, access_point_arn):
"""Allow the execution role to validate and mount the S3 Files access point.
* `s3files:GetAccessPoint` on `*` — the runtime validates this at harness
create time. Keep it unscoped; a scoped/conditioned form is rejected at
create with "Ensure the role has s3files:GetAccessPoint".
* `s3files:ClientMount`/`ClientWrite` — used when the microVM mounts the
access point; scoped to the file system with an AccessPointArn condition.
"""
fs_arn = access_point_arn.split("/access-point/")[0]
iam = boto3.client("iam")
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3FilesValidate",
"Effect": "Allow",
"Action": ["s3files:GetAccessPoint"],
"Resource": "*",
},
{
"Sid": "S3FilesClientMount",
"Effect": "Allow",
"Action": ["s3files:ClientMount", "s3files:ClientWrite"],
"Resource": fs_arn,
"Condition": {"ArnEquals": {"s3files:AccessPointArn": access_point_arn}},
},
],
}
iam.put_role_policy(RoleName=role_name, PolicyName=S3_FILES_POLICY_NAME,
PolicyDocument=json.dumps(policy))
print(f" Attached S3 Files access policy: {S3_FILES_POLICY_NAME}")
def poll_harness_status(control, harness_id, target_status="READY", timeout=HARNESS_POLL_TIMEOUT):
"""Poll until a Harness reaches the target status or times out."""
deadline = time.monotonic() + timeout
while True:
resp = control.get_harness(harnessId=harness_id)
status = resp["harness"]["status"]
print(f" Harness status: {status}")
if status == target_status:
return resp
if status in ("FAILED", "DELETE_FAILED"):
reason = resp["harness"].get("failureReason", "")
raise RuntimeError(f"Harness entered {status}. {reason}")
if time.monotonic() > deadline:
raise TimeoutError(f"Harness not {target_status} after {timeout}s (current: {status})")
time.sleep(HARNESS_POLL_INTERVAL)
def stream_turn(client, harness_arn, message, model_id, mount, raw=False):
"""Run one wiki operation in its OWN session (proves cross-session persistence)."""
session_id = str(uuid.uuid4()).upper()
system = (
f"You maintain a persistent markdown wiki mounted at {mount}. "
f"Layers: {mount}/sources (raw, read-only), {mount}/pages (your markdown pages), "
f"{mount}/AGENTS.md (schema), {mount}/index.md (catalog), {mount}/log.md (append-only). "
"Use your filesystem and shell tools to read and write files directly. "
"Keep pages concise and cross-linked with [[wiki-links]]."
)
response = client.invoke_harness(
harnessArn=harness_arn,
runtimeSessionId=session_id,
messages=[{"role": "user", "content": [{"text": message}]}],
model={"bedrockModelConfig": {"modelId": model_id}},
systemPrompt=[{"text": system}],
timeoutSeconds=300,
)
full_text = ""
try:
for event in response["stream"]:
if raw:
print(json.dumps(event, default=str))
continue
if "contentBlockStart" in event:
start = event["contentBlockStart"].get("start", {})
if "toolUse" in start:
print(f"\n [Tool: {start['toolUse'].get('name', '?')}]", flush=True)
elif "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
if "text" in delta:
print(delta["text"], end="", flush=True)
full_text += delta["text"]
elif "messageStop" in event:
print()
elif "internalServerException" in event:
print(f"\n Error: {event['internalServerException']}")
except botocore.exceptions.EventStreamError:
if not full_text:
raise
return full_text
def seed_sources(client, harness_arn, mount):
"""Write the raw source docs and bootstrap the schema into the mount (one session)."""
session_id = str(uuid.uuid4()).upper()
def run(cmd):
resp = client.invoke_agent_runtime_command(
agentRuntimeArn=harness_arn, runtimeSessionId=session_id, body={"command": cmd}
)
for event in resp["stream"]:
if "chunk" in event and "contentDelta" in event["chunk"]:
d = event["chunk"]["contentDelta"]
if "stderr" in d:
print(d["stderr"], end="")
run(f"mkdir -p {mount}/sources {mount}/pages")
for name, body in SOURCES.items():
# base64 to avoid any shell-quoting issues with the markdown body
import base64
b64 = base64.b64encode(body.encode()).decode()
run(f"echo {b64} | base64 -d > {mount}/sources/{name}")
# Bootstrap schema/index/log only if not present (idempotent for re-runs)
run(f"test -f {mount}/AGENTS.md || printf '# Wiki Schema\\n\\nsources/ raw inputs. pages/ LLM pages. index.md catalog. log.md history.\\n' > {mount}/AGENTS.md")
run(f"test -f {mount}/index.md || printf '# Index\\n' > {mount}/index.md")
run(f"test -f {mount}/log.md || printf '# Log\\n' > {mount}/log.md")
print(f" Seeded {len(SOURCES)} source(s) and bootstrapped schema under {mount}")
# ── Main ────────────────────────────────────────────────────────────────────
def main(args=None):
if args is None:
args = parser.parse_args()
if not MOUNT_PATH_PATTERN.match(args.mount_path):
parser.error(f"--mount-path must look like /mnt/<name> (got: {args.mount_path})")
control = get_agentcore_control_client()
client = get_agentcore_client()
mount = args.mount_path.rstrip("/")
harness_id = None
try:
# ── Step 0: IAM role (with S3 access) ─────────────────────────
print("=" * 60)
print("Step 0: IAM execution role")
print("=" * 60)
if args.role_arn:
role_arn = args.role_arn
print(f" Using provided role: {role_arn}")
print(" (ensure it can access the S3 Files access point)")
else:
role_arn = create_harness_role()
attach_s3_files_policy(ROLE_NAME, args.access_point_arn)
print(" Waiting for IAM propagation...")
time.sleep(10)
# ── Step 1: Create harness with the wiki mounted (VPC mode) ───
print("\n" + "=" * 60)
print(f"Step 1: Create harness with the wiki mounted at {mount}")
print("=" * 60)
# S3 Files mounts require VPC network mode so the microVM can reach the
# access point's mount target over your VPC.
network = {
"networkMode": "VPC",
"networkModeConfig": {"subnets": args.subnet_ids, "securityGroups": args.security_group_ids},
}
filesystem = [{"s3FilesAccessPoint": {"accessPointArn": args.access_point_arn, "mountPath": mount}}]
harness_name = f"S3LlmWiki_{uuid.uuid4().hex[:8]}"
resp = control.create_harness(
harnessName=harness_name,
executionRoleArn=role_arn,
environment={
"agentCoreRuntimeEnvironment": {
"networkConfiguration": network,
"filesystemConfigurations": filesystem,
}
},
)
harness_id = resp["harness"]["harnessId"]
harness_arn = resp["harness"]["arn"]
print(f" Harness ID: {harness_id}")
print(f" Harness ARN: {harness_arn}")
poll_harness_status(control, harness_id)
# ── Step 2: Seed raw sources (separate session) ───────────────
if args.op in ("all", "ingest"):
print("\n" + "=" * 60)
print("Step 2: Seed raw sources into the mount")
print("=" * 60)
seed_sources(client, harness_arn, mount)
time.sleep(5)
# ── Step 3: INGEST — compile sources into the wiki ────────────
if args.op in ("all", "ingest"):
print("\n" + "=" * 60)
print("Step 3: INGEST — integrate sources into the wiki")
print("=" * 60 + "\n")
stream_turn(
client, harness_arn,
f"Ingest every file in {mount}/sources that isn't represented yet. For each, create or "
f"update concise pages under {mount}/pages (concept/entity pages), cross-link with "
f"[[links]], update {mount}/index.md, and append a line to {mount}/log.md. "
"Summarize what you ingested and which pages you touched.",
args.model, mount, raw=args.raw_events,
)
time.sleep(5)
# ── Step 4: QUERY — answer from the wiki, file the answer ─────
if args.op in ("all", "query"):
print("\n" + "=" * 60)
print("Step 4: QUERY — answer from the wiki (fresh session)")
print("=" * 60)
print(f" Question: {args.message}\n")
stream_turn(
client, harness_arn,
f"Using only the wiki under {mount}/pages, answer: \"{args.message}\". Cite the wiki "
f"pages you used. Then file your answer as a new page under {mount}/pages and link it "
f"from {mount}/index.md so the exploration compounds.",
args.model, mount, raw=args.raw_events,
)
time.sleep(5)
# ── Step 5: LINT — health-check the wiki ──────────────────────
if args.op in ("all", "lint"):
print("\n" + "=" * 60)
print("Step 5: LINT — check the wiki for issues (fresh session)")
print("=" * 60 + "\n")
stream_turn(
client, harness_arn,
f"Lint the wiki under {mount}: list any contradictions, stale claims, "
f"orphan pages (not linked from {mount}/index.md), or broken [[links]]. Report findings; "
"fix trivial issues directly.",
args.model, mount, raw=args.raw_events,
)
print("\n" + "=" * 60)
print("Done! The wiki persists in S3 — re-run with --op query to see it compound.")
print("=" * 60)
finally:
if not args.skip_cleanup and harness_id:
print("\nCleaning up...")
try:
control.delete_harness(harnessId=harness_id)
print(f" Deleted harness: {harness_id}")
except Exception as e:
print(f" Warning: failed to delete harness: {e}")
print(" Note: the S3 bucket, access point, AND the wiki it holds are left intact.")
if __name__ == "__main__":
main()
@@ -0,0 +1,97 @@
# AWS Builder Agent — Harness + AWS Skills
| Information | Details |
|:--------------------|:-------------------------------------------------------------------------|
| Tutorial type | Use Case |
| Agent type | AWS engineering / coding agent |
| Agentic Framework | None (direct boto3) |
| LLM model | Anthropic Claude Haiku 4.5 |
| Tutorial components | harness — **AWS Skills** (`awsSkills`), filesystem + shell tools, multi-turn |
| Example complexity | Intermediate |
## Overview
**This is the "how do you build an agent with the harness?" example — and the
answer is harness + AWS Skills.** The harness *is* the agent: you declare the
model, the tools, and the skills in one `create_harness` call, then invoke. No
orchestration code, no framework.
Here we build an **AWS engineering assistant** by loading the
[AWS Agent Toolkit](https://github.com/aws/agent-toolkit-for-aws) skills via the
`awsSkills` parameter. Those skills give the agent curated AWS expertise
(serverless, CDK, CloudFormation, observability), and the harness's built-in
filesystem + shell tools let it actually **scaffold a runnable project**, not
just describe one.
> **Why this matters:** AWS Skills are the fastest way to see the benefit of the
> harness. A small, cheap model + the right skills = an AWS-aware coding agent in
> ~3 API calls. Change the skill paths or the prompt and you have a different
> agent — that is the whole harness model.
## How AWS Skills power this agent
```python
control.create_harness(
harnessName=name,
executionRoleArn=role_arn,
# ── This one parameter is what makes it an AWS expert ──
skills=[{"awsSkills": {"paths": ["core-skills/aws-serverless", "core-skills/aws-cdk"]}}],
systemPrompt=[{"text": "You are a senior AWS solutions engineer..."}],
)
```
`awsSkills` selects bundles from the AWS Agent Toolkit. See
[13-aws-skills](../../01-advanced-examples/13-aws-skills) for every selection
mode (all / glob / specific / mixed).
## What it does, end to end
1. **Create** the agent — harness + `awsSkills` + a builder system prompt
2. **Design** (turn 1) — the agent designs a serverless URL shortener (API GW + Lambda + DynamoDB)
3. **Scaffold** (turn 2, same session) — it writes a real CDK project to the VM filesystem
4. **Inspect**`ExecuteCommand` lists the files the agent created
5. **Clean up**
## Sample Prompts
**Brief (default)**: "Design a minimal serverless URL shortener on AWS: API Gateway + Lambda + DynamoDB..."
**Expected Behavior**: The agent designs the architecture, then scaffolds a TypeScript CDK app with handler files, README, and package.json under `/tmp/url-shortener`.
**Brief (`-m`)**: "Design and scaffold a CDK app for an S3 + Lambda thumbnail pipeline."
**Expected Behavior**: Same design → scaffold flow for a different serverless use case, drawing on the loaded AWS Skills.
## Key Concepts
**The skill is the difference**: Without `awsSkills`, a small model gives generic answers. With it, the agent applies real AWS best practices and current patterns.
**Multi-turn, one VM**: Design and scaffold run in the same `session_id`, so files from the scaffold step persist and can be inspected.
**Agent acts, not just talks**: The harness's default filesystem + shell tools let the agent write runnable code, not placeholders.
## Clean Up
```python
control.delete_harness(harnessId=harness_id)
from utils.iam import delete_harness_role
delete_harness_role()
```
The script deletes the harness on exit (pass `--skip-cleanup` to keep it).
## Running the Python Scripts
```bash
pip install -r ../../requirements.txt
```
```bash
# Build the default serverless URL-shortener agent
python aws_builder_agent.py
# Give it your own brief
python aws_builder_agent.py \
-m "Design and scaffold a CDK app for an S3 + Lambda thumbnail pipeline."
# Narrow the AWS Skills the agent loads
python aws_builder_agent.py --skill-paths core-skills/aws-cdk core-skills/aws-serverless
```
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""
AWS Builder Agent — building agents with the harness + AWS Skills
This use case answers a simple question: *how do you use the harness to build a
real agent?* The answer is that the harness IS the agent — you declare the model,
the tools, and the skills in one `create_harness` call, then invoke it. No
orchestration code, no framework.
Here we build an **AWS engineering assistant**: a harness agent loaded with the
[AWS Agent Toolkit](https://github.com/aws/agent-toolkit-for-aws) skills
(`awsSkills`). The agent gains curated AWS expertise — serverless, CDK,
CloudFormation, observability — and uses its built-in filesystem + shell tools to
actually scaffold a project, not just describe one.
What it does, end to end:
1. Create a harness with AWS Skills + a builder system prompt
2. Turn 1 — ask the agent to DESIGN a small serverless app (architecture)
3. Turn 2 — same session: ask it to SCAFFOLD the project (write files to the VM)
4. Inspect the files the agent created (ExecuteCommand)
5. Clean up
The point: a capable, AWS-aware coding agent in ~3 API calls. Swap the skill
paths or the prompt and you have a different agent — that's the harness model.
Usage:
# Build the default serverless URL-shortener agent
python aws_builder_agent.py
# Give it your own brief
python aws_builder_agent.py \\
-m "Design and scaffold a CDK app for an S3 + Lambda thumbnail pipeline."
# Narrow the skills the agent loads
python aws_builder_agent.py --skill-paths core-skills/aws-cdk core-skills/aws-serverless
# Keep the harness after the demo
python aws_builder_agent.py --skip-cleanup
# See all options
python aws_builder_agent.py --help
"""
import argparse
import json
import os
import sys
import time
import uuid
from pathlib import Path
import boto3
import botocore.exceptions
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from utils.iam import create_harness_role
from utils.client import get_agentcore_client, get_agentcore_control_client
REGION = os.getenv("AWS_DEFAULT_REGION")
# ── Constants ───────────────────────────────────────────────────────────────
DEFAULT_MODEL = "global.anthropic.claude-haiku-4-5-20251001-v1:0"
DEFAULT_SKILL_PATHS = ["core-skills/aws-serverless", "core-skills/aws-cdk"]
PROJECT_DIR = "/tmp/url-shortener"
DESIGN_PROMPT = (
"Design a minimal serverless URL shortener on AWS: API Gateway + Lambda + "
"DynamoDB. Describe the architecture, the data model, and the two endpoints "
"(create short URL, resolve short URL). Keep it to a short, concrete design."
)
SCAFFOLD_PROMPT = (
f"Now scaffold that project under {PROJECT_DIR}. Create a CDK app (TypeScript) "
"with the stack definition, a lambda/ directory with the two handler files, a "
"README.md, and a package.json. Write real, runnable starter code — not "
"placeholders. When done, list the files you created."
)
HARNESS_POLL_INTERVAL = 5
HARNESS_POLL_TIMEOUT = 180
# ── CLI ─────────────────────────────────────────────────────────────────────
parser = argparse.ArgumentParser(
description="Build an AWS engineering agent with the harness + AWS Skills.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--message",
"-m",
default=None,
help="Override the design brief (the scaffold step follows automatically)",
)
parser.add_argument(
"--skill-paths",
nargs="+",
default=DEFAULT_SKILL_PATHS,
metavar="PATH",
help=f"AWS skill paths to load (default: {' '.join(DEFAULT_SKILL_PATHS)})",
)
parser.add_argument(
"--model",
default=DEFAULT_MODEL,
metavar="MODEL_ID",
help=f"Bedrock model ID (default: {DEFAULT_MODEL})",
)
parser.add_argument(
"--role-arn",
default=None,
metavar="ARN",
help="Use an existing IAM execution role ARN instead of creating one",
)
parser.add_argument(
"--skip-cleanup",
action="store_true",
help="Keep the harness after the demo",
)
parser.add_argument(
"--raw-events",
action="store_true",
help="Print raw JSON streaming events from invoke",
)
# ── Helpers ─────────────────────────────────────────────────────────────────
def poll_harness_status(control, harness_id, target_status="READY", timeout=HARNESS_POLL_TIMEOUT):
"""Poll until a Harness reaches the target status or times out."""
deadline = time.monotonic() + timeout
while True:
resp = control.get_harness(harnessId=harness_id)
status = resp["harness"]["status"]
print(f" Harness status: {status}")
if status == target_status:
return resp
if status in ("FAILED", "DELETE_FAILED"):
reason = resp["harness"].get("failureReason", "")
raise RuntimeError(f"Harness entered {status}. {reason}")
if time.monotonic() > deadline:
raise TimeoutError(f"Harness not {target_status} after {timeout}s (current: {status})")
time.sleep(HARNESS_POLL_INTERVAL)
def stream_turn(client, harness_arn, session_id, message, model_id, raw=False):
"""Invoke the harness for one conversational turn and stream the response."""
response = client.invoke_harness(
harnessArn=harness_arn,
runtimeSessionId=session_id,
messages=[{"role": "user", "content": [{"text": message}]}],
model={"bedrockModelConfig": {"modelId": model_id}},
timeoutSeconds=300,
)
full_text = ""
try:
for event in response["stream"]:
if raw:
print(json.dumps(event, default=str))
continue
if "contentBlockStart" in event:
start = event["contentBlockStart"].get("start", {})
if "toolUse" in start:
print(f"\n [Tool: {start['toolUse'].get('name', '?')}]", flush=True)
elif "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
if "text" in delta:
print(delta["text"], end="", flush=True)
full_text += delta["text"]
elif "messageStop" in event:
print()
elif "internalServerException" in event:
print(f"\n Error: {event['internalServerException']}")
except botocore.exceptions.EventStreamError:
if not full_text:
raise
return full_text
def run_command(client, harness_arn, session_id, command):
"""Run a shell command on the agent's VM and print stdout/stderr."""
print(f" $ {command}")
resp = client.invoke_agent_runtime_command(
agentRuntimeArn=harness_arn,
runtimeSessionId=session_id,
body={"command": command},
)
for event in resp["stream"]:
if "chunk" in event and "contentDelta" in event["chunk"]:
delta = event["chunk"]["contentDelta"]
if "stdout" in delta:
print(delta["stdout"], end="")
if "stderr" in delta:
print(delta["stderr"], end="")
print()
# ── Main ────────────────────────────────────────────────────────────────────
def main(args=None):
if args is None:
args = parser.parse_args()
control = get_agentcore_control_client()
client = get_agentcore_client()
design_prompt = args.message or DESIGN_PROMPT
skills = [{"awsSkills": {"paths": args.skill_paths}}]
harness_id = None
try:
# ── Step 0: IAM role ──────────────────────────────────────────
print("=" * 60)
print("Step 0: IAM execution role")
print("=" * 60)
if args.role_arn:
role_arn = args.role_arn
print(f" Using provided role: {role_arn}")
else:
role_arn = create_harness_role()
print(" Waiting for IAM propagation...")
time.sleep(10)
# ── Step 1: Create the agent (harness + AWS Skills) ───────────
print("\n" + "=" * 60)
print("Step 1: Create the AWS builder agent")
print("=" * 60)
print(f" AWS skills: {args.skill_paths}")
harness_name = f"AwsBuilder_{uuid.uuid4().hex[:8]}"
resp = control.create_harness(
harnessName=harness_name,
executionRoleArn=role_arn,
skills=skills,
systemPrompt=[
{
"text": (
"You are a senior AWS solutions engineer. Use your AWS skills to "
"design and build well-architected, runnable projects. Prefer "
"infrastructure-as-code and serverless best practices. When asked to "
"scaffold, write real files to the filesystem using your tools."
)
}
],
)
harness_id = resp["harness"]["harnessId"]
harness_arn = resp["harness"]["arn"]
print(f" Harness ID: {harness_id}")
print(f" Harness ARN: {harness_arn}")
poll_harness_status(control, harness_id)
session_id = str(uuid.uuid4()).upper()
print(f" Session ID: {session_id}")
# ── Step 2: Design ────────────────────────────────────────────
print("\n" + "=" * 60)
print("Step 2: Design the solution")
print("=" * 60)
print(f" Brief: {design_prompt[:80]}{'...' if len(design_prompt) > 80 else ''}\n")
stream_turn(client, harness_arn, session_id, design_prompt, args.model, raw=args.raw_events)
# ── Step 3: Scaffold (same session — VM state persists) ───────
print("\n" + "=" * 60)
print("Step 3: Scaffold the project on the agent's VM")
print("=" * 60 + "\n")
stream_turn(client, harness_arn, session_id, SCAFFOLD_PROMPT, args.model, raw=args.raw_events)
# ── Step 4: Inspect what the agent built ──────────────────────
print("\n" + "=" * 60)
print("Step 4: Inspect the generated project")
print("=" * 60)
run_command(client, harness_arn, session_id, f"find {PROJECT_DIR} -type f 2>/dev/null | head -40")
print("=" * 60)
print("Done! The harness + AWS Skills produced a working AWS agent.")
print("=" * 60)
finally:
if not args.skip_cleanup and harness_id:
print("\nCleaning up...")
try:
control.delete_harness(harnessId=harness_id)
print(f" Deleted harness: {harness_id}")
except Exception as e:
print(f" Warning: failed to delete harness: {e}")
if __name__ == "__main__":
main()
+21 -3
View File
@@ -11,8 +11,8 @@ The new managed agent harness feature in AgentCore replaces all that upfront bui
| Folder | What's inside |
|:-------|:--------------|
| `00-getting-started/` | Core workflow: create harness, invoke, ExecuteCommand |
| `01-advanced-examples/` | Custom containers, gateway, execution limits, MCP, skills, VPC, OAuth |
| `02-use-cases/` | End-to-end applications (travel agent, webapp visual testing) |
| `01-advanced-examples/` | Custom containers, gateway, execution limits, MCP, skills, VPC, OAuth, AWS Skills, S3 filesystem |
| `02-use-cases/` | End-to-end applications (travel agent, webapp visual testing, AWS builder agent) |
| `utils/` | Shared IAM and boto3 client helpers used by all scripts |
## How this tree is organized
@@ -25,8 +25,10 @@ self-contained — copy any folder and it runs independently.
- **By feature** → `01-advanced-examples/<feature>/`
- **By end-to-end scenario** → `02-use-cases/<use-case>/`
- **By tool type** → MCP: `04-mcp-integration/`, Browser: `01-travel-agent/` (Part 5), Skills: `05-agent-skills/`
- **By tool type** → MCP: `04-mcp-integration/`, Browser: `01-travel-agent/` (Part 5), Skills: `05-agent-skills/` (custom) and `13-aws-skills/` (native AWS Skills)
- **Auth patterns** → `07-oauth/` (JWT inbound + OAuth outbound)
- **Persistent storage** → `14-s3-filesystem/` (mount S3 as the agent filesystem; includes an LLM wiki)
- **Build an agent with AWS Skills** → `02-use-cases/03-aws-builder-agent/` (harness + AWS Skills = an AWS engineering agent)
## AgentCore CLI
@@ -99,11 +101,27 @@ python 01-advanced-examples/04-mcp-integration/mcp_integration.py
# Agent skills (xlsx spreadsheets)
python 01-advanced-examples/05-agent-skills/agent_skills.py
# AWS Skills (native skill bundles from the AWS Agent Toolkit)
python 01-advanced-examples/13-aws-skills/aws_skills.py
# S3 filesystem mount (persistent storage across sessions; requires VPC subnets + SGs)
python 01-advanced-examples/14-s3-filesystem/s3_filesystem.py \
--access-point-arn arn:aws:s3files:REGION:ACCOUNT:file-system/fs-xxxx/access-point/fsap-xxxx \
--subnet-ids subnet-xxxx --security-group-ids sg-xxxx
# S3-backed LLM wiki (ingest → query → lint)
python 01-advanced-examples/14-s3-filesystem/s3_llm_wiki.py \
--access-point-arn arn:aws:s3files:REGION:ACCOUNT:file-system/fs-xxxx/access-point/fsap-xxxx \
--subnet-ids subnet-xxxx --security-group-ids sg-xxxx
# OAuth + JWT auth
export HARNESS_USER_NAME="testuser"
export HARNESS_USER_PASS="TestPassword123!"
python 01-advanced-examples/07-oauth/oauth_gateway.py
# AWS builder agent (harness + AWS Skills builds a serverless app)
python 02-use-cases/03-aws-builder-agent/aws_builder_agent.py
# Travel guide agent
python 02-use-cases/01-travel-agent/travel_agent.py