Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/cli/tui/screens/agent/AddAgentScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import {
} from './types';
import { Box, Text, useInput } from 'ink';
import Spinner from 'ink-spinner';
import { basename, resolve } from 'path';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

// Helper to get provider display name and env var name from ModelProvider
Expand Down Expand Up @@ -1086,12 +1085,18 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg
{byoStep === 'dockerfile' && (
<PathInput
placeholder="Select a Dockerfile"
basePath={resolve(project?.projectRoot ?? process.cwd(), byoConfig.codeLocation)}
// basePath omitted → defaults to process.cwd(), matching the
// "add policy" wizard. Resolving from the not-yet-created
// project's codeLocation produced confusing "not a valid path"
// errors (issue #1128).
pathType="file"
allowEmpty
emptyHelpText="Press Enter to use the default Dockerfile"
onSubmit={value => {
setByoConfig(c => ({ ...c, dockerfile: value ? basename(value) : '' }));
// Preserve the full user-supplied path so handleByoPath can
// locate and copy the source file. The file is collapsed to a
// basename only after a successful copy into the code dir.
setByoConfig(c => ({ ...c, dockerfile: value }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same regression as the matching change in GenerateWizardUI.tsx: the full path is now preserved in byoConfig.dockerfile, and in the agentcore create flow it reaches mapByoConfigToAgent (useAddAgent.ts:49) via useCreateFlow.ts:378 without being basename'd or copied. The resulting agent spec fails AgentEnvSpecSchema.dockerfile validation for any /-containing input.

This branch is fine for the agent add path (where handleByoPath copies and basename's), but we need the create path to do the same work before this can land. See the inline comment on GenerateWizardUI.tsx for options.

goToNextByoStep('dockerfile');
}}
onCancel={handleByoBack}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { resolveDockerfileSource } from '../useAddAgent';
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';

/**
* Regression tests for issue #1128: the Dockerfile picker in the agent BYO
* wizard must resolve user-supplied paths relative to the directory the user
* invoked `agentcore` from, NOT relative to the (possibly not-yet-created)
* project's code directory.
*/
describe('resolveDockerfileSource', () => {
it('returns null for undefined (no dockerfile selected)', () => {
expect(resolveDockerfileSource(undefined)).toBeNull();
});

it('returns null for empty string (user pressed Enter for default)', () => {
expect(resolveDockerfileSource('')).toBeNull();
});

it('returns null for a bare filename (no slash)', () => {
// A bare filename means the file is expected to already live inside the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This test locks in the "bare filename → null (no copy)" behavior. Given that the reporter's exact input in #1128 is a bare filename (MyDockerfile) placed in their cwd, this test is arguably encoding the bug rather than preventing it. Please re-evaluate this case as part of the fix discussed in the useAddAgent.ts comment.

// build context's code directory; no copy is needed.
expect(resolveDockerfileSource('Dockerfile')).toBeNull();
expect(resolveDockerfileSource('Dockerfile.dev')).toBeNull();
});

it('resolves a relative path against the supplied cwd, not against any project root', () => {
const cwd = '/home/user/workplace/.github';
expect(resolveDockerfileSource('docker/MyDockerfile.dev', cwd)).toBe(resolve(cwd, 'docker/MyDockerfile.dev'));
});

it('resolves a nested relative path against the supplied cwd', () => {
const cwd = '/home/user/workplace/.github';
expect(resolveDockerfileSource('subdir/MyDockerfile', cwd)).toBe(resolve(cwd, 'subdir/MyDockerfile'));
});

it('resolves "./Dockerfile" against the supplied cwd', () => {
const cwd = '/home/user/workplace/.github';
// "./Dockerfile" contains "/", so it is treated as a real path, not a
// bare filename. resolve() collapses the "./" segment.
expect(resolveDockerfileSource('./Dockerfile', cwd)).toBe(resolve(cwd, 'Dockerfile'));
});

it('preserves absolute paths (they are not re-rooted)', () => {
const cwd = '/home/user/workplace/.github';
const abs = '/tmp/elsewhere/Dockerfile';
expect(resolveDockerfileSource(abs, cwd)).toBe(abs);
});

it('defaults to process.cwd() when no cwd argument is supplied', () => {
const original = process.cwd();
expect(resolveDockerfileSource('subdir/Dockerfile')).toBe(resolve(original, 'subdir/Dockerfile'));
});

it('does NOT resolve relative to a project root (regression for #1128)', () => {
// The bug: previously the picker rooted at <projectRoot>/<codeLocation>,
// e.g. /home/user/.github/myproj/app/myagent/. A user typing
// "MyDockerfile" from /home/user/.github would have it looked up at
// /home/user/.github/myproj/app/myagent/MyDockerfile — wrong.
const cwd = '/home/user/workplace/.github';
const resolved = resolveDockerfileSource('subdir/MyDockerfile', cwd);
expect(resolved).toBe('/home/user/workplace/.github/subdir/MyDockerfile');
expect(resolved).not.toContain('/app/');
expect(resolved).not.toContain('/myproj/');
});
});
56 changes: 42 additions & 14 deletions src/cli/tui/screens/agent/useAddAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ export interface AddAgentError {

export type AddAgentOutcome = AddAgentCreateResult | AddAgentByoResult | AddAgentError;

// ─────────────────────────────────────────────────────────────────────────────
// Dockerfile source resolution
// ─────────────────────────────────────────────────────────────────────────────

/**
* Resolve the absolute source path of a user-supplied Dockerfile.
*
* The path is interpreted relative to the directory the user invoked
* `agentcore` from (process.cwd()), matching the "add policy" file picker
* (issue #1128). Absolute paths are returned unchanged. A `null` return
* value means the caller should not attempt to copy the file (the user
* either left the field empty, indicating they want the default
* Dockerfile, or supplied a bare filename indicating the file already
* lives inside the build context).
*/
export function resolveDockerfileSource(dockerfile: string | undefined, cwd: string = process.cwd()): string | null {
// Empty / undefined / bare-filename inputs do not need resolution: empty
// means "use default", bare filename means "already in code dir".
if (!dockerfile?.includes('/')) {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This short-circuits on any input without a /, treating bare filenames as "already in code dir, nothing to copy". That doesn't match the reporter's scenario in #1128: they typed MyDockerfile (a bare filename) expecting the file they placed next to the wizard in their cwd to be picked up. After this PR, the validation error goes away, but the file still isn't copied — the agent spec will reference MyDockerfile in the code dir, which doesn't exist yet, and the next deploy will fail.

A few ways to address this:

  1. Always try cwd first: if dockerfile is non-empty, test existsSync(resolve(cwd, dockerfile)) and return that path; if it doesn't exist there, return null (i.e. the file was already placed in the code dir, or user wants the template default). This handles MyDockerfile in cwd naturally.
  2. Require users to disambiguate: prefix bare filenames with ./ in the picker before submitting, so the /-check is a reliable "user referred to cwd" signal. Less forgiving for the The path to dockerfile should be fixed to where the agentcore command was run #1128 reporter.
  3. Leave as-is and document that bare filenames are interpreted as "file already lives in the code dir" — but then the tests / PR description should not claim that MyDockerfile in cwd gets copied, because it doesn't.

Related: whichever behavior you pick for this helper also needs to be applied in useCreateFlow.ts (see the other inline comment).

return resolve(cwd, dockerfile);
}

// ─────────────────────────────────────────────────────────────────────────────
// Config Mappers
// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -260,14 +284,16 @@ async function handleCreatePath(
const renderer = createRenderer(renderConfig);
await renderer.render({ outputDir: projectRoot });

// If dockerfile is a path (contains /), copy it into the agent directory (overwriting template default)
if (generateConfig.dockerfile?.includes('/')) {
const sourcePath = resolve(projectRoot, generateConfig.dockerfile);
if (!existsSync(sourcePath)) {
return { ok: false, error: `Dockerfile not found at ${sourcePath}` };
// If dockerfile is a path (contains /), copy it into the agent directory (overwriting template default).
// The path is interpreted relative to the directory the user invoked agentcore from (process.cwd()),
// matching the behavior of the "add policy" file picker (issue #1128).
const createSourcePath = resolveDockerfileSource(generateConfig.dockerfile);
if (createSourcePath !== null) {
if (!existsSync(createSourcePath)) {
return { ok: false, error: `Dockerfile not found at ${createSourcePath}` };
}
const filename = basename(sourcePath);
copyFileSync(sourcePath, join(agentPath, filename));
const filename = basename(createSourcePath);
copyFileSync(createSourcePath, join(agentPath, filename));
generateConfig.dockerfile = filename;
}

Expand Down Expand Up @@ -369,15 +395,17 @@ async function handleByoPath(
const codeDir = join(projectRoot, config.codeLocation.replace(/\/$/, ''));
mkdirSync(codeDir, { recursive: true });

// If dockerfile is a path (contains /), copy it into the code directory and use the filename
// If dockerfile is a path (contains /), copy it into the code directory and use the filename.
// The user-supplied path is resolved relative to process.cwd() — the directory the user
// invoked agentcore from — matching the "add policy" file picker (issue #1128).
let dockerfileName = config.dockerfile;
if (dockerfileName?.includes('/')) {
const sourcePath = resolve(projectRoot, dockerfileName);
if (!existsSync(sourcePath)) {
return { ok: false, error: `Dockerfile not found at ${sourcePath}` };
const byoSourcePath = resolveDockerfileSource(dockerfileName);
if (byoSourcePath !== null) {
if (!existsSync(byoSourcePath)) {
return { ok: false, error: `Dockerfile not found at ${byoSourcePath}` };
}
dockerfileName = basename(sourcePath);
copyFileSync(sourcePath, join(codeDir, dockerfileName));
dockerfileName = basename(byoSourcePath);
copyFileSync(byoSourcePath, join(codeDir, dockerfileName));
}

const project = await configIO.readProjectSpec();
Expand Down
7 changes: 5 additions & 2 deletions src/cli/tui/screens/generate/GenerateWizardUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
} from './types';
import type { useGenerateWizard } from './useGenerateWizard';
import { Box, Text, useInput } from 'ink';
import { basename } from 'path';

// Helper to get provider display name and env var name from ModelProvider
function getProviderInfo(provider: ModelProvider): { name: string; envVarName: string } {
Expand Down Expand Up @@ -234,7 +233,11 @@ export function GenerateWizardUI({
allowEmpty
emptyHelpText="Press Enter to use the default Dockerfile"
onSubmit={value => {
wizard.setDockerfile(value ? basename(value) : undefined);
// Preserve the full user-supplied path so handleCreatePath can
// locate and copy the source file. The path is collapsed to a
// basename only after a successful copy into the agent dir
// (issue #1128).
wizard.setDockerfile(value || undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the regression path. Removing basename() here preserves the full user-supplied path in wizard.config.dockerfile. That config is consumed by two code paths:

  1. agent adduseAddAgent.ts::handleCreatePath (which this PR updates to copy the file and then replace generateConfig.dockerfile with basename(...) before persisting). ✅
  2. agentcore createsrc/cli/tui/screens/create/useCreateFlow.ts (lines ~259–326), which is not updated by this PR. It writes addAgentConfig.dockerfile straight into generateConfig (line 264) and passes it to writeAgentToProject, which schema-validates via AgentCoreProjectSpecSchemaAgentEnvSpecSchema.dockerfile regex ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ (src/schema/schemas/agent-env.ts:229-234; see also the explicit test at src/schema/schemas/__tests__/agent-env.test.ts:426-431'path/to/Dockerfile'success: false).

So if a user runs agentcore create → Create agent → Custom Dockerfile and types anything containing / (e.g. ./Dockerfile, subdir/Dockerfile, an absolute path), project creation will fail schema validation — a regression vs. the pre-PR behavior where basename() always produced a schema-valid filename.

Options to fix:

  1. Preferred: Update useCreateFlow.ts to mirror the handleCreatePath / handleByoPath logic from useAddAgent.ts — call resolveDockerfileSource, verify existence, copyFileSync into the agent code dir, and basename() the value before it reaches mapGenerateConfigToAgent / mapByoConfigToAgent. (Arguably the duplicated logic between useCreateFlow.ts and useAddAgent.ts should be factored into a shared helper — that would also prevent this class of bug from recurring.)
  2. Keep basename() on submit in GenerateWizardUI but also stash the original full path on the wizard/byo config under a separate field (e.g. dockerfileSource) and have both code paths read that. More invasive but avoids mutating generateConfig.dockerfile late.
  3. Short-term: revert just this one-line change (and the matching one in AddAgentScreen.tsx) so that the wizard continues to submit a basename. The validation-error fix (the basePath change in AddAgentScreen.tsx) stands on its own and is the main thing The path to dockerfile should be fixed to where the agentcore command was run #1128 asks for.

Whichever route you take, please add a test (ideally an integration-style one) that exercises agentcore create → BYO Container → Custom Dockerfile end-to-end with a path that contains /, so this path is protected going forward.

}}
onCancel={onBack}
/>
Expand Down
Loading