feat(agentcore-harness): add AgentCore Harness generator - #1005
feat(agentcore-harness): add AgentCore Harness generator#1005ktibbs9417 wants to merge 14 commits into
Conversation
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
cogwirrel
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Nit: I think we can remove this, it's assumed that generators just vend code into your codeabase :)
| 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) | |
There was a problem hiding this comment.
We can remove this, the GeneratorParameters component above should cover the options :)
| 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. |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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" { |
There was a problem hiding this comment.
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}'`, |
| "factory": "./src/agentcore-harness/generator", | ||
| "schema": "./src/agentcore-harness/schema.json", | ||
| "description": "Generate an AgentCore Harness project", | ||
| "metric": "g68", |
There was a problem hiding this comment.
Might need to update this to g69 on a rebase :)
| # Add a managed AgentCore Harness agent loop | ||
| pnpm nx g @aws/nx-plugin:agentcore-harness | ||
|
|
| "enquirer": "2.4.1", | ||
| "esbuild": "0.28.1", | ||
| "exponential-backoff": "3.1.3", | ||
| "fast-check": "4.9.0", |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
perhaps we should implement IConnectable too for connecting with services in a VPC such as rdb like #610
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.
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_harnessresource, an execution role with a non-obvious least-privilege baseline, runtime-configuration registration, and a local invocation client. This adds anagentcore-harnessgenerator that does that consistently for both CDK and Terraform.Description of changes
Adds the
agentcore-harnessgenerator (metricg68, guide pagedocs/src/content/docs/en/guides/agentcore-harness.mdx).invoke.tsentry point, aninvoke-harness.tsinvocation client over@aws-sdk/client-bedrock-agentcore, atsconfig.json, a README, and reservedinvoke/buildtargets. Runtime configuration is resolved through AppConfig, so nothing is hard-coded and no credentials are emitted.--infra agentcorevends the Harness through the native resource for the resolved IaC provider (--iac cdk|terraform, honouring the workspaceinheritdefault).--infra nonewrites only the project, and a later re-run escalates cleanly to infrastructure using the creation defaults persisted in project metadata.addToRolePolicyor theadditional_execution_role_policy_statementsvariable.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.generators.json, the docs sidebar, the plugin README, POWER/SKILL manifests, and the e2e smoke matrices, and addsfast-checkas a root dev dependency for the property tests.The branch also merges
mainso 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 tog68becauseg60was taken byts#website, and dependencies now go through the catalog-awareaddDependenciesToPackageJsonwrapper so generated versions are recorded in the pnpm catalog like every other generator.Description of how you validated changes
schema.d.ts/generators.jsoncontract, the rendered CDK construct and Terraform module, the invocation client, option resolution, preflight diagnostics and the guide.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.--infra noneescalation to--infra agentcore, and a differently-named second Harness leaving the first intact, for both providers.idempotencyandinfra-nonesmoke matrices and adds auser-owned-fileslane asserting generated user-editable files survive re-runs byte-for-byte.pnpm nx run @aws/nx-plugin:compileand 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