Skip to content

feat: add managed tool catalog and build environments - #397

Open
JWThewes wants to merge 26 commits into
mainfrom
feat/390-managed-build-environments
Open

feat: add managed tool catalog and build environments#397
JWThewes wants to merge 26 commits into
mainfrom
feat/390-managed-build-environments

Conversation

@JWThewes

@JWThewes JWThewes commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • ship one protected Standard environment with Node.js and Python, plus managed Java, Go, Rust, Maven, and Gradle tool definitions
  • let platform administrators import additional ARM64 tools such as .NET from public HTTPS archives without changing platform source
  • normalize each verified tool version into an immutable OCI artifact with retained provenance, SBOM, size, scan findings, and functional verification evidence
  • compose exact published tool-version digests into immutable environment revisions with dependency resolution, conflict checks, and projected AgentCore image-size enforcement
  • preserve reviewed Critical and High findings through publication after explicit administrator acceptance
  • snapshot the exact environment revision, image digest, runtime version, endpoint, compatibility version, tools, and verification result into every intent
  • add Tools and Environments views to Platform Settings, plus project assignment and repository compatibility warnings

Tool ingestion

  • validate public HTTPS sources and every redirect
  • compute and retain content-addressed source digests
  • distinguish Publisher verified evidence from Platform pinned imports
  • reject unsafe archives, traversal, extraction bombs, unsafe symlinks, credential-bearing URLs, and oversized payloads
  • run advanced installers in a credentialless container without metadata, private-network, Docker-socket, or host access
  • run version and functional verification networklessly as the non-root runtime user
  • support Java, Go, Rust, Maven, Gradle, .NET, generic CLI, and bounded custom verification fixtures

Compatibility

  • Standard is the only seeded environment
  • new and rebuilt environments use catalog-backed schema-v2 recipes
  • historical schema-v1 records remain readable for audit but cannot be built, retried, or published
  • no reset API, Lambda, IAM role, route, or Platform Settings view is shipped
  • the sole development account's legacy non-Standard runtimes, images, lookups, revisions, environments, and migration marker were removed manually

Verification

  • complete repository suite: 140 files, 2,591 tests passed
  • complete frontend suite: 73 files, 482 tests passed
  • managed tool and environment control plane: 10 files, 79 tests passed
  • npm run format:check
  • npm run lint
  • npm run secretlint
  • npm run sdk:check
  • npm --prefix frontend run typecheck
  • npm --prefix frontend run build
  • all remaining environment Lambda handlers bundled successfully
  • terraform fmt -recursive -check terraform
  • Terraform provider-schema validation with a local backend; only the two existing DynamoDB deprecation warnings remain
  • root and frontend production dependency audits report zero vulnerabilities

Terraform was not planned or applied against AWS. The deployed-stack workflow and .NET catalog example are documented in docs/development/testing.md.

Closes #390

@JWThewes
JWThewes marked this pull request as draft August 11, 2026 09:11
Comment thread lambda/environments/recipe.js Fixed
@JWThewes JWThewes changed the title feat: add managed build environments feat: add managed tool catalog and build environments Aug 13, 2026
Comment thread lambda/environments/test/tool-catalog.test.js Fixed
@JWThewes
JWThewes marked this pull request as ready for review August 14, 2026 12:51
Comment on lines +80 to +88
environmentId: environment.environmentId,
name: environment.name,
revisionId: revision.revisionId,
imageDigest: revision.imageDigest,
runtimeVersion: revision.runtimeVersion ?? null,
runtimeArn: revision.runtimeArn,
runtimeEndpoint: revision.runtimeEndpoint ?? null,
compatibilityVersion,
verification: revision.verification ?? null,

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.

Suggested change
environmentId: environment.environmentId,
name: environment.name,
revisionId: revision.revisionId,
imageDigest: revision.imageDigest,
runtimeVersion: revision.runtimeVersion ?? null,
runtimeArn: revision.runtimeArn,
runtimeEndpoint: revision.runtimeEndpoint ?? null,
compatibilityVersion,
verification: revision.verification ?? null,
environmentId: environment.environmentId,
name: environment.name,
revisionId: revision.revisionId,
imageDigest: revision.imageDigest,
runtimeVersion: revision.runtimeVersion ?? null,
runtimeArn: revision.runtimeArn,
runtimeEndpoint: revision.runtimeEndpoint ?? null,
compatibilityVersion,
verification: revision.verification ?? null,
tools: revision.flattenedRecipe?.resolvedTools ?? [],

as documented in https://github.com/aws-samples/sample-collaborative-ai-dlc/pull/397/changes#diff-51840c6001bebb21d56b029ca1ff58b642117adcdf734564433f7c6e8051cdabR86, we might want to also have the tools. Maybe add a test asserting those snapshots survive into the intent and audit output.

Comment on lines +68 to +82
Create a `.NET SDK` tool using an official Linux ARM64 SDK archive and the
`.NET` preset. Confirm source inspection, normalization, scanning, `dotnet
--version`, and a real console build succeed, then publish it.

Create catalog-backed environments based on Standard:

1. Select Go and publish the resulting environment.
2. Select Maven and confirm the recommended Java version is added
automatically.
3. Select `.NET SDK` and publish the resulting environment.
4. Confirm every generated Dockerfile copies tools from exact OCI digests and
retains the protected base entrypoint, command, user, port, and health
behavior.
5. Confirm the projected and actual compressed image sizes stay below the
configured AgentCore image limit.

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.

not clear, we mix languages, .NET seems important, I don't understand

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.

Quite verbose, without diagram, a bit painful to read. Can we imagine a TL;DR or the main instructions/options first and "going further" separated. Or the actionable options first and FYI later. Something to make it more digest.

@@ -1,3 +1,3 @@
# Projects and Settings

A project is the workspace where intents run. It represents a product, service, or feature area and groups together:

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.

By reading this, I see we still speak about "projects", not "spaces", created this issue for follow up: #409

verification: fallback.verification ?? { status: 'PASSED', source: 'legacy-runtime' },
});

const supportsCompatibilityVersion = (candidate, current) => {

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.

Duplication of isSupportedCompatibilityVersion in recipe.js:701

throw new Error('deterministic runtime validation failed');
}
} finally {
await runtimeClient.send(

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.

No try catch of the StopRuntimeSessionCommand. A stop failure masks the real validation error; because the replacement error is retryable, a broken revision returns {pending:true} and is retried forever instead of being marked FAILED.

imageBuild: 'PASSED',
baseDigest: 'PASSED',
architecture: 'arm64',
nonRoot: deterministic.nonRoot === true,

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.

The deterministic check at lines 517–523 only fails verification when ok, nonce, or compatibilityVersion are wrong — it never looks at nonRoot. Yet at line 544 verification is hard-coded to status: 'PASSED', and line 549 records nonRoot: deterministic.nonRoot === true. Because ok is defined as workspaceWritable && protectedRuntime (in managed-runtime-check.js) and excludes nonRoot, a runtime running as root returns nonRoot: false but still passes the check at 517–523 → the revision goes READY with status: 'PASSED' and nonRoot: false recorded, which contradicts itself. Please either add nonRoot === true to the condition at 517–523, or stop recording a field verification doesn't gate on. (Low priority — non-root is already enforced at build time in recipe.js.)

Comment thread lambda/intents/index.js
return response(405, { error: 'Method not allowed' });
} catch {
console.error('intents handler error');
return response(500, { error: 'Internal server error' });

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.

resolveEnvironmentSnapshot can throw different errors (ENVIRONMENT_NOT_PUBLISHED, ENVIRONMENT_COMPATIBILITY_UNSUPPORTED, ...) but the catch simply return an opaque 500, loosing the interesting information. Please bind the error and map these codes to a 409, the way this file already does at line 1283 for SOURCE_CONTROL_NOT_READY.

}
}
})().finally(() => {
initialization = null;

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.

initialize (lines 328...) memoizes its promise with ??=, but nulls it in .finally, which runs on success too. So the next request re-runs the whole block — seedSystemTools(), listVersionsByStatus('DRAFT'), listVersionsByStatus('FAILED') on every call; the cache only dedupes concurrent calls, never across requests. Compare lambda/environments/index.js:352–359, which nulls the cache in .catch (error only) so a successful init persists. Please switch to the .catch pattern here.

Comment thread lambda/projects/index.js
sk: `REV#${revisionId}`,
});

const readPublishedEnvironment = async (environmentId) => {

@jeromevdl jeromevdl Aug 19, 2026

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.

Two code paths gate the same project environment_id differently:

  • readPublishedEnvironment (here), used by the assignment PUT handler, accepts on status only — line 1002: ['PUBLISHED', 'SUPERSEDED'].includes(revision.status). It never checks compatibility.
  • resolveEnvironmentSnapshot (environment-snapshot.js:32–95), used at intent-create, additionally requires supportsCompatibilityVersion(...) at lines 73–81.

So: publish a revision at compatibility version 1 → assign it to a project (passes) → platform advances RUNTIME_COMPATIBILITY_VERSION to 3 → intent-create now throws ENVIRONMENT_COMPATIBILITY_UNSUPPORTED. An environment that was accepted at assignment is rejected at run time. Please apply the same acceptancrules (ideally one shared resolver) in both places.

new GetCommand({
TableName: processTable,
Key: { pk: `EXEC#${intentId}`, sk: 'META' },
ConsistentRead: true,

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 might not need the ConsistentRead here. The execution snapshot is written once at setup and is immutable per execution, so eventual consistency is safe here.

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.

Why having 2 recipe.js (v1 and v2) ? Both are introduced in this PR so using v1 and v2 is a bit misleading and introduces some idea of legacy while there is not: 2 schemas, 2 validators, 2 Dockerfile/verification/build-context generators, 2 test suites...
If you want to distinguish system fixed allow-list from catalog-driven, maybe be more explicit in the naming than v1 and v2.

@jeromevdl jeromevdl 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.

This feature is quite complex and there is legitimately complexity in the PR to handle this, but the PR adds 20-30% incidental complexity on top of it because of duplicated subsystems:

  • recipe v1 vs v2
  • 3 different ways to evaluate if environment is ready: readPublishedEnvironment, resolveEnvironmentSnapshot, publishedBase
  • one stack for environments, one for tools very close to the environment (createStore, createHandler, startBuild, createStatusHandler, ... in index/status/store vs tools-index/tools-status/tools-store, both modeled as: a versioned entity with DRAFT/BUILDING/READY/PUBLISHED-style revisions, built via CodeBuild, whose status is reconciled by a polling handler, fronted by a CRUD API handler backed by a DynamoDB store. That shared lifecycle is implemented twice. I've compared the shapes and signatures here, not diffed the two stacks line by line, so I can't put a precise "X% identical" number on it.

Can you check what could be merged (schema v1/v2 ? duplications of env and tools ? one environment resolution function ?) and maybe others to simplify / reduce the amount of code?

@@ -129,11 +130,24 @@ const discussionSessionIdFor = (intentId, discussionId) =>
`aidlc-discuss-${intentId}-${discussionId}`.padEnd(33, '0');

export const invokeDiscussionAssist = async ({ intentId, payload }) => {

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.

This PR adds V2_PROCESS_TABLE to the discussions lambda, so invokeDiscussionAssist now does a DynamoDB Get where it previously had zero DB dependency — and if (!meta) throw. For a normal intent the META row always exists (created at DRAFT), so it's unreachable in practice, but a throttled or IAM-denied Get now throws where before it couldn't. New blast radius on a hot interactive path

@jeromevdl

Copy link
Copy Markdown
Contributor

Also, agents/index.js:79-80,107-108 (fetchRuntimeCapabilities, verifyMcpServers) still hit the core AGENTCORE_RUNTIME_ARN, never a project's managed environment.

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.

[RFC]: Managed build environments for agent verification

3 participants