diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 0e3ec303e..6cdb6e59c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -35,6 +35,12 @@ jobs: matrix: variant: [Released, Prerelease] build: [GA, Preview] + # The prerelease channel produces a single tarball (GA and preview builds are + # identical post-cutover), so there is no separate prerelease preview artifact. + # Released/Preview is kept — it verifies the @preview npm tag stays publishable. + exclude: + - variant: Prerelease + build: Preview steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 @@ -72,7 +78,6 @@ jobs: "Released/GA") SPEC="@aws/agentcore" ;; "Released/Preview") SPEC="@aws/agentcore@preview" ;; "Prerelease/GA") SPEC="${PRERELEASE_BASE_URL}/agentcore-cli-prerelease.tgz" ;; - "Prerelease/Preview") SPEC="${PRERELEASE_BASE_URL}/agentcore-cli-prerelease-preview.tgz" ;; *) echo "Unknown variant/build combination" >&2; exit 1 ;; esac echo "Installing: $SPEC" diff --git a/.github/workflows/e2e-tests-full.yml b/.github/workflows/e2e-tests-full.yml index ce7878d95..4cd50d2ef 100644 --- a/.github/workflows/e2e-tests-full.yml +++ b/.github/workflows/e2e-tests-full.yml @@ -30,7 +30,6 @@ jobs: strategy: fail-fast: false matrix: - cli-build: [preview, ga] shard: ['1/5', '2/5', '3/5', '4/5', '5/5'] steps: - uses: actions/checkout@v6 @@ -60,10 +59,8 @@ jobs: E2E,${{ secrets.E2E_SECRET_ARN }} parse-json-secrets: true - run: npm ci - - name: Build CLI (${{ matrix.cli-build }}) + - name: Build CLI run: npm run build - env: - BUILD_PREVIEW: ${{ matrix.cli-build == 'preview' && '1' || '0' }} - name: Generate GitHub App Token id: app-token uses: actions/create-github-app-token@v3 @@ -97,7 +94,7 @@ jobs: CDK_REPO: ${{ secrets.CDK_REPO_NAME }} - name: Install CLI globally run: npm install -g "$(npm pack | tail -1)" - - name: Run E2E tests (${{ matrix.cli-build }}, shard ${{ matrix.shard }}) + - name: Run E2E tests (shard ${{ matrix.shard }}) env: AWS_ACCOUNT_ID: ${{ steps.aws.outputs.account_id }} AWS_REGION: ${{ inputs.aws_region || 'us-east-1' }} @@ -112,8 +109,6 @@ jobs: CDP_API_KEY_ID: ${{ env.E2E_CDP_API_KEY_ID }} CDP_API_KEY_SECRET: ${{ env.E2E_CDP_API_KEY_SECRET }} CDP_WALLET_SECRET: ${{ env.E2E_CDP_WALLET_SECRET }} - BUILD_PREVIEW: ${{ matrix.cli-build == 'preview' && '1' || '0' }} - run: npx vitest run --project e2e --shard=${{ matrix.shard }} browser-tests: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4e8edb4aa..522f97703 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -100,18 +100,16 @@ jobs: - run: npm ci - - name: Bundle GA and preview tarballs + - name: Bundle tarball run: | npm run bundle - GA_TARBALL=$(ls aws-agentcore-*.tgz | grep -v preview | head -1) - PREVIEW_TARBALL=$(ls aws-agentcore-*-preview-*.tgz | head -1) - echo "GA_TARBALL=$PWD/$GA_TARBALL" >> "$GITHUB_ENV" - echo "PREVIEW_TARBALL=$PWD/$PREVIEW_TARBALL" >> "$GITHUB_ENV" + TARBALL=$(ls aws-agentcore-*.tgz | head -1) + echo "TARBALL=$PWD/$TARBALL" >> "$GITHUB_ENV" env: AGENTCORE_CDK_PATH: /tmp/cdk-repo - - name: Install GA CLI globally - run: npm install -g "$GA_TARBALL" + - name: Install CLI globally + run: npm install -g "$TARBALL" - name: Detect changed e2e test files id: changed @@ -166,10 +164,7 @@ jobs: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/payment-strands-bedrock.test.ts ${{ steps.changed.outputs.ga_extra }} - - name: Install preview CLI globally - run: npm install -g "$PREVIEW_TARBALL" - - - name: Run E2E tests (preview/harness) + - name: Run E2E tests (harness) env: AWS_ACCOUNT_ID: ${{ steps.aws.outputs.account_id }} AWS_REGION: ${{ inputs.aws_region || 'us-east-1' }} @@ -180,5 +175,4 @@ jobs: E2E_S3_ACCESS_POINT_ARN: ${{ env.E2E_S3_ACCESS_POINT_ARN }} E2E_FILESYSTEM_SUBNET_ID: ${{ env.E2E_FILESYSTEM_SUBNET_ID }} E2E_FILESYSTEM_SECURITY_GROUP_ID: ${{ env.E2E_FILESYSTEM_SECURITY_GROUP_ID }} - BUILD_PREVIEW: '1' run: npx vitest run --project e2e e2e-tests/harness-bedrock.test.ts ${{ steps.changed.outputs.harness_extra }} diff --git a/.github/workflows/prerelease-tarball.yml b/.github/workflows/prerelease-tarball.yml index daddcd7c8..c3dd14f87 100644 --- a/.github/workflows/prerelease-tarball.yml +++ b/.github/workflows/prerelease-tarball.yml @@ -62,23 +62,16 @@ jobs: # Delete existing release if it exists (to update the tarballs) gh release delete "$TAG" --yes --cleanup-tag 2>/dev/null || true - # Create a new pre-release with both tarballs + # Create a new pre-release with the tarball gh release create "$TAG" \ "${TARBALL_BASE}.tgz" \ - "${TARBALL_BASE}-preview.tgz" \ --title "Prerelease" \ - --notes "Auto-generated tarballs from the latest commit on main. + --notes "Auto-generated tarball from the latest commit on main. Version: \`${VERSION_SUFFIX}\` (cli-cdk) - **GA build** (no harness features): \`\`\` npm install -g https://github.com/aws/agentcore-cli/releases/download/prerelease/${TARBALL_BASE}.tgz - \`\`\` - - **Preview build** (harness features enabled): - \`\`\` - npm install -g https://github.com/aws/agentcore-cli/releases/download/prerelease/${TARBALL_BASE}-preview.tgz \`\`\`" \ --prerelease \ --target "${{ github.sha }}" diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml index 6a99f94f0..a93042260 100644 --- a/.github/workflows/release-main-and-preview.yml +++ b/.github/workflows/release-main-and-preview.yml @@ -377,8 +377,6 @@ jobs: echo "Set package.json version to $VERSION for preview publish" - name: Build package - env: - BUILD_PREVIEW: '1' run: npm run build - name: Publish to npm diff --git a/README.md b/README.md index 1ec84d801..4af195be8 100644 --- a/README.md +++ b/README.md @@ -167,13 +167,7 @@ clusters of bad outcomes. | `resume online-insights` | Resume a paused online insights config | | `archive insights` | Delete an insights job on the service + clear local history | -### Harness — `[preview]` - -> Harness commands are only available in the preview release of the CLI. Install it with: -> -> ```bash -> npm install -g @aws/agentcore@preview -> ``` +### Harness A harness bundles a runtime, model, tools, skills, memory, and observability into one declarative config. Use it when you want infra without writing agent code. diff --git a/e2e-tests/harness-e2e-helper.ts b/e2e-tests/harness-e2e-helper.ts index 9be2b42aa..65999ca43 100644 --- a/e2e-tests/harness-e2e-helper.ts +++ b/e2e-tests/harness-e2e-helper.ts @@ -9,9 +9,7 @@ import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const hasAws = hasAwsCredentials(); -// Harness features are only available in preview builds (BUILD_PREVIEW=1). -const isPreviewBuild = process.env.BUILD_PREVIEW === '1'; -const baseCanRun = prereqs.npm && prereqs.git && hasAws && isPreviewBuild; +const baseCanRun = prereqs.npm && prereqs.git && hasAws; interface HarnessE2EConfig { modelProvider: 'bedrock' | 'open_ai' | 'gemini'; @@ -33,8 +31,7 @@ export function createHarnessE2ESuite(cfg: HarnessE2EConfig) { if (!canRun) { logger.warn( `tests are skipped due to insufficient conditions. ` + - `npm=${prereqs.npm}, git=${prereqs.git}, hasAws=${hasAws}, ` + - `isPreviewBuild=${isPreviewBuild}, hasRequiredVar=${hasRequiredVar}` + `npm=${prereqs.npm}, git=${prereqs.git}, hasAws=${hasAws}, hasRequiredVar=${hasRequiredVar}` ); } diff --git a/esbuild.config.mjs b/esbuild.config.mjs index f01062b05..a76f194af 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -52,9 +52,6 @@ await esbuild.build({ minify: true, keepNames: true, jsx: 'automatic', - define: { - __PREVIEW__: process.env.BUILD_PREVIEW === '1' ? 'true' : 'false', - }, // Inject require shim for ESM compatibility with CommonJS dependencies banner: { js: `import { createRequire } from 'module'; import { fileURLToPath as __ef } from 'url'; import { dirname as __ed } from 'path'; const require = createRequire(import.meta.url); const __filename = __ef(import.meta.url); const __dirname = __ed(__filename);`, diff --git a/integ-tests/add-remove-harness.test.ts b/integ-tests/add-remove-harness.test.ts index 099f17959..c72a33197 100644 --- a/integ-tests/add-remove-harness.test.ts +++ b/integ-tests/add-remove-harness.test.ts @@ -4,15 +4,11 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -// Harness features are only available in preview builds (BUILD_PREVIEW=1). -// The standard CI build is GA, so skip these tests unless the preview bundle is present. -const isPreviewBuild = process.env.BUILD_PREVIEW === '1'; - async function readHarnessSpec(projectPath: string, harnessName: string) { return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8')); } -describe.skipIf(!isPreviewBuild)('integration: harness add/remove lifecycle', () => { +describe('integration: harness add/remove lifecycle', () => { let project: TestProject; const harnessName = 'TestHarness'; @@ -87,7 +83,7 @@ describe.skipIf(!isPreviewBuild)('integration: harness add/remove lifecycle', () }); }); -describe.skipIf(!isPreviewBuild)('integration: harness configuration options', () => { +describe('integration: harness configuration options', () => { let project: TestProject; beforeAll(async () => { @@ -167,7 +163,7 @@ describe.skipIf(!isPreviewBuild)('integration: harness configuration options', ( }); }); -describe.skipIf(!isPreviewBuild)('integration: harness validation errors', () => { +describe('integration: harness validation errors', () => { let project: TestProject; beforeAll(async () => { @@ -194,7 +190,7 @@ describe.skipIf(!isPreviewBuild)('integration: harness validation errors', () => }); }); -describe.skipIf(!isPreviewBuild)('integration: create project with harness', () => { +describe('integration: create project with harness', () => { let project: TestProject; const harnessName = 'CreateHarness'; diff --git a/integ-tests/create-edge-cases.test.ts b/integ-tests/create-edge-cases.test.ts index d7982eed8..4e65a1157 100644 --- a/integ-tests/create-edge-cases.test.ts +++ b/integ-tests/create-edge-cases.test.ts @@ -34,13 +34,13 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create edge cases', `Error should mention reserved/conflict: ${json.error}` ).toBeTruthy(); + // No agent-path flags -> create defaults to the harness path. telemetry.assertMetricEmitted({ command: 'create', exit_reason: 'failure', error_name: 'ValidationError', error_source: 'user', - agent_environment: 'runtime', - agent_language: 'python', + agent_environment: 'harness', has_agent: 'true', }); }); @@ -141,12 +141,11 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create edge cases', expect(json.success).toBe(true); expect(json.projectPath).toBeTruthy(); + // No agent-path flags -> create defaults to the harness path. telemetry.assertMetricEmitted({ command: 'create', exit_reason: 'success', - agent_environment: 'runtime', - agent_language: 'python', - agent_framework: 'strands', + agent_environment: 'harness', model_provider: 'bedrock', has_agent: 'true', }); @@ -197,9 +196,7 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create edge cases', }); }); -const isPreviewBuild = process.env.BUILD_PREVIEW === '1'; - -describe.skipIf(!isPreviewBuild || !prereqs.npm || !prereqs.git)('integration: create harness project', () => { +describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create harness project', () => { let testDir: string; const telemetry = createTelemetryHelper(); diff --git a/integ-tests/dev-server.test.ts b/integ-tests/dev-server.test.ts index ee9a694da..844aaff91 100644 --- a/integ-tests/dev-server.test.ts +++ b/integ-tests/dev-server.test.ts @@ -175,9 +175,7 @@ describe('integration: dev server', () => { ); }); -const isPreviewBuild = process.env.BUILD_PREVIEW === '1'; - -describe.skipIf(!isPreviewBuild || !hasNpm || !hasGit || !hasUv)('integration: dev with harness-only project', () => { +describe.skipIf(!hasNpm || !hasGit || !hasUv)('integration: dev with harness-only project', () => { const telemetry = createTelemetryHelper(); let projectPath: string; diff --git a/integ-tests/json-output.test.ts b/integ-tests/json-output.test.ts index e4ef5835f..143c453d0 100644 --- a/integ-tests/json-output.test.ts +++ b/integ-tests/json-output.test.ts @@ -40,14 +40,15 @@ describe('JSON output structure', () => { ).toBeTruthy(); }); - it('missing required options returns error JSON', async () => { - // Missing --language, --framework, etc without --no-agent + it('bare create (no agent flags) returns harness success JSON', async () => { + // Without agent-path flags or --no-agent, create defaults to the harness path + // and succeeds rather than erroring on missing agent options. const result = await runCLI(['create', '--name', 'ValidName', '--json'], testDir); - expect(result.exitCode).toBe(1); + expect(result.exitCode, `stdout: ${result.stdout}`).toBe(0); const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - expect(typeof json.error).toBe('string'); + expect(json.success).toBe(true); + expect(typeof json.projectPath).toBe('string'); }); it('invalid framework returns error JSON', async () => { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 39fc1fb47..8cd3becdc 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@aws/agentcore", - "version": "0.19.0", + "version": "0.20.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aws/agentcore", - "version": "0.19.0", + "version": "0.20.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ea35543f9..30bba9f33 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "build:lib": "tsc -p tsconfig.build.json", "build:cli": "node esbuild.config.mjs", "build:assets": "node scripts/copy-assets.mjs", - "build:preview": "BUILD_PREVIEW=1 node esbuild.config.mjs", "build:harness": "BUILD_HARNESS=1 node esbuild.config.mjs", "cli": "npx tsx src/cli/index.ts", "typecheck": "tsc --noEmit --incremental", diff --git a/scripts/bundle.mjs b/scripts/bundle.mjs index 66ec76cc0..08fd78677 100644 --- a/scripts/bundle.mjs +++ b/scripts/bundle.mjs @@ -19,8 +19,7 @@ * AGENTCORE_INSPECTOR_PATH — path to the agent-inspector repo. * Falls back to ../agent-inspector. If found, builds and bundles * the local version instead of the npm-installed one. - * AGENTCORE_TARBALL_OUTPUT — output path (without .tgz) for the GA tarball. - * Preview tarball gets '-preview' appended. Relative to cwd. + * AGENTCORE_TARBALL_OUTPUT — output path (without .tgz) for the tarball. Relative to cwd. * AGENTCORE_TARBALL_VERSION_SUFFIX — version prerelease suffix (e.g. "abc12-def34"). * Defaults to a timestamp if not set. */ @@ -146,14 +145,13 @@ function restoreVersion({ pkgJsonPath, originalVersion }) { /** * If AGENTCORE_TARBALL_OUTPUT is set, return a resolved path using it as base. - * Appends '-preview' suffix for preview builds. Always appends .tgz. + * Always appends .tgz. */ -function resolveTarballPath(tarballPath, { preview = false } = {}) { +function resolveTarballPath(tarballPath) { const envPath = process.env.AGENTCORE_TARBALL_OUTPUT; if (!envPath) return tarballPath; - const suffix = preview ? '-preview' : ''; const base = envPath.replace(/\.tgz$/, ''); - return path.resolve(`${base}${suffix}.tgz`); + return path.resolve(`${base}.tgz`); } // Step 1: Resolve and build CDK constructs @@ -230,64 +228,18 @@ const cliTarballName = `aws-agentcore-${cliVersionInfo.bumpedVersion}.tgz`; const cliTarballPath = path.join(cliRoot, cliTarballName); if (!fs.existsSync(cliTarballPath)) { - console.error(`ERROR: Expected GA tarball at ${cliTarballPath} but not found.`); + console.error(`ERROR: Expected tarball at ${cliTarballPath} but not found.`); process.exit(1); } -const gaTarballPath = resolveTarballPath(cliTarballPath); -if (gaTarballPath !== cliTarballPath) { - fs.mkdirSync(path.dirname(gaTarballPath), { recursive: true }); - fs.renameSync(cliTarballPath, gaTarballPath); - log(`Renamed tarball to: ${gaTarballPath}`); -} -log(`Done! GA Tarball: ${gaTarballPath}`); -log(`Install with: npm install -g ${gaTarballPath}`); -log('When you run agentcore create, the bundled CDK constructs will be installed automatically.'); - -// Step 6: Rebuild CLI with BUILD_PREVIEW=1 -log('Rebuilding CLI with BUILD_PREVIEW=1 for preview tarball...'); -run('npm', ['run', 'build:cli'], { cwd: cliRoot, env: { ...process.env, BUILD_PREVIEW: '1' } }); - -// Step 7: Bump version to preview variant -function bumpPreviewVersion(pkgDir) { - const pkgJsonPath = path.join(pkgDir, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); - const originalVersion = pkg.version; - const baseVersion = originalVersion.split('-')[0]; - pkg.version = `${baseVersion}-preview-${versionSuffix}`; - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n'); - log(`Bumped ${pkg.name} version: ${originalVersion} -> ${pkg.version}`); - return { pkgJsonPath, originalVersion, bumpedVersion: pkg.version }; -} - -const previewVersionInfo = bumpPreviewVersion(cliRoot); - -// Step 8: Pack preview tarball -try { - log('Packing CLI preview tarball...'); - run('npm', ['pack'], { cwd: cliRoot }); -} finally { - restoreVersion(previewVersionInfo); -} - -const previewTarballName = `aws-agentcore-${previewVersionInfo.bumpedVersion}.tgz`; -const previewTarballPath = path.join(cliRoot, previewTarballName); - -if (!fs.existsSync(previewTarballPath)) { - console.error(`ERROR: Expected preview tarball at ${previewTarballPath} but not found.`); - process.exit(1); -} - -const finalPreviewPath = resolveTarballPath(previewTarballPath, { preview: true }); -if (finalPreviewPath !== previewTarballPath) { - fs.mkdirSync(path.dirname(finalPreviewPath), { recursive: true }); - fs.renameSync(previewTarballPath, finalPreviewPath); - log(`Renamed tarball to: ${finalPreviewPath}`); +const finalTarballPath = resolveTarballPath(cliTarballPath); +if (finalTarballPath !== cliTarballPath) { + fs.mkdirSync(path.dirname(finalTarballPath), { recursive: true }); + fs.renameSync(cliTarballPath, finalTarballPath); + log(`Renamed tarball to: ${finalTarballPath}`); } // Final output -log(`GA tarball: ${gaTarballPath}`); -log(`Preview tarball: ${finalPreviewPath}`); -log(`Install GA: npm install -g ${gaTarballPath}`); -log(`Install Preview: npm install -g ${finalPreviewPath}`); +log(`Done! Tarball: ${finalTarballPath}`); +log(`Install with: npm install -g ${finalTarballPath}`); log('When you run agentcore create, the bundled CDK constructs will be installed automatically.'); diff --git a/scripts/run-e2e-local.sh b/scripts/run-e2e-local.sh index 26cd86385..411abde24 100755 --- a/scripts/run-e2e-local.sh +++ b/scripts/run-e2e-local.sh @@ -9,10 +9,6 @@ # Optional env vars: # AWS_REGION — defaults to us-east-1 # CDK_REPO_PATH — local path to CDK constructs repo; if set, builds and uses it as CDK_TARBALL -# BUILD_PREVIEW — set to 1 to build a preview CLI and run the harness e2e tests -# (e2e-tests/harness-*.test.ts). Harness features are gated to preview -# builds; without this they self-skip. The var is read at build time -# (esbuild bakes __PREVIEW__=true) and at test runtime (skip gate). # # Usage: # export E2E_ROLE_ARN=arn:aws:iam:::role/ @@ -20,7 +16,6 @@ # ./scripts/run-e2e-local.sh # runs strands-bedrock.test.ts (CI default) # ./scripts/run-e2e-local.sh --all # runs the full e2e suite # ./scripts/run-e2e-local.sh e2e-tests/foo.test.ts # runs a specific test file -# BUILD_PREVIEW=1 ./scripts/run-e2e-local.sh e2e-tests/harness-bedrock.test.ts # runs harness tests # # Prerequisites: aws CLI, node >=20.19, npm, git, uv, jq diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 154ece89b..6cfcc62d2 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -124,15 +124,9 @@ async function main() { } } - // Harness is preview-gated. The CLI bundle bakes the preview flag at build time and - // forwards it to this child process via AGENTCORE_PREVIEW (see toolkit-lib/wrapper.ts). - // This app is built separately and cannot see that build-time define, so it gates on the - // env var. Absent/anything-but-'1' defaults to off so a stale harnesses[] entry in a - // non-preview build never synthesizes an AWS::BedrockAgentCore::Harness resource. - const previewEnabled = process.env.AGENTCORE_PREVIEW === '1'; - + // Synthesize an AWS::BedrockAgentCore::Harness resource for each harness entry in the spec. const harnessConfigs: HarnessConfig[] = []; - for (const entry of previewEnabled ? (specAny.harnesses ?? []) : []) { + for (const entry of specAny.harnesses ?? []) { const harnessDir = path.resolve(projectRoot, entry.path); const harnessPath = path.resolve(harnessDir, 'harness.json'); try { diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 44fc565ec..b15b3281d 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -79,15 +79,9 @@ async function main() { } } - // Harness is preview-gated. The CLI bundle bakes the preview flag at build time and - // forwards it to this child process via AGENTCORE_PREVIEW (see toolkit-lib/wrapper.ts). - // This app is built separately and cannot see that build-time define, so it gates on the - // env var. Absent/anything-but-'1' defaults to off so a stale harnesses[] entry in a - // non-preview build never synthesizes an AWS::BedrockAgentCore::Harness resource. - const previewEnabled = process.env.AGENTCORE_PREVIEW === '1'; - + // Synthesize an AWS::BedrockAgentCore::Harness resource for each harness entry in the spec. const harnessConfigs: HarnessConfig[] = []; - for (const entry of previewEnabled ? (specAny.harnesses ?? []) : []) { + for (const entry of specAny.harnesses ?? []) { const harnessDir = path.resolve(projectRoot, entry.path); const harnessPath = path.resolve(harnessDir, 'harness.json'); try { diff --git a/src/cli/__tests__/preview-flag.test.ts b/src/cli/__tests__/preview-flag.test.ts deleted file mode 100644 index d54fa3327..000000000 --- a/src/cli/__tests__/preview-flag.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { execSync } from 'child_process'; -import { mkdtempSync, readFileSync, rmSync } from 'fs'; -import { tmpdir } from 'os'; -import { join } from 'path'; -import { afterAll, beforeAll, describe, expect, test } from 'vitest'; - -describe('Preview feature flag', () => { - test('isPreviewEnabled returns false when __PREVIEW__ is false', async () => { - const { isPreviewEnabled } = await import('../feature-flags'); - expect(isPreviewEnabled()).toBe(false); - }); - - describe('dead code elimination', () => { - let tempDir: string; - - beforeAll(() => { - tempDir = mkdtempSync(join(tmpdir(), 'preview-flag-test-')); - }); - - afterAll(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - test('GA build contains no harness code', () => { - const outfile = join(tempDir, 'ga-bundle.mjs'); - execSync(`node esbuild.config.mjs`, { - cwd: process.cwd(), - env: { ...process.env, BUILD_PREVIEW: undefined, ESBUILD_OUTFILE: outfile }, - stdio: 'pipe', - }); - const bundle = readFileSync(outfile, 'utf-8'); - // harness-deployer is a standalone module that should be fully eliminated - expect(bundle).not.toContain('harness-deployer'); - // imperativeManager is only instantiated inside isPreviewEnabled() guards - expect(bundle).not.toContain('imperativeManager'); - }); - - test('Preview build contains harness code', () => { - const outfile = join(tempDir, 'preview-bundle.mjs'); - execSync(`node esbuild.config.mjs`, { - cwd: process.cwd(), - env: { ...process.env, BUILD_PREVIEW: '1', ESBUILD_OUTFILE: outfile }, - stdio: 'pipe', - }); - const bundle = readFileSync(outfile, 'utf-8'); - expect(bundle).toContain('harness'); - }); - }); -}); diff --git a/src/cli/cdk/toolkit-lib/wrapper.ts b/src/cli/cdk/toolkit-lib/wrapper.ts index 0ba5a474e..e3b9158a9 100644 --- a/src/cli/cdk/toolkit-lib/wrapper.ts +++ b/src/cli/cdk/toolkit-lib/wrapper.ts @@ -1,7 +1,6 @@ import { CONFIG_DIR } from '../../../lib'; import { CDK_APP_ENTRY, CDK_PROJECT_DIR } from '../../constants'; import { isChangesetInProgressError } from '../../errors'; -import { isPreviewEnabled } from '../../feature-flags'; import type { CdkToolkitWrapperOptions, DeployOptions, DestroyOptions, DiffOptions, ListOptions } from './types'; import { BaseCredentials, @@ -108,16 +107,12 @@ export class CdkToolkitWrapper { sdkConfig, }); - // The vended CDK app (dist/bin/cdk.js) runs as a child process and cannot see the - // build-time `__PREVIEW__` define baked into this CLI bundle. Pass the preview state - // through the child env so bin/cdk.ts can gate preview-only resources (e.g. harnesses) - // off when preview is disabled. The toolkit overlays this on top of process.env, so - // PATH/AWS_PROFILE are preserved; env must be unconditional so the flag is never - // dropped when no region override is present (absent flag defaults to off). + // The vended CDK app (dist/bin/cdk.js) runs as a child process. Forward the region + // override through the child env when present. The toolkit overlays this on top of + // process.env, so PATH/AWS_PROFILE are preserved. this.cloudAssemblySource = await this.toolkit.fromCdkApp(this.getCdkAppCommand(), { workingDirectory: this.projectDir, env: { - AGENTCORE_PREVIEW: isPreviewEnabled() ? '1' : '0', ...(region && { AWS_REGION: region, AWS_DEFAULT_REGION: region }), }, }); diff --git a/src/cli/cli.ts b/src/cli/cli.ts index f7aa35f3b..30ec862d7 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -35,7 +35,6 @@ import { registerUpdate } from './commands/update'; import { registerValidate } from './commands/validate'; import { registerView } from './commands/view'; import { COMMAND_DESCRIPTIONS, PACKAGE_VERSION } from './constants'; -import { isPreviewEnabled } from './feature-flags'; import { printPostCommandNotices, printTelemetryNotice } from './notices'; import { ALL_PRIMITIVES } from './primitives'; import { TelemetryClientAccessor } from './telemetry'; @@ -122,23 +121,19 @@ export function registerCommands(program: Command) { registerConfig(program); registerDataset(program); registerArchive(program); - // Register export command (preview-only) - if (isPreviewEnabled()) { - registerExport(program); - } + // Register export command + registerExport(program); // Register primitive subcommands (add agent, remove agent, add memory, etc.) for (const primitive of ALL_PRIMITIVES) { primitive.registerCommands(addCmd, removeCmd); } - // Register standalone add/remove subcommands (preview-only) - if (isPreviewEnabled()) { - registerAddTool(addCmd); - registerRemoveTool(removeCmd); - registerAddSkill(addCmd); - registerRemoveSkill(removeCmd); - } + // Register standalone add/remove subcommands + registerAddTool(addCmd); + registerRemoveTool(removeCmd); + registerAddSkill(addCmd); + registerRemoveSkill(removeCmd); } export const main = async (argv: string[]) => { diff --git a/src/cli/commands/create/__tests__/create.test.ts b/src/cli/commands/create/__tests__/create.test.ts index 8506f8d39..5264371d8 100644 --- a/src/cli/commands/create/__tests__/create.test.ts +++ b/src/cli/commands/create/__tests__/create.test.ts @@ -81,12 +81,18 @@ describe('create command', () => { expect(await exists(join(json.projectPath, 'app', name))).toBeTruthy(); }); - it('requires all options without --no-agent', async () => { - const result = await runCLI(['create', '--name', 'Incomplete', '--json'], testDir); + it('creates a harness project when no agent flags are given', async () => { + // Without agent-path flags (--framework/--language/etc.) or --no-agent, create + // defaults to the harness path rather than erroring on incomplete agent options. + // Harness names are limited to 23 chars, so keep this short. + const name = `H${Date.now().toString().slice(-6)}`; + const result = await runCLI(['create', '--name', name, '--json'], testDir); - expect(result.exitCode).toBe(1); + expect(result.exitCode, `stdout: ${result.stdout}`).toBe(0); const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); + expect(json.success).toBe(true); + expect(json.harnessName).toBe(name); + expect(await exists(join(json.projectPath, 'app', name, 'harness.json'))).toBeTruthy(); }); it('validates framework', async () => { diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index 430623744..df3e52537 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -379,10 +379,10 @@ export function getDryRunInfo(options: { const wouldCreate = [ `${projectRoot}/`, `${projectRoot}/agentcore/`, - `${projectRoot}/agentcore/project.json`, + `${projectRoot}/agentcore/agentcore.json`, `${projectRoot}/agentcore/aws-targets.json`, `${projectRoot}/agentcore/.env.local`, - `${projectRoot}/cdk/`, + `${projectRoot}/agentcore/cdk/`, ]; if (language === 'Python') { @@ -405,3 +405,29 @@ export function getDryRunInfo(options: { wouldCreate, }; } + +export function getHarnessDryRunInfo(options: { name: string; cwd: string; projectName?: string }): CreateResult { + const { name, cwd } = options; + const projectName = options.projectName ?? name; + const projectRoot = join(cwd, projectName); + + const wouldCreate = [ + `${projectRoot}/`, + `${projectRoot}/agentcore/`, + `${projectRoot}/agentcore/agentcore.json`, + `${projectRoot}/agentcore/aws-targets.json`, + `${projectRoot}/agentcore/.env.local`, + `${projectRoot}/agentcore/cdk/`, + `${projectRoot}/app/${name}/`, + `${projectRoot}/app/${name}/harness.json`, + `${projectRoot}/app/${name}/system-prompt.md`, + ]; + + return { + success: true, + dryRun: true, + projectPath: projectRoot, + harnessName: name, + wouldCreate, + }; +} diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index a147b4776..3360a65c4 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -11,7 +11,6 @@ import type { import { LIFECYCLE_TIMEOUT_MAX, LIFECYCLE_TIMEOUT_MIN } from '../../../schema'; import { ANSI, COMMAND_DESCRIPTIONS } from '../../constants'; import { getErrorMessage } from '../../errors'; -import { isPreviewEnabled } from '../../feature-flags'; import { ADDITIONAL_PARAMS_JSON_ERROR } from '../../primitives/constants'; import { harnessPrimitive } from '../../primitives/registry'; import { runCliCommand, withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; @@ -30,7 +29,13 @@ import { renderTUI } from '../../tui'; import { requireTTY } from '../../tui/guards'; import { resolveAndValidateFilesystemMounts } from '../shared/filesystem-utils'; import { parseCommaSeparatedList } from '../shared/vpc-utils'; -import { type ProgressCallback, createProject, createProjectWithAgent, getDryRunInfo } from './action'; +import { + type ProgressCallback, + createProject, + createProjectWithAgent, + getDryRunInfo, + getHarnessDryRunInfo, +} from './action'; import { createProjectWithHarness } from './harness-action'; import { normalizeHarnessModelProvider, validateCreateHarnessOptions } from './harness-validate'; import type { CreateOptions } from './types'; @@ -87,10 +92,10 @@ function printCreateSummary( console.log(''); } -/** Flags that trigger the agent/runtime path (preview mode) */ +/** Flags that trigger the agent/runtime path */ const AGENT_PATH_FLAGS = ['framework', 'language', 'build', 'protocol', 'type', 'agentId', 'agentAliasId'] as const; -/** Flags that are harness-only (preview mode) */ +/** Flags that are harness-only */ const HARNESS_ONLY_FLAGS = [ 'modelId', 'apiKeyArn', @@ -100,19 +105,20 @@ const HARNESS_ONLY_FLAGS = [ 'maxTokens', 'timeout', 'truncationStrategy', + 'container', ] as const; -/** Determines if the agent path should be taken based on provided flags (preview mode) */ +/** Determines if the agent path should be taken based on provided flags */ function isAgentPath(options: CreateOptions): boolean { return AGENT_PATH_FLAGS.some(flag => options[flag] !== undefined); } -/** Determines if any harness-only flags are present (preview mode) */ +/** Determines if any harness-only flags are present */ function hasHarnessOnlyFlags(options: CreateOptions): boolean { return HARNESS_ONLY_FLAGS.some(flag => options[flag] !== undefined); } -/** Print completion summary after successful harness create (preview mode) */ +/** Print completion summary after successful harness create */ function printCreateHarnessSummary(projectName: string, harnessName: string): void { const green = '\x1b[32m'; const cyan = '\x1b[36m'; @@ -137,12 +143,49 @@ function printCreateHarnessSummary(projectName: string, harnessName: string): vo console.log(''); } -/** Handle CLI mode for the harness path (preview mode) */ +/** Handle CLI mode for the harness path */ async function handleCreateHarnessCLI(options: CreateOptions): Promise { const cwd = options.outputDir ?? getWorkingDirectory(); const name = options.name ?? options.projectName; const projectName = options.projectName ?? name; + // Handle dry-run mode (no telemetry for dry-run) + if (options.dryRun) { + const validation = validateCreateHarnessOptions( + { + name, + projectName, + modelProvider: options.modelProvider, + modelId: options.modelId, + apiKeyArn: options.apiKeyArn, + networkMode: options.networkMode, + efsAccessPointArn: options.efsAccessPointArn, + efsMountPath: options.efsMountPath, + s3AccessPointArn: options.s3AccessPointArn, + s3MountPath: options.s3MountPath, + }, + cwd + ); + if (!validation.valid) { + if (options.json) { + console.log(JSON.stringify({ success: false, error: validation.error })); + } else { + console.error(validation.error); + } + process.exit(1); + } + const result = getHarnessDryRunInfo({ name: name!, projectName, cwd }); + if (options.json) { + console.log(JSON.stringify(serializeResult(result))); + } else if (result.success) { + console.log('Dry run - would create:'); + for (const path of result.wouldCreate ?? []) { + console.log(` ${path}`); + } + } + process.exit(0); + } + const result = await withCommandRunTelemetry( 'create', { @@ -191,7 +234,7 @@ async function handleCreateHarnessCLI(options: CreateOptions): Promise { }; const modelId = options.modelId ?? defaultModelIds[provider] ?? 'global.anthropic.claude-sonnet-4-6'; - const containerOption = harnessPrimitive!.parseContainerFlag(options.container); + const containerOption = harnessPrimitive.parseContainerFlag(options.container); let additionalParams: Record | undefined; if (options.additionalParams) { @@ -453,25 +496,23 @@ export const registerCreate = (program: Command) => { .option('--dry-run', 'Preview what would be created without making changes [non-interactive]') .option('--json', 'Output as JSON [non-interactive]'); - if (isPreviewEnabled()) { - createCmd - .option('--model-id ', 'Model ID for harness [non-interactive]') - .option('--api-key-arn ', 'API key ARN for non-Bedrock harness providers [non-interactive]') - .option('--api-base ', 'Base URL for the harness model provider API endpoint (lite_llm) [non-interactive]') - .option( - '--additional-params ', - 'Provider-specific harness params as a JSON object (lite_llm) [non-interactive]' - ) - .option('--no-harness-memory', 'Skip auto-creating memory for harness [non-interactive]') - .option('--max-iterations ', 'Max agent loop iterations (harness) [non-interactive]') - .option('--max-tokens ', 'Max tokens per iteration (harness) [non-interactive]') - .option('--timeout ', 'Max execution duration in seconds (harness) [non-interactive]') - .option( - '--truncation-strategy ', - 'Truncation strategy: sliding_window or summarization (harness) [non-interactive]' - ) - .option('--container ', 'Container image URI or Dockerfile path (harness) [non-interactive]'); - } + createCmd + .option('--model-id ', 'Model ID for harness [non-interactive]') + .option('--api-key-arn ', 'API key ARN for non-Bedrock harness providers [non-interactive]') + .option('--api-base ', 'Base URL for the harness model provider API endpoint (lite_llm) [non-interactive]') + .option( + '--additional-params ', + 'Provider-specific harness params as a JSON object (lite_llm) [non-interactive]' + ) + .option('--no-harness-memory', 'Skip auto-creating memory for harness [non-interactive]') + .option('--max-iterations ', 'Max agent loop iterations (harness) [non-interactive]') + .option('--max-tokens ', 'Max tokens per iteration (harness) [non-interactive]') + .option('--timeout ', 'Max execution duration in seconds (harness) [non-interactive]') + .option( + '--truncation-strategy ', + 'Truncation strategy: sliding_window or summarization (harness) [non-interactive]' + ) + .option('--container ', 'Container image URI or Dockerfile path (harness) [non-interactive]'); createCmd.action(async (rawOptions: Record) => { const options = rawOptions as Record & { @@ -513,130 +554,89 @@ export const registerCreate = (program: Command) => { container?: string; }; try { - if (isPreviewEnabled()) { - // Preview mode: fork between harness and agent paths - const hasAnyFlag = Boolean( - options.name ?? - options.projectName ?? - (options.agent === false ? true : null) ?? - options.defaults ?? - options.build ?? - options.language ?? - options.framework ?? - options.modelProvider ?? - options.apiKey ?? - options.memory ?? - options.protocol ?? - options.type ?? - options.agentId ?? - options.agentAliasId ?? - options.region ?? - options.networkMode ?? - options.subnets ?? - options.securityGroups ?? - options.idleTimeout ?? - options.maxLifetime ?? - options.outputDir ?? - options.skipGit ?? - options.skipPythonSetup ?? - options.skipInstall ?? - options.dryRun ?? - options.json ?? - options.modelId ?? - options.apiKeyArn ?? - (options.harnessMemory === false ? true : null) ?? - options.maxIterations ?? - options.maxTokens ?? - options.timeout ?? - options.truncationStrategy - ); - - if (!hasAnyFlag) { - requireTTY(); - await handleCreateTUI(); - return; - } + // Fork between harness and agent paths + const hasAnyFlag = Boolean( + options.name ?? + options.projectName ?? + (options.agent === false ? true : null) ?? + options.defaults ?? + options.build ?? + options.language ?? + options.framework ?? + options.modelProvider ?? + options.apiKey ?? + options.memory ?? + options.protocol ?? + options.type ?? + options.agentId ?? + options.agentAliasId ?? + options.region ?? + options.networkMode ?? + options.subnets ?? + options.securityGroups ?? + options.idleTimeout ?? + options.maxLifetime ?? + options.outputDir ?? + options.skipGit ?? + options.skipPythonSetup ?? + options.skipInstall ?? + options.dryRun ?? + options.json ?? + options.modelId ?? + options.apiKeyArn ?? + (options.harnessMemory === false ? true : null) ?? + options.maxIterations ?? + options.maxTokens ?? + options.timeout ?? + options.truncationStrategy + ); - const opts = options as CreateOptions; - - // Conflict detection: agent-path flags + harness-only flags - if (isAgentPath(opts) && hasHarnessOnlyFlags(opts)) { - const error = - 'Cannot mix agent-path flags (--framework, --language, etc.) with harness-only flags (--model-id, --max-iterations, etc.)'; - if (opts.json) { - console.log(JSON.stringify({ success: false, error })); - } else { - console.error(error); - } - process.exit(1); - } + if (!hasAnyFlag) { + requireTTY(); + await handleCreateTUI(); + return; + } - // --no-agent: bare project (no harness, no agent) - if (opts.agent === false) { - opts.language = opts.language ?? 'Python'; - await handleCreateCLI(opts); - return; - } + const opts = options as CreateOptions; - // Agent path: any agent-specific flag triggers it - if (isAgentPath(opts)) { - if (opts.defaults) { - opts.language = opts.language ?? 'Python'; - opts.build = opts.build ?? 'CodeZip'; - opts.framework = opts.framework ?? 'Strands'; - opts.modelProvider = opts.modelProvider ?? 'Bedrock'; - opts.memory = opts.memory ?? 'none'; - } - opts.language = opts.language ?? 'Python'; - await handleCreateCLI(opts); - return; + // Conflict detection: agent-path flags + harness-only flags + if (isAgentPath(opts) && hasHarnessOnlyFlags(opts)) { + const error = + 'Cannot mix agent-path flags (--framework, --language, etc.) with harness-only flags (--model-id, --max-iterations, etc.)'; + if (opts.json) { + console.log(JSON.stringify({ success: false, error })); + } else { + console.error(error); } + process.exit(1); + } - // Harness path (default in preview mode) - if (!opts.json && !opts.modelProvider && !hasHarnessOnlyFlags(opts)) { - console.log('Creating a harness project (pass --framework to create an agent project instead).'); - } - await handleCreateHarnessCLI(opts); - } else { - // GA mode: original behavior - // Apply defaults if --defaults flag is set - if (options.defaults) { - options.language = options.language ?? 'Python'; - options.build = options.build ?? 'CodeZip'; - options.framework = options.framework ?? 'Strands'; - options.modelProvider = options.modelProvider ?? 'Bedrock'; - options.memory = options.memory ?? 'none'; - } + // --no-agent: bare project (no harness, no agent) + if (opts.agent === false) { + opts.language = opts.language ?? 'Python'; + await handleCreateCLI(opts); + return; + } - // Any flag triggers non-interactive CLI mode - const hasAnyFlag = Boolean( - options.name ?? - options.projectName ?? - (options.agent === false ? true : null) ?? - options.defaults ?? - options.build ?? - options.language ?? - options.framework ?? - options.modelProvider ?? - options.apiKey ?? - options.memory ?? - options.outputDir ?? - options.skipGit ?? - options.skipPythonSetup ?? - options.skipInstall ?? - options.dryRun ?? - options.json - ); - - if (hasAnyFlag) { - // Default language to Python (only supported option) for CLI mode - options.language = options.language ?? 'Python'; - await handleCreateCLI(options as CreateOptions); - } else { - requireTTY(); - await handleCreateTUI(); + // Agent path: any agent-specific flag triggers it + if (isAgentPath(opts)) { + if (opts.defaults) { + opts.language = opts.language ?? 'Python'; + opts.build = opts.build ?? 'CodeZip'; + opts.framework = opts.framework ?? 'Strands'; + opts.modelProvider = opts.modelProvider ?? 'Bedrock'; + opts.memory = opts.memory ?? 'none'; } + opts.language = opts.language ?? 'Python'; + await handleCreateCLI(opts); + return; + } + + // Harness path (default) + if (!opts.json && !opts.modelProvider && !hasHarnessOnlyFlags(opts)) { + console.log('Creating a harness project (pass --framework to create an agent project instead).'); } + await handleCreateHarnessCLI(opts); } catch (error) { render(Error: {getErrorMessage(error)}); process.exit(1); diff --git a/src/cli/commands/create/harness-action.ts b/src/cli/commands/create/harness-action.ts index 68cbfff6e..a6e9db258 100644 --- a/src/cli/commands/create/harness-action.ts +++ b/src/cli/commands/create/harness-action.ts @@ -58,7 +58,7 @@ export async function createProjectWithHarness(options: CreateHarnessProjectOpti try { onProgress?.('Add harness to project', 'start'); - const harnessResult = await harnessPrimitive!.add({ + const harnessResult = await harnessPrimitive.add({ name: options.name, modelProvider: options.modelProvider, modelId: options.modelId, @@ -98,6 +98,7 @@ export async function createProjectWithHarness(options: CreateHarnessProjectOpti return { success: true, projectPath: projectRoot, + harnessName: options.name, warnings: projectResult.warnings, }; } catch (err) { diff --git a/src/cli/commands/create/types.ts b/src/cli/commands/create/types.ts index 67e40b20f..6d0771540 100644 --- a/src/cli/commands/create/types.ts +++ b/src/cli/commands/create/types.ts @@ -47,6 +47,7 @@ export interface CreateOptions extends VpcOptions { export type CreateResult = Result<{ projectPath?: string; agentName?: string; + harnessName?: string; dryRun?: boolean; wouldCreate?: string[]; }> & { warnings?: string[] }; diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 5f4f96e93..b3e349f69 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -22,7 +22,7 @@ import { parseRuntimeEndpointOutputs, } from '../../cloudformation'; import { getErrorMessage } from '../../errors'; -import { isGatedFeaturesEnabled, isPreviewEnabled } from '../../feature-flags'; +import { isGatedFeaturesEnabled } from '../../feature-flags'; import { ExecLogger } from '../../logging'; import { MANAGED_MEMORY_DEPLOY_NOTICE, @@ -192,7 +192,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise undefined); for (const orphan of findOrphanHarnesses(preDeployState)) { const warning = @@ -671,10 +671,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise 0 ? parsePaymentOutputs(outputs, paymentSpecs) : undefined; // Parse harness outputs (harnesses are now part of the CloudFormation stack). - // Preview-gated: when preview is off the vended app never synthesizes a harness - // (see bin/cdk.ts), so there are no outputs to parse — skip entirely to keep the - // gate complete and avoid warning on a harness that was intentionally not deployed. - const harnessNames = isPreviewEnabled() ? (context.projectSpec.harnesses ?? []).map(h => h.name) : []; + const harnessNames = (context.projectSpec.harnesses ?? []).map(h => h.name); const deployedHarnesses = parseHarnessOutputs(outputs, harnessNames); endStep('success'); @@ -878,7 +875,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise 0; + const hasHarnesses = (context.projectSpec.harnesses ?? []).length > 0; const hasInvokable = agentNames.length > 0 || hasHarnesses; const nextSteps = hasInvokable ? [...AGENT_NEXT_STEPS] : [...MEMORY_ONLY_NEXT_STEPS]; const notes: string[] = []; diff --git a/src/cli/commands/dev/browser-mode.ts b/src/cli/commands/dev/browser-mode.ts index 02a3f74cb..b40600e21 100644 --- a/src/cli/commands/dev/browser-mode.ts +++ b/src/cli/commands/dev/browser-mode.ts @@ -1,7 +1,6 @@ import { ConfigIO, findConfigRoot, getWorkingDirectory } from '../../../lib'; import type { AgentCoreProjectSpec } from '../../../schema'; import { ANSI } from '../../constants'; -import { isPreviewEnabled } from '../../feature-flags'; import { isDeploySkippable } from '../../operations/deploy/change-detection'; import { getDevConfig, getDevSupportedAgents, loadDevEnv, loadProjectConfig } from '../../operations/dev'; import { type OtelCollector, startOtelCollector } from '../../operations/dev/otel'; @@ -127,7 +126,7 @@ export async function launchBrowserDev(): Promise { } const hasRuntimes = project.runtimes.length > 0; - const hasHarnesses = isPreviewEnabled() && (project.harnesses ?? []).length > 0; + const hasHarnesses = (project.harnesses ?? []).length > 0; if (!hasRuntimes && !hasHarnesses) { console.error('Error: No agents or harnesses defined in project.'); @@ -164,7 +163,7 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise { const { envVars } = await loadDevEnv(workingDir); const supportedAgents = getDevSupportedAgents(project); - const projectHasHarnesses = isPreviewEnabled() && (project.harnesses ?? []).length > 0; + const projectHasHarnesses = (project.harnesses ?? []).length > 0; if (supportedAgents.length === 0 && !projectHasHarnesses) { console.error('Error: No dev-supported agents found.'); @@ -193,41 +192,39 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise { // Handlers re-resolve on each call so newly deployed memories are picked up. const baseDir = configRoot ?? workingDir; - // Discover deployed harnesses from project config + deployed state (preview mode) + // Discover deployed harnesses from project config + deployed state const harnessInfoList: HarnessInfo[] = []; - if (isPreviewEnabled()) { - try { - const configIO = new ConfigIO({ baseDir }); - if (configIO.configExists('state') && configIO.configExists('awsTargets')) { - const deployedState = await configIO.readDeployedState(); - const awsTargets = await configIO.readAWSDeploymentTargets(); - const targetName = Object.keys(deployedState.targets)[0]; - if (targetName) { - const targetState = deployedState.targets[targetName]; - const targetConfig = awsTargets.find(t => t.name === targetName); - if (targetConfig) { - for (const harness of project.harnesses ?? []) { - const state = targetState?.resources?.harnesses?.[harness.name]; - if (state) { - harnessInfoList.push({ - name: harness.name, - harnessArn: state.harnessArn, - region: targetConfig.region, - }); - } - } - if (harnessInfoList.length > 0) { - onLog( - 'info', - `Found ${harnessInfoList.length} deployed harness(es): ${harnessInfoList.map(h => h.name).join(', ')}` - ); + try { + const configIO = new ConfigIO({ baseDir }); + if (configIO.configExists('state') && configIO.configExists('awsTargets')) { + const deployedState = await configIO.readDeployedState(); + const awsTargets = await configIO.readAWSDeploymentTargets(); + const targetName = Object.keys(deployedState.targets)[0]; + if (targetName) { + const targetState = deployedState.targets[targetName]; + const targetConfig = awsTargets.find(t => t.name === targetName); + if (targetConfig) { + for (const harness of project.harnesses ?? []) { + const state = targetState?.resources?.harnesses?.[harness.name]; + if (state) { + harnessInfoList.push({ + name: harness.name, + harnessArn: state.harnessArn, + region: targetConfig.region, + }); } } + if (harnessInfoList.length > 0) { + onLog( + 'info', + `Found ${harnessInfoList.length} deployed harness(es): ${harnessInfoList.map(h => h.name).join(', ')}` + ); + } } } - } catch { - // Harness discovery is best-effort — local dev works without it } + } catch { + // Harness discovery is best-effort — local dev works without it } await runWebUI({ diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index b0dde4c66..e680899d7 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -10,7 +10,6 @@ import { failureResult } from '../../../lib/result.js'; import { COMMAND_DESCRIPTIONS } from '../../constants'; import { getErrorMessage } from '../../errors'; import { detectContainerRuntime } from '../../external-requirements'; -import { isPreviewEnabled } from '../../feature-flags'; import { ExecLogger } from '../../logging'; import { isDeploySkippable } from '../../operations/deploy/change-detection'; import { @@ -323,7 +322,7 @@ export const registerDev = (program: Command) => { } const hasRuntimes = project.runtimes && project.runtimes.length > 0; - const hasHarnesses = isPreviewEnabled() && project.harnesses && project.harnesses.length > 0; + const hasHarnesses = project.harnesses && project.harnesses.length > 0; if (!hasRuntimes && !hasHarnesses) { throw new ValidationError( @@ -360,8 +359,8 @@ export const registerDev = (program: Command) => { // --logs: non-interactive server mode if (opts.logs) { - // Preview: harness-only projects need deploy then print invoke instructions - if (isPreviewEnabled() && supportedAgents.length === 0 && hasHarnesses) { + // Harness-only projects need deploy then print invoke instructions + if (supportedAgents.length === 0 && hasHarnesses) { recorder.set({ agent_environment: 'harness' as const }); if (!opts.skipDeploy) { await runCliDeploy(); @@ -408,8 +407,8 @@ export const registerDev = (program: Command) => { ); } - // Deploy resources before starting dev server (preview mode with harnesses) - if (isPreviewEnabled() && !opts.skipDeploy && hasHarnesses) { + // Deploy resources before starting dev server (harness projects) + if (!opts.skipDeploy && hasHarnesses) { await runCliDeploy(); } @@ -510,57 +509,41 @@ export const registerDev = (program: Command) => { }; } - // Preview browser mode: check if deploy is needed BEFORE entering the TUI. + // Browser mode: check if deploy is needed BEFORE entering the TUI. // This avoids an alt-screen flash when there's nothing to deploy. // The TUI (launchTuiDevScreenWithPicker) is only used to show deploy progress. - if (isPreviewEnabled()) { - const isHarnessOnly = hasHarnesses && supportedAgents.length === 0; - let needsTuiDeploy = false; + const isHarnessOnly = hasHarnesses && supportedAgents.length === 0; + let needsTuiDeploy = false; - if (isHarnessOnly && !opts.skipDeploy) { - needsTuiDeploy = !(await isDeploySkippable()); - } + if (isHarnessOnly && !opts.skipDeploy) { + needsTuiDeploy = !(await isDeploySkippable()); + } - if (needsTuiDeploy) { - // Deploy is needed — show TUI deploy progress, then launch browser - const pickerResult = await launchTuiDevScreenWithPicker(workingDir, { - skipDeploy: opts.skipDeploy, - }); + if (needsTuiDeploy) { + // Deploy is needed — show TUI deploy progress, then launch browser + const pickerResult = await launchTuiDevScreenWithPicker(workingDir, { + skipDeploy: opts.skipDeploy, + }); - if (pickerResult != null) { - recorder.set({ ui_mode: 'browser' as const }); - return { - success: true as const, - blockingPromise: runBrowserMode({ - workingDir, - project, - port, - agentName: pickerResult.agentName, - harnessName: pickerResult.harnessName, - otelEnvVars, - collector, - }), - }; - } - return { success: true as const, blockingPromise: Promise.resolve() }; + if (pickerResult != null) { + recorder.set({ ui_mode: 'browser' as const }); + return { + success: true as const, + blockingPromise: runBrowserMode({ + workingDir, + project, + port, + agentName: pickerResult.agentName, + harnessName: pickerResult.harnessName, + otelEnvVars, + collector, + }), + }; } - - // No deploy needed — skip TUI entirely, go straight to browser - recorder.set({ ui_mode: 'browser' as const }); - return { - success: true as const, - blockingPromise: runBrowserMode({ - workingDir, - project, - port, - agentName: opts.runtime, - otelEnvVars, - collector, - }), - }; + return { success: true as const, blockingPromise: Promise.resolve() }; } - // Default: browser mode (blocks forever) + // No deploy needed — skip TUI entirely, go straight to browser recorder.set({ ui_mode: 'browser' as const }); return { success: true as const, diff --git a/src/cli/commands/invoke/__tests__/action-gateway.test.ts b/src/cli/commands/invoke/__tests__/action-gateway.test.ts index 4cd4f2dab..93a895ce6 100644 --- a/src/cli/commands/invoke/__tests__/action-gateway.test.ts +++ b/src/cli/commands/invoke/__tests__/action-gateway.test.ts @@ -14,10 +14,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // branch actually calls it rather than returning the body raw. // --------------------------------------------------------------------------- -vi.mock('../../../feature-flags', () => ({ - isPreviewEnabled: () => false, -})); - vi.mock('../../../logging', () => ({ InvokeLogger: class { logFilePath = '/tmp/fake.log'; diff --git a/src/cli/commands/invoke/__tests__/action-payments.test.ts b/src/cli/commands/invoke/__tests__/action-payments.test.ts index ca63938c4..724955766 100644 --- a/src/cli/commands/invoke/__tests__/action-payments.test.ts +++ b/src/cli/commands/invoke/__tests__/action-payments.test.ts @@ -26,10 +26,6 @@ vi.mock('../resolve', () => ({ resolveInvokeTarget: (...args: unknown[]) => mockResolveInvokeTarget(...args), })); -vi.mock('../../../feature-flags', () => ({ - isPreviewEnabled: () => false, -})); - // Mock the entire aws barrel. Re-export the real DEFAULT_RUNTIME_USER_ID constant // so the production fallback value stays in sync with the source of truth. vi.mock('../../../aws', () => ({ diff --git a/src/cli/commands/invoke/__tests__/command.test.ts b/src/cli/commands/invoke/__tests__/command.test.ts index cf6c8d7ed..d24ec0978 100644 --- a/src/cli/commands/invoke/__tests__/command.test.ts +++ b/src/cli/commands/invoke/__tests__/command.test.ts @@ -1,5 +1,4 @@ // Tests for invoke CLI mode — exitCode propagation and flag validation -import { isPreviewEnabled } from '../../../feature-flags'; import { handleInvoke } from '../action.js'; import { registerInvoke } from '../command.js'; import { resolvePrompt } from '../resolve-prompt.js'; @@ -49,10 +48,6 @@ vi.mock('ink', () => ({ vi.mock('react', async importOriginal => ({ ...(await importOriginal()) })); -vi.mock('../../../feature-flags', () => ({ - isPreviewEnabled: vi.fn().mockReturnValue(false), -})); - vi.mock('../validate.js', () => ({ validateInvokeOptions: vi.fn().mockReturnValue({ valid: true }), })); @@ -130,8 +125,6 @@ describe('invoke CLI mode — exitCode propagation', () => { }); it('emits a JSON error envelope (not console.error) for bad --additional-params in --json mode', async () => { - // --additional-params is preview-gated; enable preview for this case. - vi.mocked(isPreviewEnabled).mockReturnValue(true); const logged: string[] = []; const erred: string[] = []; vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => logged.push(args.join(' '))); diff --git a/src/cli/commands/invoke/__tests__/utils.test.ts b/src/cli/commands/invoke/__tests__/utils.test.ts index 7187c7409..b0f87bb9f 100644 --- a/src/cli/commands/invoke/__tests__/utils.test.ts +++ b/src/cli/commands/invoke/__tests__/utils.test.ts @@ -2,22 +2,8 @@ import { computeInvokeAttrs } from '../utils'; import { describe, expect, it } from 'vitest'; describe('computeInvokeAttrs', () => { - it('returns runtime when preview is false regardless of harness flags', () => { + it('returns harness when harnessName is set', () => { const attrs = computeInvokeAttrs({ - preview: false, - harnessName: 'my-harness', - harnessCount: 1, - runtimeCount: 0, - stream: true, - hasSessionId: false, - }); - expect(attrs.agent_environment).toBe('runtime'); - expect(attrs.agent_protocol).toBe('http'); - }); - - it('returns harness when harnessName is set and preview is true', () => { - const attrs = computeInvokeAttrs({ - preview: true, harnessName: 'my-harness', harnessCount: 1, runtimeCount: 1, @@ -29,9 +15,8 @@ describe('computeInvokeAttrs', () => { expect(attrs.has_session_id).toBe(true); }); - it('returns harness when harnessArn is set and preview is true', () => { + it('returns harness when harnessArn is set', () => { const attrs = computeInvokeAttrs({ - preview: true, harnessArn: 'arn:aws:bedrock:us-east-1:123:harness/h1', harnessCount: 0, runtimeCount: 1, @@ -44,7 +29,6 @@ describe('computeInvokeAttrs', () => { it('returns harness when project has only harnesses', () => { const attrs = computeInvokeAttrs({ - preview: true, harnessCount: 2, runtimeCount: 0, stream: false, @@ -55,7 +39,6 @@ describe('computeInvokeAttrs', () => { it('returns runtime for mixed project without explicit harness flag', () => { const attrs = computeInvokeAttrs({ - preview: true, harnessCount: 1, runtimeCount: 1, stream: false, @@ -67,7 +50,6 @@ describe('computeInvokeAttrs', () => { it('passes auth_type based on bearerToken', () => { const withToken = computeInvokeAttrs({ - preview: false, harnessCount: 0, runtimeCount: 1, stream: false, @@ -77,7 +59,6 @@ describe('computeInvokeAttrs', () => { expect(withToken.auth_type).toBe('bearer_token'); const withoutToken = computeInvokeAttrs({ - preview: false, harnessCount: 0, runtimeCount: 1, stream: false, @@ -88,7 +69,6 @@ describe('computeInvokeAttrs', () => { it('uses provided agentProtocol for runtime', () => { const attrs = computeInvokeAttrs({ - preview: false, harnessCount: 0, runtimeCount: 1, stream: false, diff --git a/src/cli/commands/invoke/action.ts b/src/cli/commands/invoke/action.ts index 5b867eac5..7840b50b0 100644 --- a/src/cli/commands/invoke/action.ts +++ b/src/cli/commands/invoke/action.ts @@ -18,7 +18,6 @@ import { import { invokeHarness } from '../../aws/agentcore-harness'; import { dnsSuffix } from '../../aws/partition'; import { ANSI } from '../../constants'; -import { isPreviewEnabled } from '../../feature-flags'; import { InvokeLogger } from '../../logging'; import { formatMcpToolList } from '../../operations/dev/utils'; import { canFetchHarnessToken, fetchHarnessToken } from '../../operations/fetch-access'; @@ -239,51 +238,49 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption }; } - // Preview: route to harness before runtime resolution - if (isPreviewEnabled()) { - const harnessEntries = project.harnesses ?? []; - const isHarnessInvoke = options.harnessName != null || (harnessEntries.length > 0 && project.runtimes.length === 0); + // Route to harness before runtime resolution + const harnessEntries = project.harnesses ?? []; + const isHarnessInvoke = options.harnessName != null || (harnessEntries.length > 0 && project.runtimes.length === 0); - if (isHarnessInvoke) { - const targetNames = Object.keys(deployedState.targets); - if (targetNames.length === 0) { - return { - success: false, - error: new ResourceNotFoundError('No deployed targets found. Run `agentcore deploy` first.'), - }; - } - const selectedTarget = options.targetName ?? targetNames[0]!; - if (options.targetName && !targetNames.includes(options.targetName)) { - return { - success: false, - error: new ResourceNotFoundError( - `Target '${options.targetName}' not found. Available: ${targetNames.join(', ')}` - ), - }; - } - const harnessTargetState = deployedState.targets[selectedTarget]; - const harnessTargetConfig = awsTargets.find(t => t.name === selectedTarget); - if (!harnessTargetConfig) { - return { - success: false, - error: new ResourceNotFoundError(`Target config '${selectedTarget}' not found in aws-targets`), - }; - } - return handleHarnessInvoke(project, harnessTargetState, harnessTargetConfig, selectedTarget, options); + if (isHarnessInvoke) { + const targetNames = Object.keys(deployedState.targets); + if (targetNames.length === 0) { + return { + success: false, + error: new ResourceNotFoundError('No deployed targets found. Run `agentcore deploy` first.'), + }; } - - if (harnessEntries.length > 0 && project.runtimes.length > 0 && !options.agentName) { - const runtimeNames = project.runtimes.map(a => a.name); - const harnessNames = harnessEntries.map(h => h.name); + const selectedTarget = options.targetName ?? targetNames[0]!; + if (options.targetName && !targetNames.includes(options.targetName)) { return { success: false, - error: new ValidationError( - `Project has both runtimes and harnesses. Specify one:\n` + - ` --runtime: ${runtimeNames.join(', ')}\n` + - ` --harness: ${harnessNames.join(', ')}` + error: new ResourceNotFoundError( + `Target '${options.targetName}' not found. Available: ${targetNames.join(', ')}` ), }; } + const harnessTargetState = deployedState.targets[selectedTarget]; + const harnessTargetConfig = awsTargets.find(t => t.name === selectedTarget); + if (!harnessTargetConfig) { + return { + success: false, + error: new ResourceNotFoundError(`Target config '${selectedTarget}' not found in aws-targets`), + }; + } + return handleHarnessInvoke(project, harnessTargetState, harnessTargetConfig, selectedTarget, options); + } + + if (harnessEntries.length > 0 && project.runtimes.length > 0 && !options.agentName) { + const runtimeNames = project.runtimes.map(a => a.name); + const harnessNames = harnessEntries.map(h => h.name); + return { + success: false, + error: new ValidationError( + `Project has both runtimes and harnesses. Specify one:\n` + + ` --runtime: ${runtimeNames.join(', ')}\n` + + ` --harness: ${harnessNames.join(', ')}` + ), + }; } const resolved = await resolveInvokeTarget({ diff --git a/src/cli/commands/invoke/command.tsx b/src/cli/commands/invoke/command.tsx index 42c9f697f..574aa1357 100644 --- a/src/cli/commands/invoke/command.tsx +++ b/src/cli/commands/invoke/command.tsx @@ -1,7 +1,6 @@ import { ValidationError, serializeResult } from '../../../lib'; import { COMMAND_DESCRIPTIONS } from '../../constants'; import { getErrorMessage } from '../../errors'; -import { isPreviewEnabled } from '../../feature-flags'; import { ADDITIONAL_PARAMS_JSON_ERROR } from '../../primitives/constants'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { renderTUI } from '../../tui'; @@ -40,8 +39,8 @@ async function handleInvokeCLI(options: InvokeOptions, preloadedContext?: Invoke let spinner: NodeJS.Timeout | undefined; try { - // Preview: direct harness invoke by ARN (no project required) - if (isPreviewEnabled() && options.harnessArn) { + // Direct harness invoke by ARN (no project required) + if (options.harnessArn) { const region = options.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION; if (!region) { const msg = '--region is required with --harness-arn (or set AWS_REGION)'; @@ -60,9 +59,8 @@ async function handleInvokeCLI(options: InvokeOptions, preloadedContext?: Invoke // Show spinner for non-streaming, non-json, non-exec invocations // Harness invoke always streams directly to stdout, so skip spinner for harness const isHarness = - isPreviewEnabled() && - (options.harnessName != null || - ((context.project.harnesses ?? []).length > 0 && context.project.runtimes.length === 0)); + options.harnessName != null || + ((context.project.harnesses ?? []).length > 0 && context.project.runtimes.length === 0); if (!options.stream && !options.json && !options.exec && !isHarness) { spinner = startSpinner('Invoking agent...'); } @@ -169,40 +167,38 @@ export const registerInvoke = (program: Command) => { 'End-user identity (wallet owner) for payments; scopes the instrument/session/budget. Defaults to --user-id when omitted.' ); - if (isPreviewEnabled()) { - invokeCmd - .option('--harness ', 'Select specific harness to invoke [non-interactive]') - .option('--harness-arn ', 'Invoke a harness by ARN (no project required) [non-interactive]') - .option('--region ', 'AWS region (required with --harness-arn when no project) [non-interactive]') - .option('--verbose', 'Print verbose streaming JSON events (harness only) [non-interactive]') - .option('--model-id ', 'Override model for this invocation (harness only) [non-interactive]') - .option( - '--model-provider ', - 'Override model provider: bedrock, open_ai, gemini, lite_llm (harness only) [non-interactive]' - ) - .option('--api-key-arn ', 'Override API key ARN for open_ai/gemini (harness only) [non-interactive]') - .option('--api-base ', 'Override LiteLLM API base URL (harness only, lite_llm) [non-interactive]') - .option( - '--additional-params ', - 'Override LiteLLM additional params as a JSON object (harness only, lite_llm) [non-interactive]' - ) - .option('--tools ', 'Override tools, comma-separated (harness only) [non-interactive]') - .option('--max-iterations ', 'Override max iterations (harness only) [non-interactive]', parseInt) - .option('--max-tokens ', 'Override max tokens (harness only) [non-interactive]', parseInt) - .option('--harness-timeout ', 'Override timeout seconds (harness only) [non-interactive]', parseInt) - .option( - '--skills ', - 'Skills override, comma-separated (path, s3://uri, or https://git-url). Git auth not supported here — configure via agentcore add skill [non-interactive]' - ) - .option('--system-prompt ', 'Override system prompt (harness only) [non-interactive]') - .option('--allowed-tools ', 'Override allowed tools, comma-separated (harness only) [non-interactive]') - .option('--actor-id ', 'Override memory actor ID (harness only) [non-interactive]'); - } + invokeCmd + .option('--harness ', 'Select specific harness to invoke [non-interactive]') + .option('--harness-arn ', 'Invoke a harness by ARN (no project required) [non-interactive]') + .option('--region ', 'AWS region (required with --harness-arn when no project) [non-interactive]') + .option('--verbose', 'Print verbose streaming JSON events (harness only) [non-interactive]') + .option('--model-id ', 'Override model for this invocation (harness only) [non-interactive]') + .option( + '--model-provider ', + 'Override model provider: bedrock, open_ai, gemini, lite_llm (harness only) [non-interactive]' + ) + .option('--api-key-arn ', 'Override API key ARN for open_ai/gemini (harness only) [non-interactive]') + .option('--api-base ', 'Override LiteLLM API base URL (harness only, lite_llm) [non-interactive]') + .option( + '--additional-params ', + 'Override LiteLLM additional params as a JSON object (harness only, lite_llm) [non-interactive]' + ) + .option('--tools ', 'Override tools, comma-separated (harness only) [non-interactive]') + .option('--max-iterations ', 'Override max iterations (harness only) [non-interactive]', parseInt) + .option('--max-tokens ', 'Override max tokens (harness only) [non-interactive]', parseInt) + .option('--harness-timeout ', 'Override timeout seconds (harness only) [non-interactive]', parseInt) + .option( + '--skills ', + 'Skills override, comma-separated (path, s3://uri, or https://git-url). Git auth not supported here — configure via agentcore add skill [non-interactive]' + ) + .option('--system-prompt ', 'Override system prompt (harness only) [non-interactive]') + .option('--allowed-tools ', 'Override allowed tools, comma-separated (harness only) [non-interactive]') + .option('--actor-id ', 'Override memory actor ID (harness only) [non-interactive]'); // Group the long flag list into labelled sections (mirrors `add ab-test`). // Core flags (prompt/prompt-file/runtime/target/session-id/user-id) stay in the // default "Options:" block; everything else is hidden there and re-listed under a - // section heading below. Preview/harness sections are only emitted when registered. + // section heading below. const hiddenFromDefaultHelp = new Set([ // Payments '--payment-user-id', @@ -265,10 +261,9 @@ MCP & Advanced [non-interactive] ` ); - if (isPreviewEnabled()) { - invokeCmd.addHelpText( - 'after', - ` + invokeCmd.addHelpText( + 'after', + ` Harness [non-interactive] --harness Select specific harness to invoke --harness-arn Invoke a harness by ARN (no project required) @@ -288,8 +283,7 @@ Model & Runtime Overrides (harness only) [non-interactive] --max-tokens Override max tokens --harness-timeout Override timeout seconds ` - ); - } + ); invokeCmd.action( async ( @@ -335,8 +329,8 @@ Model & Runtime Overrides (harness only) [non-interactive] } ) => { try { - // Skip requireProject when --harness-arn provided (preview mode) - if (!(isPreviewEnabled() && cliOptions.harnessArn)) { + // Skip requireProject when --harness-arn provided + if (!cliOptions.harnessArn) { requireProject(); } @@ -387,7 +381,6 @@ Model & Runtime Overrides (harness only) [non-interactive] const result = await withCommandRunTelemetry( 'invoke', computeInvokeAttrs({ - preview: isPreviewEnabled(), harnessName: cliOptions.harness, harnessArn: cliOptions.harnessArn, harnessCount: invokeContext?.project.harnesses?.length ?? 0, diff --git a/src/cli/commands/invoke/utils.ts b/src/cli/commands/invoke/utils.ts index beda64134..240681885 100644 --- a/src/cli/commands/invoke/utils.ts +++ b/src/cli/commands/invoke/utils.ts @@ -12,7 +12,6 @@ function isHarnessInvoke(options: { } export function computeInvokeAttrs(options: { - preview: boolean; harnessName?: string; harnessArn?: string; harnessCount: number; @@ -22,7 +21,7 @@ export function computeInvokeAttrs(options: { bearerToken?: string; agentProtocol?: string; }) { - const isHarness = options.preview && isHarnessInvoke(options); + const isHarness = isHarnessInvoke(options); return { agent_environment: standardize(AgentEnvironment, isHarness ? 'harness' : 'runtime'), has_stream: options.stream, diff --git a/src/cli/commands/status/__tests__/action.test.ts b/src/cli/commands/status/__tests__/action.test.ts index 367048370..4cb7ac66a 100644 --- a/src/cli/commands/status/__tests__/action.test.ts +++ b/src/cli/commands/status/__tests__/action.test.ts @@ -25,10 +25,8 @@ vi.mock('../../../aws/bedrock-agent', () => ({ getLatestIngestionJob: (...args: unknown[]) => mockGetLatestIngestionJob(...args), })); -const mockIsPreviewEnabled = vi.fn(() => true); const mockIsGatedFeaturesEnabled = vi.fn(() => true); vi.mock('../../../feature-flags', () => ({ - isPreviewEnabled: () => mockIsPreviewEnabled(), isGatedFeaturesEnabled: () => mockIsGatedFeaturesEnabled(), })); @@ -485,31 +483,6 @@ describe('computeResourceStatuses', () => { expect(harnessEntry!.identifier).toBe('arn:aws:bedrock:us-east-1:123456789:harness/h-456'); }); - it('does not include harnesses when preview is disabled', () => { - mockIsPreviewEnabled.mockReturnValueOnce(false); - - const project = { - ...baseProject, - harnesses: [{ name: 'my-harness', path: 'harnesses/my-harness' }], - } as unknown as AgentCoreProjectSpec; - - const resources: DeployedResourceState = { - harnesses: { - 'my-harness': { - harnessId: 'h-123', - harnessArn: 'arn:aws:bedrock:us-east-1:123456789:harness/h-123', - roleArn: 'arn:aws:iam::123456789:role/test', - status: 'ACTIVE', - }, - }, - }; - - const result = computeResourceStatuses(project, resources); - const harnessEntries = result.filter(r => r.resourceType === 'harness'); - - expect(harnessEntries).toHaveLength(0); - }); - it('renders the config version (v{N}) on a deployed harness when gated features are enabled', () => { const project = { ...baseProject, diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index d2337cb1f..c62920c37 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -6,7 +6,7 @@ import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-con import { getPaymentManager } from '../../aws/agentcore-payments'; import { getKnowledgeBase, getLatestIngestionJob } from '../../aws/bedrock-agent'; import { getErrorMessage } from '../../errors'; -import { isGatedFeaturesEnabled, isPreviewEnabled } from '../../feature-flags'; +import { isGatedFeaturesEnabled } from '../../feature-flags'; import { ExecLogger } from '../../logging'; import type { ResourceDeploymentState } from './constants'; import { buildRuntimeInvocationUrl } from './constants'; @@ -310,15 +310,13 @@ export function computeResourceStatuses( getParentName: item => item.agentName, }); - const harnesses = isPreviewEnabled() - ? diffResourceSet({ - resourceType: 'harness', - localItems: project.harnesses ?? [], - deployedRecord: resources?.harnesses ?? {}, - getIdentifier: deployed => deployed.harnessArn, - getLocalDetail: () => undefined, - }) - : []; + const harnesses = diffResourceSet({ + resourceType: 'harness', + localItems: project.harnesses ?? [], + deployedRecord: resources?.harnesses ?? {}, + getIdentifier: deployed => deployed.harnessArn, + getLocalDetail: () => undefined, + }); // Config version (gated): the harness Version is service-incremented and only known from deployed // state, so enrich each entry's detail post-pass rather than via getLocalDetail (local spec has none). diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index 1d93e8162..dda4e226f 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -1,7 +1,6 @@ import { ValidationError, serializeResult } from '../../../lib'; import { COMMAND_DESCRIPTIONS } from '../../constants'; import { getErrorMessage } from '../../errors'; -import { isPreviewEnabled } from '../../feature-flags'; import { getDatasetStatus } from '../../operations/dataset'; import type { DatasetStatusResult } from '../../operations/dataset'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; @@ -27,7 +26,7 @@ const VALID_RESOURCE_TYPES = [ 'config-bundle', 'dataset', 'knowledge-base', - ...(isPreviewEnabled() ? (['harness'] as const) : []), + 'harness', ] as const; const VALID_STATES = ['deployed', 'local-only', 'pending-removal'] as const; diff --git a/src/cli/feature-flags.ts b/src/cli/feature-flags.ts index 03d08854d..9ae63f29b 100644 --- a/src/cli/feature-flags.ts +++ b/src/cli/feature-flags.ts @@ -1,5 +1 @@ -declare const __PREVIEW__: boolean; - -export const isPreviewEnabled = (): boolean => __PREVIEW__; - export const isGatedFeaturesEnabled = (): boolean => process.env.ENABLE_GATED_FEATURES === '1'; diff --git a/src/cli/index.ts b/src/cli/index.ts index 33d64012b..9006973ee 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,9 +2,6 @@ import { main } from './cli.js'; import { getErrorMessage } from './errors.js'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -(globalThis as any).__PREVIEW__ ??= process.env.BUILD_PREVIEW === '1'; - // Global safety net — prevent raw stack traces from reaching the user process.on('uncaughtException', err => { console.error(`Error: ${getErrorMessage(err)}`); diff --git a/src/cli/primitives/registry.ts b/src/cli/primitives/registry.ts index 4af040421..d578c4b6f 100644 --- a/src/cli/primitives/registry.ts +++ b/src/cli/primitives/registry.ts @@ -1,4 +1,3 @@ -import { isPreviewEnabled } from '../feature-flags'; import { AgentPrimitive } from './AgentPrimitive'; import type { BasePrimitive } from './BasePrimitive'; import { ConfigBundlePrimitive } from './ConfigBundlePrimitive'; @@ -23,7 +22,7 @@ import type { RemovableResource } from './types'; * Singleton instances of all primitives. */ export const agentPrimitive = new AgentPrimitive(); -export const harnessPrimitive = isPreviewEnabled() ? new HarnessPrimitive() : undefined; +export const harnessPrimitive = new HarnessPrimitive(); export const memoryPrimitive = new MemoryPrimitive(); export const datasetPrimitive = new DatasetPrimitive(); export const credentialPrimitive = new CredentialPrimitive(); @@ -45,7 +44,7 @@ export const paymentConnectorPrimitive = new PaymentConnectorPrimitive(); */ export const ALL_PRIMITIVES: BasePrimitive[] = [ agentPrimitive, - ...(harnessPrimitive ? [harnessPrimitive] : []), + harnessPrimitive, memoryPrimitive, datasetPrimitive, credentialPrimitive, diff --git a/src/cli/tui/__tests__/app-command-coverage.test.ts b/src/cli/tui/__tests__/app-command-coverage.test.ts index b19975d83..8eaa1eeea 100644 --- a/src/cli/tui/__tests__/app-command-coverage.test.ts +++ b/src/cli/tui/__tests__/app-command-coverage.test.ts @@ -21,6 +21,7 @@ const ROUTED_COMMANDS = new Set([ 'create', 'add', 'remove', + 'export', 'run', 'evals', 'fetch', diff --git a/src/cli/tui/hooks/useRemove.ts b/src/cli/tui/hooks/useRemove.ts index b8177c632..48eaca8b8 100644 --- a/src/cli/tui/hooks/useRemove.ts +++ b/src/cli/tui/hooks/useRemove.ts @@ -125,7 +125,7 @@ export function useRemovableAgents() { export function useRemovableHarnesses() { const { items: harnesses, ...rest } = useRemovableResources(() => - harnessPrimitive ? harnessPrimitive.getRemovable().then(r => r.map(h => h.name)) : Promise.resolve([]) + harnessPrimitive.getRemovable().then(r => r.map(h => h.name)) ); return { harnesses, ...rest }; } @@ -244,7 +244,7 @@ export function useRemovalPreview() { [loadPreview] ); const loadHarnessPreview = useCallback( - (name: string) => loadPreview(n => harnessPrimitive!.previewRemove(n), name), + (name: string) => loadPreview(n => harnessPrimitive.previewRemove(n), name), [loadPreview] ); const loadGatewayPreview = useCallback( @@ -342,7 +342,7 @@ export function useRemoveAgent() { export function useRemoveHarness() { return useRemoveResource( - (name: string) => harnessPrimitive!.remove(name), + (name: string) => harnessPrimitive.remove(name), 'harness', name => name ); diff --git a/src/cli/tui/screens/add/AddScreen.tsx b/src/cli/tui/screens/add/AddScreen.tsx index aca3e59a8..5da5bda5b 100644 --- a/src/cli/tui/screens/add/AddScreen.tsx +++ b/src/cli/tui/screens/add/AddScreen.tsx @@ -1,4 +1,4 @@ -import { isGatedFeaturesEnabled, isPreviewEnabled } from '../../../feature-flags'; +import { isGatedFeaturesEnabled } from '../../../feature-flags'; import type { SelectableItem } from '../../components'; import { SelectScreen } from '../../components'; @@ -45,9 +45,7 @@ const BASE_ADD_RESOURCES: { id: AddResourceType; title: string; description: str ]; const ADD_RESOURCES: { id: AddResourceType; title: string; description: string }[] = [ - ...(isPreviewEnabled() - ? [{ id: 'harness' as const, title: 'Harness', description: 'Managed config-based agent loop, no code required' }] - : []), + { id: 'harness', title: 'Harness', description: 'Managed config-based agent loop, no code required' }, ...BASE_ADD_RESOURCES, ]; diff --git a/src/cli/tui/screens/create/CreateScreen.tsx b/src/cli/tui/screens/create/CreateScreen.tsx index d0fe55294..9b9e7907f 100644 --- a/src/cli/tui/screens/create/CreateScreen.tsx +++ b/src/cli/tui/screens/create/CreateScreen.tsx @@ -1,7 +1,6 @@ import { DEFAULT_MODEL_IDS, ProjectNameSchema } from '../../../../schema'; import { validateFolderNotExists } from '../../../commands/create/validate'; import { VPC_ENDPOINT_WARNING } from '../../../commands/shared/vpc-utils'; -import { isPreviewEnabled } from '../../../feature-flags'; import { computeDefaultCredentialEnvVarName } from '../../../primitives/credential-utils'; import { LogLink, @@ -146,11 +145,6 @@ function getCreateNextSteps(hasAgent: boolean): NextStep[] { return [{ command: 'add', label: 'Add an agent' }]; } -const CREATE_PROMPT_ITEMS = [ - { id: 'yes', title: 'Yes, add an agent' }, - { id: 'no', title: "No, I'll do it later" }, -]; - const CREATE_TYPE_ITEMS = [ { id: 'harness', title: 'Harness (recommended)', description: 'Managed config-based agent loop, no code required' }, { @@ -256,7 +250,6 @@ export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateS const flow = useCreateFlow(cwd); // Project root is cwd/projectName (new project directory) const projectRoot = join(cwd, flow.projectName); - const preview = isPreviewEnabled(); // Completion state for next steps const allSuccess = !flow.hasError && flow.isComplete && flow.phase === 'complete'; @@ -288,24 +281,14 @@ export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateS } }, [allSuccess, handleExit]); - // GA mode: binary create prompt navigation - const { selectedIndex: createPromptIndex } = useListNavigation({ - items: CREATE_PROMPT_ITEMS, - onSelect: item => { - flow.setWantsCreate(item.id === 'yes'); - }, - onExit: handleExit, - isActive: !preview && flow.phase === 'create-prompt', - }); - - // Preview mode: 3-option create type selection navigation + // 3-option create type selection navigation const { selectedIndex: createTypeIndex } = useListNavigation({ items: CREATE_TYPE_ITEMS, onSelect: item => { flow.handleCreateTypeSelection(item.id as 'harness' | 'agent' | 'skip'); }, onExit: handleExit, - isActive: preview && flow.phase === 'create-type-prompt', + isActive: flow.phase === 'create-type-prompt', }); // Checking phase: instant async check — render nothing to avoid a flash before the real UI @@ -324,8 +307,8 @@ export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateS ); } - // Harness wizard phase (preview only, separate component, no header conflict) - if (preview && flow.phase === 'harness-wizard') { + // Harness wizard phase (separate component, no header conflict) + if (flow.phase === 'harness-wizard') { return ( )} - {phase === 'create-prompt' && !preview && ( - <> - - Would you like to add an agent now? - - - - - - )} - - {phase === 'create-type-prompt' && preview && ( + {phase === 'create-type-prompt' && ( <> What would you like to build? diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index 146498b12..11ba29c81 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -11,7 +11,6 @@ import type { DeployedState } from '../../../../schema'; import { getCredentialProvider } from '../../../aws/account'; import { validateFilesystemMountsConfiguration } from '../../../commands/shared/filesystem-utils'; import { getErrorMessage } from '../../../errors'; -import { isPreviewEnabled } from '../../../feature-flags'; import { CreateLogger } from '../../../logging'; import { initGitRepo, setupNodeProject, setupPythonProject, writeEnvFile, writeGitignore } from '../../../operations'; import { createConfigBundleForAgent } from '../../../operations/agent/config-bundle-defaults'; @@ -55,7 +54,6 @@ type CreatePhase = | 'checking' | 'existing-project-error' | 'input' - | 'create-prompt' | 'create-type-prompt' | 'create-wizard' | 'harness-wizard' @@ -74,10 +72,7 @@ interface CreateFlowState { // Project name actions setProjectName: (name: string) => void; confirmProjectName: () => void; - // Create prompt actions (GA mode) - wantsCreate: boolean; - setWantsCreate: (wants: boolean) => void; - // Create type selection (preview mode) + // Create type selection handleCreateTypeSelection: (choice: 'harness' | 'agent' | 'skip') => void; // Add agent config (set when AddAgentScreen completes) addAgentConfig: AddAgentConfig | null; @@ -151,9 +146,6 @@ export function useCreateFlow(cwd: string): CreateFlowState { const [outputDir, setOutputDir] = useState(); const [logFilePath, setLogFilePath] = useState(); - // Create prompt state - const [wantsCreate, setWantsCreate] = useState(false); - // Add agent config (from AddAgentScreen) const [addAgentConfig, setAddAgentConfig] = useState(null); @@ -184,30 +176,13 @@ export function useCreateFlow(cwd: string): CreateFlowState { }, [cwd, phase]); const confirmProjectName = useCallback(() => { - setPhase(isPreviewEnabled() ? 'create-type-prompt' : 'create-prompt'); + setPhase('create-type-prompt'); }, []); const updateStep = (index: number, update: Partial) => { setSteps(prev => prev.map((s, i) => (i === index ? { ...s, ...update } : s))); }; - // Create prompt handlers - const handleSetWantsCreate = useCallback( - (wants: boolean) => { - setWantsCreate(wants); - if (wants) { - setAddAgentConfig(null); // Reset any previous config - setPhase('create-wizard'); - } else { - // Skip add agent, go straight to running - setAddAgentConfig(null); - setSteps(getCreateSteps(projectName, null)); - setPhase('running'); - } - }, - [projectName] - ); - // Handle completion from AddAgentScreen const handleAddAgentComplete = useCallback( (config: AddAgentConfig) => { @@ -220,7 +195,7 @@ export function useCreateFlow(cwd: string): CreateFlowState { // Go back from add agent wizard to create prompt const goBackFromAddAgent = useCallback(() => { - setPhase(isPreviewEnabled() ? 'create-type-prompt' : 'create-prompt'); + setPhase('create-type-prompt'); }, []); // Preview mode: create type selection handler @@ -646,7 +621,7 @@ export function useCreateFlow(cwd: string): CreateFlowState { await withMinDuration(async () => { logger.logSubStep(`Adding harness: ${addHarnessConfig.name}`); const { harnessPrimitive: hp } = await import('../../../primitives/registry'); - const result = await hp!.add({ + const result = await hp.add({ name: addHarnessConfig.name, modelProvider: addHarnessConfig.modelProvider, modelId: addHarnessConfig.modelId, @@ -775,10 +750,7 @@ export function useCreateFlow(cwd: string): CreateFlowState { logFilePath, setProjectName, confirmProjectName, - // Create prompt (GA) - wantsCreate, - setWantsCreate: handleSetWantsCreate, - // Create type selection (preview) + // Create type selection handleCreateTypeSelection, // Add agent addAgentConfig, diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index a5a1ebc97..25323e9e5 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -19,7 +19,6 @@ import { } from '../../../cloudformation'; import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from '../../../commands/deploy/utils.js'; import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors'; -import { isPreviewEnabled } from '../../../feature-flags'; import { ExecLogger } from '../../../logging'; import { MANAGED_MEMORY_DEPLOY_NOTICE, @@ -449,11 +448,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState const existingState = await configIO.readDeployedState().catch(() => undefined); // Parse harness outputs (harnesses are now part of the CloudFormation stack). - // Preview-gated to match the synth path: with preview off, bin/cdk.ts emits no harness - // resource/outputs, so skip parsing entirely (see toolkit-lib/wrapper.ts + bin/cdk.ts). - const harnessNames = isPreviewEnabled() - ? (ctx.projectSpec.harnesses ?? []).map((h: { name: string }) => h.name) - : []; + const harnessNames = (ctx.projectSpec.harnesses ?? []).map((h: { name: string }) => h.name); const deployedHarnesses = parseHarnessOutputs(outputs, harnessNames); let deployedState = buildDeployedState({ diff --git a/src/cli/tui/screens/dev/DevScreen.tsx b/src/cli/tui/screens/dev/DevScreen.tsx index bffaba95b..8dbc66c75 100644 --- a/src/cli/tui/screens/dev/DevScreen.tsx +++ b/src/cli/tui/screens/dev/DevScreen.tsx @@ -1,5 +1,4 @@ import type { AgentEnvSpec } from '../../../../schema'; -import { isPreviewEnabled } from '../../../feature-flags'; import { isDeploySkippable } from '../../../operations/deploy/change-detection'; import { getDevSupportedAgents, getEndpointUrl, loadProjectConfig } from '../../../operations/dev'; import { @@ -28,9 +27,9 @@ interface DevScreenProps { agentName?: string; /** Custom headers to forward to the agent on every invocation */ headers?: Record; - /** Skip automatic resource deployment (preview) */ + /** Skip automatic resource deployment */ skipDeploy?: boolean; - /** Called when deploy completes and browser mode should launch (preview) */ + /** Called when deploy completes and browser mode should launch */ onLaunchBrowser?: (selection?: { agentName?: string; harnessName?: string }) => void; } @@ -138,7 +137,6 @@ const MAX_VISIBLE_TOOLS = 5; export function DevScreen(props: DevScreenProps) { const { onLaunchBrowser } = props; - const preview = isPreviewEnabled(); const [mode, setMode] = useState('select-agent'); const [isExiting, setIsExiting] = useState(false); const [scrollOffset, setScrollOffset] = useState(0); @@ -158,7 +156,7 @@ export function DevScreen(props: DevScreenProps) { const [isContainerExec, setIsContainerExec] = useState(false); const [execInputEmpty, setExecInputEmpty] = useState(true); - // Harness state (preview) + // Harness state const [availableHarnesses, setAvailableHarnesses] = useState([]); const [selectedHarness, setSelectedHarness] = useState(); @@ -171,7 +169,7 @@ export function DevScreen(props: DevScreenProps) { const agents = getDevSupportedAgents(project); setSupportedAgents(agents); - const harnesses = preview ? (project?.harnesses ?? []).map(h => h.name) : []; + const harnesses = (project?.harnesses ?? []).map(h => h.name); setAvailableHarnesses(harnesses); // If agent name was provided via CLI, validate it @@ -218,7 +216,7 @@ export function DevScreen(props: DevScreenProps) { } }; void load(); - }, [workingDir, props.agentName, preview, onLaunchBrowser]); + }, [workingDir, props.agentName, onLaunchBrowser]); const onServerReady = useCallback(() => setMode(prev => (prev === 'chat' ? 'input' : prev)), []); @@ -433,7 +431,7 @@ export function DevScreen(props: DevScreenProps) { setMode('chat'); } } - } else if (preview) { + } else { const harnessIdx = selectedAgentIndex - supportedAgents.length; const harnessName = availableHarnesses[harnessIdx]; if (harnessName) { @@ -521,14 +519,8 @@ export function DevScreen(props: DevScreenProps) { return ( - - {preview ? 'No agents or harnesses defined in project.' : 'No agents defined in project.'} - - - {preview - ? 'Dev mode requires at least one agent with an entrypoint or a harness.' - : 'Dev mode requires at least one agent with an entrypoint.'} - + No agents or harnesses defined in project. + Dev mode requires at least one agent with an entrypoint or a harness. Run agentcore add agent to create one. @@ -569,8 +561,8 @@ export function DevScreen(props: DevScreenProps) { ); } - // If harness mode (preview), render the InvokeScreen with the pre-selected harness - if (preview && mode === 'harness') { + // If harness mode, render the InvokeScreen with the pre-selected harness + if (mode === 'harness') { return ; } @@ -611,19 +603,17 @@ export function DevScreen(props: DevScreenProps) { description: `${agent.runtimeVersion} · ${agent.build}`, })); - const harnessItems = preview - ? availableHarnesses.map((name, i) => ({ - id: `harness-${i}`, - title: name, - description: 'Harness', - })) - : []; + const harnessItems = availableHarnesses.map((name, i) => ({ + id: `harness-${i}`, + title: name, + description: 'Harness', + })); const allItems = [...agentItems, ...harnessItems]; return ( - 0 ? 'Select Target' : 'Select Agent'} fullWidth> + 0 ? 'Select Target' : 'Select Agent'} fullWidth> diff --git a/src/cli/tui/screens/harness/AddHarnessFlow.tsx b/src/cli/tui/screens/harness/AddHarnessFlow.tsx index bede73f7a..8c8882706 100644 --- a/src/cli/tui/screens/harness/AddHarnessFlow.tsx +++ b/src/cli/tui/screens/harness/AddHarnessFlow.tsx @@ -55,7 +55,7 @@ export function AddHarnessFlow({ isInteractive = true, onExit, onBack, onDev, on setFlow({ name: 'create-success', harnessName: config.name, loading: true, loadingMessage: 'Creating harness...' }); try { const { harnessPrimitive } = await import('../../../primitives/registry'); - const result = await harnessPrimitive!.add({ + const result = await harnessPrimitive.add({ name: config.name, modelProvider: config.modelProvider, modelId: config.modelId, diff --git a/src/cli/tui/screens/harness/useAddHarnessWizard.ts b/src/cli/tui/screens/harness/useAddHarnessWizard.ts index ccf057293..5448c542f 100644 --- a/src/cli/tui/screens/harness/useAddHarnessWizard.ts +++ b/src/cli/tui/screens/harness/useAddHarnessWizard.ts @@ -1,5 +1,5 @@ import type { HarnessApiFormat, HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; -import { isGatedFeaturesEnabled, isPreviewEnabled } from '../../../feature-flags'; +import { isGatedFeaturesEnabled } from '../../../feature-flags'; import type { JwtConfig } from '../../components/jwt-config'; import { HARNESS_FILESYSTEM_STEP_NAMES, useFilesystemMountState } from '../../hooks/useFilesystemMountState'; import type { AddHarnessConfig, AddHarnessStep, AdvancedSetting, ContainerMode } from './types'; @@ -74,7 +74,7 @@ export function useAddHarnessWizard() { const allSteps = useMemo(() => { const steps: AddHarnessStep[] = ['name', 'model-provider']; - if ((config.modelProvider === 'bedrock' || config.modelProvider === 'open_ai') && isPreviewEnabled()) { + if (config.modelProvider === 'bedrock' || config.modelProvider === 'open_ai') { steps.push('api-format'); } @@ -349,15 +349,13 @@ export function useAddHarnessWizard() { // apiBase / additionalParams only apply to lite_llm — clear them when switching away. ...(modelProvider !== 'lite_llm' && { apiBase: undefined, additionalParams: undefined }), })); - // bedrock and open_ai both have a preview-gated api-format step that sits before api-key-arn + // bedrock and open_ai both have an api-format step that sits before api-key-arn // in allSteps — route through it for BOTH (open_ai previously jumped straight to api-key-arn, // making api-format forward-unreachable and leaving a false ✓ on the skipped step). - if ((modelProvider === 'bedrock' || modelProvider === 'open_ai') && isPreviewEnabled()) { + if (modelProvider === 'bedrock' || modelProvider === 'open_ai') { setStep('api-format'); - } else if (modelProvider !== 'bedrock') { - setStep('api-key-arn'); } else { - setStep('container'); + setStep('api-key-arn'); } }, []); diff --git a/src/cli/tui/screens/invoke/InvokeScreen.tsx b/src/cli/tui/screens/invoke/InvokeScreen.tsx index 860e43132..092076a1e 100644 --- a/src/cli/tui/screens/invoke/InvokeScreen.tsx +++ b/src/cli/tui/screens/invoke/InvokeScreen.tsx @@ -1,4 +1,3 @@ -import { isPreviewEnabled } from '../../../feature-flags'; import { buildTraceConsoleUrl } from '../../../operations/traces'; import { GradientText, LogLink, Panel, Screen, SelectList, TextInput } from '../../components'; import { setExitMessage } from '../../exit-message'; @@ -22,7 +21,7 @@ interface InvokeScreenProps { onExec?: (result: { runtimeArn: string; region: string; sessionId?: string }) => void; /** True when remounting after a PTY detour — shows [session resumed] hint. False for direct --session-id invocations. */ isResume?: boolean; - /** Pre-select a harness by name, skipping the agent selection screen (preview) */ + /** Pre-select a harness by name, skipping the agent selection screen */ initialHarnessName?: string; /** Payment instrument ID (wallet) forwarded on every turn when invoking with payments */ initialPaymentInstrumentId?: string; @@ -166,7 +165,6 @@ export function InvokeScreen({ initialPaymentUserId, initialAutoSession, }: InvokeScreenProps) { - const preview = isPreviewEnabled(); const { phase, config, @@ -218,11 +216,10 @@ export function InvokeScreen({ }, [sessionId, messages.length]); // Compute auth type early so hooks can reference it - const totalInvokables = (config?.runtimes.length ?? 0) + (preview ? (config?.harnesses.length ?? 0) : 0); + const totalInvokables = (config?.runtimes.length ?? 0) + (config?.harnesses.length ?? 0); const runtimeCount = config?.runtimes.length ?? 0; const currentAgent = selectedAgent < runtimeCount ? config?.runtimes[selectedAgent] : undefined; - const currentHarness = - preview && selectedAgent >= runtimeCount ? config?.harnesses[selectedAgent - runtimeCount] : undefined; + const currentHarness = selectedAgent >= runtimeCount ? config?.harnesses[selectedAgent - runtimeCount] : undefined; const isCustomJwt = (currentAgent?.authorizerType ?? currentHarness?.authorizerType) === 'CUSTOM_JWT'; // Handle initial prompt - skip agent selection if only one invokable @@ -348,7 +345,7 @@ export function InvokeScreen({ if (key.return) { const chosen = config.runtimes[selectedAgent]; const chosenHarness = - preview && selectedAgent >= config.runtimes.length + selectedAgent >= config.runtimes.length ? config.harnesses[selectedAgent - config.runtimes.length] : undefined; const authType = chosen?.authorizerType ?? chosenHarness?.authorizerType; @@ -438,7 +435,7 @@ export function InvokeScreen({ return null; } - const isHarnessSelected = preview && selectedAgent >= config.runtimes.length; + const isHarnessSelected = selectedAgent >= config.runtimes.length; const agent = isHarnessSelected ? undefined : config.runtimes[selectedAgent]; const selectedHarness = isHarnessSelected ? config.harnesses[selectedAgent - config.runtimes.length] : undefined; const selectedName = agent?.name ?? selectedHarness?.name; @@ -459,13 +456,11 @@ export function InvokeScreen({ title: a.name, description: `${a.protocol && a.protocol !== 'HTTP' ? `${a.protocol} · ` : ''}Agent`, })), - ...(preview - ? config.harnesses.map((h, i) => ({ - id: String(config.runtimes.length + i), - title: h.name, - description: 'Harness', - })) - : []), + ...config.harnesses.map((h, i) => ({ + id: String(config.runtimes.length + i), + title: h.name, + description: 'Harness', + })), ]; const isMcp = !isHarnessSelected && agentProtocol === 'MCP'; diff --git a/src/cli/tui/screens/invoke/useInvokeFlow.ts b/src/cli/tui/screens/invoke/useInvokeFlow.ts index e015a0857..fd01237b6 100644 --- a/src/cli/tui/screens/invoke/useInvokeFlow.ts +++ b/src/cli/tui/screens/invoke/useInvokeFlow.ts @@ -26,7 +26,6 @@ import { invokeHarness } from '../../../aws/agentcore-harness'; import { computeInvokeAttrs } from '../../../commands/invoke/utils'; import { ANSI } from '../../../constants'; import { getErrorMessage } from '../../../errors'; -import { isPreviewEnabled } from '../../../feature-flags'; import { InvokeLogger } from '../../../logging'; import { formatMcpToolList } from '../../../operations/dev/utils'; import { @@ -75,7 +74,7 @@ export interface InvokeFlowOptions { initialBearerToken?: string; /** Show [session resumed] hint on load — true only when remounting after a PTY detour. */ isResume?: boolean; - /** Pre-select a harness by name, skipping the agent selection screen (preview) */ + /** Pre-select a harness by name, skipping the agent selection screen */ initialHarnessName?: string; /** Payment instrument ID (wallet) forwarded on every invocation when payments are used */ initialPaymentInstrumentId?: string; @@ -177,7 +176,6 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState const result = await withCommandRunTelemetry( 'invoke', computeInvokeAttrs({ - preview: isPreviewEnabled(), harnessName: initialHarnessName, harnessCount: project?.harnesses?.length ?? 0, runtimeCount: project?.runtimes?.length ?? 0, @@ -245,19 +243,17 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState } const harnesses: InvokeConfig['harnesses'] = []; - if (isPreviewEnabled()) { - for (const harness of project.harnesses ?? []) { - const state = targetState?.resources?.harnesses?.[harness.name]; - if (!state) continue; - let authorizerType: RuntimeAuthorizerType | undefined; - try { - const spec = await configIO.readHarnessSpec(harness.name); - authorizerType = spec.authorizerType; - } catch { - // spec read is best-effort - } - harnesses.push({ name: harness.name, state, authorizerType }); + for (const harness of project.harnesses ?? []) { + const state = targetState?.resources?.harnesses?.[harness.name]; + if (!state) continue; + let authorizerType: RuntimeAuthorizerType | undefined; + try { + const spec = await configIO.readHarnessSpec(harness.name); + authorizerType = spec.authorizerType; + } catch { + // spec read is best-effort } + harnesses.push({ name: harness.name, state, authorizerType }); } if (runtimes.length === 0 && harnesses.length === 0) { @@ -552,7 +548,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState async (prompt: string) => { if (!config || phase === 'invoking') return; - const isHarness = isPreviewEnabled() && selectedAgent >= config.runtimes.length; + const isHarness = selectedAgent >= config.runtimes.length; const agent = config.runtimes[selectedAgent]; if (!agent && !isHarness) return; @@ -572,7 +568,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState const logger = loggerRef.current; - // Harness invoke (preview) + // Harness invoke if (isHarness) { const harnessIdx = selectedAgent - config.runtimes.length; const harness = config.harnesses[harnessIdx]; @@ -863,7 +859,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState async (command: string) => { if (!config || phase === 'invoking') return; - const isHarnessExec = isPreviewEnabled() && selectedAgent >= config.runtimes.length; + const isHarnessExec = selectedAgent >= config.runtimes.length; const agent = isHarnessExec ? undefined : config.runtimes[selectedAgent]; if (!agent && !isHarnessExec) return; diff --git a/src/cli/tui/screens/remove/RemoveFlow.tsx b/src/cli/tui/screens/remove/RemoveFlow.tsx index af7c4633e..036bf6cb4 100644 --- a/src/cli/tui/screens/remove/RemoveFlow.tsx +++ b/src/cli/tui/screens/remove/RemoveFlow.tsx @@ -438,7 +438,7 @@ export function RemoveFlow({ const handleConfirmOrphanHarness = useCallback(async (harnessName: string, orphanAction: OrphanAction) => { setFlow({ name: 'loading', message: `Deleting orphan harness ${harnessName} from AWS...` }); - const result = await harnessPrimitive!.remove(harnessName, { orphanAction }); + const result = await harnessPrimitive.remove(harnessName, { orphanAction }); if (result.success) { setFlow({ name: 'harness-success', harnessName }); } else { diff --git a/src/cli/tui/screens/remove/RemoveScreen.tsx b/src/cli/tui/screens/remove/RemoveScreen.tsx index 16acf4a2d..3aee40745 100644 --- a/src/cli/tui/screens/remove/RemoveScreen.tsx +++ b/src/cli/tui/screens/remove/RemoveScreen.tsx @@ -1,4 +1,4 @@ -import { isGatedFeaturesEnabled, isPreviewEnabled } from '../../../feature-flags'; +import { isGatedFeaturesEnabled } from '../../../feature-flags'; import type { SelectableItem } from '../../components'; import { SelectScreen } from '../../components'; import { useMemo } from 'react'; @@ -24,9 +24,7 @@ export type RemoveResourceType = const REMOVE_RESOURCES: { id: RemoveResourceType; title: string; description: string }[] = [ { id: 'agent', title: 'Agent', description: 'Remove an agent from the project' }, - ...(isPreviewEnabled() - ? [{ id: 'harness' as const, title: 'Harness', description: 'Remove a harness from the project' }] - : []), + { id: 'harness', title: 'Harness', description: 'Remove a harness from the project' }, { id: 'memory', title: 'Memory', description: 'Remove a memory provider' }, { id: 'credential', title: 'Credential', description: 'Remove a credential' }, { id: 'evaluator', title: 'Evaluator', description: 'Remove a custom evaluator' }, diff --git a/vitest.config.ts b/vitest.config.ts index d1e678033..44c08c51e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -26,9 +26,6 @@ const textLoaderPlugin = { }; export default defineConfig({ - define: { - __PREVIEW__: process.env.BUILD_PREVIEW === '1' ? 'true' : 'false', - }, resolve: { alias: { '@': path.resolve(__dirname, './src'),