Skip to content

feat(agentcore-harness): add AgentCore Harness generator - #1005

Open
ktibbs9417 wants to merge 14 commits into
awslabs:mainfrom
ktibbs9417:main
Open

feat(agentcore-harness): add AgentCore Harness generator#1005
ktibbs9417 wants to merge 14 commits into
awslabs:mainfrom
ktibbs9417:main

Conversation

@ktibbs9417

Copy link
Copy Markdown

Reason for this change

There is currently no generator for Amazon Bedrock AgentCore Harness. A Harness is a managed agent loop powered by Strands Agents, owning deployment defaults for the model, system prompt, tools, memory, skills, environments, truncation, authorization and execution limits, while the service accepts per-invocation overrides.

Wiring one up by hand means hand-writing the native CfnHarness / aws_bedrockagentcore_harness resource, an execution role with a non-obvious least-privilege baseline, runtime-configuration registration, and a local invocation client. This adds an agentcore-harness generator that does that consistently for both CDK and Terraform.

Description of changes

Adds the agentcore-harness generator (metric g68, guide page docs/src/content/docs/en/guides/agentcore-harness.mdx).

  • Generated project — a standalone invocation project: invoke.ts entry point, an invoke-harness.ts invocation client over @aws-sdk/client-bedrock-agentcore, a tsconfig.json, a README, and reserved invoke / build targets. Runtime configuration is resolved through AppConfig, so nothing is hard-coded and no credentials are emitted.
  • Infrastructure--infra agentcore vends the Harness through the native resource for the resolved IaC provider (--iac cdk|terraform, honouring the workspace inherit default). --infra none writes only the project, and a later re-run escalates cleanly to infrastructure using the creation defaults persisted in project metadata.
  • IAM — the generated execution role carries the twelve reviewed baseline permission statements from the AgentCore Harness security guidance, scoped to AWS-owned browser/code-interpreter resources, deployment-account workload identities and the harness-prefixed managed memory ARN. Inbound authorization is IAM by default. Customer-owned resources (Gateways, BYO memory, custom browsers/interpreters, secrets, VPCs) stay explicit opt-in extensions via addToRolePolicy or the additional_execution_role_policy_statements variable.
  • Idempotency — project creation is guarded, user-editable files are written KeepExisting, and wiring (exports, runtime config, dependsOn, metrics) is keyed so re-runs converge. Incompatible reserved targets or explicit options that conflict with persisted metadata fail preflight before any tree mutation.
  • Supporting wiring — registers the generator in generators.json, the docs sidebar, the plugin README, POWER/SKILL manifests, and the e2e smoke matrices, and adds fast-check as a root dev dependency for the property tests.

The branch also merges main so it applies cleanly, and includes two follow-up commits that reconcile the generator with changes that landed upstream in the meantime: the generator metric moved to g68 because g60 was taken by ts#website, and dependencies now go through the catalog-aware addDependenciesToPackageJson wrapper so generated versions are recorded in the pnpm catalog like every other generator.

Description of how you validated changes

  • Unit and contract tests — 26 spec files covering the schema/schema.d.ts/generators.json contract, the rendered CDK construct and Terraform module, the invocation client, option resolution, preflight diagnostics and the guide.
  • Property-based tests (fast-check) — cross-provider template semantics, the default IAM boundary and its extension points, the derived dependency set, ARN parsing and discovery, session handling, stream projection and failure modes, prompt mapping, credential-free output, and user-ownership/wiring idempotency.
  • Idempotency coverage as required by the contributing guide — re-run with the same options, --infra none escalation to --infra agentcore, and a differently-named second Harness leaving the first intact, for both providers.
  • e2e — registers the generator in the idempotency and infra-none smoke matrices and adds a user-owned-files lane asserting generated user-editable files survive re-runs byte-for-byte.
  • pnpm nx run @aws/nx-plugin:compile and the generator's full suite pass locally (271 tests).

Note that generation only writes files: the Harness exists in AWS after the generated infrastructure is deployed through the normal workflow. Nothing validates or invokes a live Harness during generation, so no AWS credentials are required.

Issue # (if applicable)

N/A

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

Tibbs added 6 commits July 23, 2026 17:52
Bring the agentcore-harness branch up to date with awslabs/nx-plugin-for-aws
main (v1.0.0-rc.48) ahead of raising a pull request.

Conflicts resolved:

- docs/astro.config.mjs: keep both the agentcore-harness and the newly
  added ts#dcr-proxy sidebar entries.
- packages/nx-plugin/src/utils/versions.ts: take upstream's bumped AWS SDK
  versions and CVE pins, retaining the harness SDK client entries at the
  bumped baseline.
- pnpm-lock.yaml: take upstream's lockfile and reconcile via pnpm install.
- packages/nx-plugin/LICENSE-THIRD-PARTY: take upstream's generated
  attribution file.
Resolve integration gaps surfaced by merging upstream/main:

- Reassign the generator metric from g60 to g68. Upstream claimed g60 for
  ts#website, and duplicate metrics fail the uniqueness guard.
- Route dependencies through the catalog-aware addDependenciesToPackageJson
  wrapper so generated versions are recorded in the pnpm catalog like every
  other generator, rather than writing raw ranges that diverge on re-run.
- Track the bumped AWS SDK and Terraform provider baselines in the version
  assertions.
- Include AgentCoreManagedMemory in the baseline permission expectations.
  Both templates emit twelve statements, not eleven.
- Strip standalone HCL comment lines before converting jsonencode blocks to
  JavaScript so annotated policy statements parse.
- Resolve catalog: references when asserting exact version pins.
- Register the generator in the preset README snapshot.
Restore files to upstream state so the pull request carries only the
agentcore-harness change:

- pnpm-lock.yaml: add just the fast-check and pure-rand entries instead of
  the ~2000 line rewrite a full install produces. Because the repo sets
  auto-install-peers=true, changing the dependency set makes pnpm re-resolve
  auto-installed peers and pick supports-color 10.2.2, which duplicates the
  babel, webpack and verdaccio trees alongside the existing 7.2.0 copies.
  The trimmed lockfile installs cleanly under pnpm i --frozen-lockfile and
  prunes 196 redundant packages.
- packages/nx-plugin/LICENSE-THIRD-PARTY: regenerating on macOS swaps the
  linux-arm64 attributions for darwin-arm64 ones. Keep the Linux-generated
  file; CI excludes this path from its mutation check.
- .gitignore: drop local tooling ignores unrelated to the feature.

packages/nx-plugin/src/sdk/ts.ts keeps a three line export reorder. The
committed file is not clean under the repository's own biome config, so the
pre-commit formatter reapplies the sort whenever the file is staged.
Each of the property's 100 cases runs the generator twice, once per
infrastructure provider, with real formatting. That lands a few seconds under
the default 120s budget when the file runs alone, so the test times out as
soon as the suite runs in parallel. Allow 300s, matching the other
whole-workspace property tests in this generator.
@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.79570% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.47%. Comparing base (8552a5f) to head (1d429fd).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...nx-plugin/src/agentcore-harness/resolve-options.ts 65.78% 7 Missing and 6 partials ⚠️
packages/nx-plugin/src/utils/versions.ts 60.00% 1 Missing and 1 partial ⚠️
...kages/nx-plugin/src/agentcore-harness/generator.ts 96.55% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1005      +/-   ##
==========================================
- Coverage   88.51%   88.47%   -0.04%     
==========================================
  Files         179      182       +3     
  Lines        6650     6758     +108     
  Branches     1615     1638      +23     
==========================================
+ Hits         5886     5979      +93     
- Misses        364      373       +9     
- Partials      400      406       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cogwirrel cogwirrel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ktibbs9417! Great start, tested deploying this and it works nicely! A few comments below :)


Generate an [Amazon Bedrock AgentCore Harness](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness.html) project. A Harness is a managed agent loop powered by Strands Agents: it owns deployment defaults for the model, system prompt, tools, memory, skills, environments, truncation, authorization, and execution limits, while the service accepts per-invocation overrides for supported fields. Reusing the same Runtime Session ID continues the same Harness session.

The generator only writes files: a standalone invocation project, plus optional CDK or Terraform infrastructure. The Harness exists in AWS after you deploy that infrastructure through your normal workflow; nothing runs, validates, or invokes a live Harness during generation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think we can remove this, it's assumed that generators just vend code into your codeabase :)

Comment on lines +29 to +39
Everything except `name` is optional, with these defaults:

| Option | Default |
| --- | --- |
| `directory` / `subDirectory` | `packages` / the kebab-case harness name (project root `packages/<name>`) |
| `modelId` | `global.anthropic.claude-sonnet-4-6` |
| `systemPrompt` | `You are a helpful AI assistant.` |
| `allowedTools` | `['@builtin']` (accepts 1–64 non-empty patterns) |
| `maxIterations`, `maxTokens`, `timeoutSeconds` | Unset — the AgentCore service defaults apply (positive integers when supplied) |
| `infra` | `agentcore` (`none` skips infrastructure) |
| `iac` | `inherit` (uses your workspace's configured provider) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this, the GeneratorParameters component above should cover the options :)

Comment on lines +58 to +60
Infrastructure is generated when `infra` is `agentcore` (the default). With `infra: none` no infrastructure is generated — set `HARNESS_ARN` to invoke a Harness managed elsewhere, and re-run the generator with `infra: agentcore` later to add infrastructure; existing project files (including your edits) are preserved.

When `iac` is `inherit` (the default), the provider configured for your workspace is used. An explicit `--iac=cdk` or `--iac=terraform` that differs from the provider of your existing shared infrastructure project fails with a diagnostic naming both providers, before any files are written.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: let's remove this line

The CDK construct for deploying your Harness lives in the `common/constructs` folder. Instantiate it from a CDK application:

```ts title="packages/infra/src/stacks/application-stack.ts"
import { MyHarness } from ':my-scope/common-constructs';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll likely need to rebase on the latest main branch - imports are now with @ :)


### Invocation permissions

A caller needs exactly `bedrock-agentcore:InvokeHarness` and `bedrock-agentcore:InvokeAgentRuntime` on the Harness ARN. The CDK construct's `grantInvokeAccess(grantee)` grants exactly these two actions on the base Harness ARN; for Terraform, add an equivalent statement to the calling principal's policy using the `harness_arn` output.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this to something like:

You can grant a caller permissions to invoke the harness as follows:

<Infrastructure>
<Fragment slot="cdk">
The CDK construct's `grantInvokeAccess` method grants the required permissions to invoke the harness

<add an example>
</Fragment>
<Fragment slot="terraform">
...

default = <%- modelIdHcl %>
}

variable "system_prompt" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for CDK too, but let's drop this as a parameter and read it from the relevant PROMPT.md file in the harness project :)

const version = TS_VERSIONS[dep];
if (!version) {
throw new Error(
`No centralized version registered in TS_VERSIONS for dependency '${dep}'`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks!

Comment thread packages/nx-plugin/generators.json Outdated
"factory": "./src/agentcore-harness/generator",
"schema": "./src/agentcore-harness/schema.json",
"description": "Generate an AgentCore Harness project",
"metric": "g68",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need to update this to g69 on a rebase :)

Comment thread packages/nx-plugin/README.md Outdated
Comment on lines +145 to +147
# Add a managed AgentCore Harness agent loop
pnpm nx g @aws/nx-plugin:agentcore-harness

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this :)

Comment thread package.json Outdated
"enquirer": "2.4.1",
"esbuild": "0.28.1",
"exponential-backoff": "3.1.3",
"fast-check": "4.9.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop this when we simplify the unit tests :)

* IAM inbound authorization; supply a custom JWT authorizer through the
* native property to change that.
*/
export class <%- nameClassName %> extends Construct implements iam.IGrantable {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we should implement IConnectable too for connecting with services in a VPC such as rdb like #610

Tibbs added 7 commits July 30, 2026 19:42
Sync the agentcore-harness branch with upstream/main at 8552a5f
(v1.0.0-rc.50), ten commits ahead of the branch point.

The merge is clean: no file conflicted. Pre-commit biome dropped one
redundant escape in an upstream-added spec template literal.
The harness shared metric g68 with the upstream ts#nx-migration
generator, tripping the registry uniqueness guard. ts#nx-migration
was registered first, so the harness is the one that moves.

g68 is the highest identifier in the merged registry across all 69
generators, and there are no gaps below it, so g69 is the next unused
sequential identifier.
Delete 31 files: the 26 harness spec files that the rewritten
generator.spec.ts supersedes, preflight.ts, the three project templates
the chat target replaces (invoke.ts, invoke-harness.ts, tsconfig.json),
and the redundant user-owned-files e2e smoke test.

Remove fast-check from the workspace devDependencies and regenerate the
lockfile. Its only consumers were the deleted property specs, and it has
no transitive dependents, so it leaves the lockfile entirely.

Replace the preflight guard with the projectExists reuse path that
agentcore-gateway already uses, and stop declaring the reserved invoke
and build targets at project creation. The guard rejected reruns the
generator serves idempotently through OverwriteStrategy.KeepExisting, so
it added a failure mode rather than protection.

Drop the two user-owned-files entries from the smoke_test CI matrices,
which match zero tests now that the spec is gone.

generator.spec.ts fails 15 tests at this commit, all of them assertions
on the deleted templates, the removed targets, and the removed preflight
rejections. It is rewritten wholesale in a following commit.
Reduce schema.json and schema.d.ts from twelve options to six, dropping
modelId, systemPrompt, allowedTools, maxIterations, maxTokens and
timeoutSeconds. Every one of them exposed a model or agent-loop tuning
knob the user now owns directly on the vended construct or module, so
carrying them through the generator only created a second place for the
same value to live.

Reduce ResolvedAgentcoreHarnessOptions from fourteen fields to eight and
delete the validators the removed options needed. runtimeConfigPath goes
with them: rc already identifies the agentcore.harnesses.<key> entry and
the templates interpolate nameClassName directly.

Replace the invoke and build targets with a single chat target running
tsx ./scripts/chat.ts, and reduce the persisted metadata to exactly
generator, name, rc and auth. The removed fields recorded creation
options that no longer exist, so the write simplifies to an
unconditional addGeneratorMetadata call. Drop typescript from the added
dev dependencies and call resolveIac directly in place of the
resolveHarnessIac wrapper.

Declare global.anthropic.claude-sonnet-4-6 as a literal model id in both
the CDK and Terraform harness templates. Make tools opt-in rather than
on by default: allowedTools becomes an optional construct props field
applied only when supplied, and the Terraform allowed_tools variable and
its two validations are gone.

Add three suppressRules calls on the CDK execution role policy, one each
naming EcrPublicTokenAccess, StsForEcrPublicPull and XRayTracingAccess
in the reason, and inline #checkov:skip comments on the retained
wildcard-resource IAM statements in the Terraform module. The
suppression helper targets a CfnResource rather than an individual
statement, so the sids stay traceable through the reasons.

Compute harnessNamePrefix in TypeScript rather than HCL. toClassName
does not guarantee a leading letter (it prefixes _ for digit-leading
words), so the generator applies an explicit H prefix and caps the
result at 31 characters, replacing the substr/regexall expression in the
Terraform locals block.

Rewrite the harness project README template around the four-file project
and the chat target, and drop the harness quick-start entry from the
plugin readme. The root readme is the pre-commit hook's mirror of the
plugin readme, so it carries the same deletion.

The prompt-related reductions are deliberately absent. systemPromptJson
and var.system_prompt stay until the PROMPT.md read replaces them, so
that this commit still renders a working template.

generator.spec.ts fails 23 tests at this commit, all of them assertions
on the removed options, the removed targets and metadata fields, the
deleted invoke.ts and tsconfig.json templates, and the deleted preflight
checks. It is rewritten wholesale in a following commit.
…d add a chat client

Move the harness system prompt out of the generator and into the user-owned
`src/PROMPT.md`, and replace the deleted single-turn invocation client with a
multi-turn chat client.

The prompt is now a generated project file the user edits directly. Both
providers read it from the workspace root rather than carrying the text through
the template context: the CDK construct reads it with `fs.readFileSync` anchored
on `findWorkspaceRoot`, so the path stays correct once the construct compiles to
`dist/`, and the Terraform module reads it with `file()` walking up from
`path.module`. `HarnessTemplateContext` gains `promptPathFromCdk` and
`promptPathFromTerraform`, both derived from the same `projectRoot` the project
is written to, so explicit `directory` and `subDirectory` placement needs no
extra logic.

Because no prompt text reaches the templates any more, the Terraform
`system_prompt` input variable is removed along with the HCL string-literal
helpers and the starter-prompt constant that fed them: `hclStringLiteral`,
`systemPromptJson`, `systemPromptHcl` and `DEFAULT_HARNESS_SYSTEM_PROMPT` are
all deleted.

`scripts/chat.ts` implements `ChatAdapter` and is driven by `chatLoop` from
`agent-chat-cli`, invoking the deployed harness with `InvokeHarnessCommand`. It
carries over the behaviour of the removed client: `HARNESS_ARN` takes precedence
over runtime-config discovery via `RUNTIME_CONFIG_APP_ID`, with an error naming
both when neither is set; the region is derived from the ARN; and one session id
per run means every turn continues the same harness session.

`generator.ts` is unchanged here — it already passes `projectRoot` and
`harnessNamePrefix`, and the prompt path is computed in the infrastructure
helper rather than added as a prop. `versions.ts` is also unchanged: every
dependency `scripts/chat.ts` needs is already declared there.

`generator.spec.ts` still asserts the previous option surface and is rewritten
wholesale in the following commit.
Replaces the 34.7 KB, 40-test generator.spec.ts with a focused suite
covering the reduced generator: five behaviour blocks plus a snapshot,
21 declarations expanding to 25 cases.

fast-check is not reintroduced. Every correctness property the design
names is now checked with example-based assertions at representative
points: the generated project surface and target set by set equality,
the prompt path for the default root and for an explicit directory plus
nested subDirectory, the name prefix across a four-row boundary table,
and no-tools-by-default on both the CDK resource and the Terraform
module.

Property 1, user-owned file preservation, is restored at the unit
level. It is the only remaining proof of preservation now that the
e2e user-owned-files spec and preflight's re-run rejection are both
gone, so it asserts full-content equality on an edited src/PROMPT.md
and an edited vended construct rather than a marker substring, which
would still pass with KeepExisting dropped.

Adds an absent-wiring case: after the user deletes the harness export
line, a re-run restores exactly one copy, neither absent nor
duplicated.

Adds a snapshot over the four rendered project files - project.json,
src/PROMPT.md, scripts/chat.ts, README.md - for the default
--name=my-harness --iac=cdk run. The vended construct and Terraform
module stay out: they are long and churn for reasons unrelated to this
generator, so the blocks above keep them on targeted assertions.
scripts/chat.ts and src/PROMPT.md appear in a snapshot for the first
time here.

The finished file lands near 23 KB against the design's ~12 KB target.
The overage is verbatim data that does not compress: six checkov skip
strings quoted in full, the four-row name-prefix boundary table, and
the full-content preservation helpers. It is still a third smaller
than the 34.7 KB it replaces, with 40 tests reduced to 25 focused
cases.
Rewrite the harness guide for the four-file project and the chat target,
and delete the "Connecting to Gateways and MCP servers" roadmap section
outright rather than replacing it, since attachment ships in a follow-up
spec and the forward-looking promise was itself what the reviewer
questioned.

Drop the redundant wiring-count assertions from the idempotency smoke
test in favour of the surviving whole-workspace git no-diff check, which
subsumes them: a re-run that adds a duplicate export changes a tracked
file and fails that check.

Retarget the infra-none user-edit fixture to src/PROMPT.md, since the
invoke.ts it previously edited no longer exists.

Add harness deploy-and-invoke coverage for both providers: instantiate
the harness in the CDK and Terraform e2e stacks with an ARN output, and
assert on it via a new invokeAgentCoreHarness helper that reaches the
harness through InvokeHarnessCommand, because a harness has no runtime
invocation endpoint. These assertions need AWS credentials, so CI
validates them rather than a local run.

Declare @aws-sdk/client-bedrock-agentcore 3.1090.0 in the root manifest,
matching the pin in versions.ts. The e2e helper needs it and the e2e
project has no package.json of its own, so it resolves from the root.

Correct the project README template's tool-configuration note:
allowed_tools is set on the module's aws_bedrockagentcore_harness
resource, not passed as a module argument, since the module exposes no
such variable and passing one would fail at plan time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants