diff --git a/src/cli/operations/deploy/__tests__/preflight-container.test.ts b/src/cli/operations/deploy/__tests__/preflight-container.test.ts index 3b203816e..78262a3f8 100644 --- a/src/cli/operations/deploy/__tests__/preflight-container.test.ts +++ b/src/cli/operations/deploy/__tests__/preflight-container.test.ts @@ -188,6 +188,19 @@ describe('validateContainerAgents', () => { expect(ensureMock).not.toHaveBeenCalled(); }); + it('does NOT ensure .dockerignore when buildContextPath is set but the Dockerfile is missing (ordering guard)', () => { + // Dockerfile missing — ensure must be SKIPPED (runs only after validation), so a failing deploy + // leaves no stray .dockerignore and the friendly "Dockerfile not found" error is not masked. + mockedExistsSync.mockReturnValue(false); + + const spec = makeSpec([ + { name: 'mono', build: 'Container', codeLocation: dir('agents/mono'), buildContextPath: dir('.') }, + ]); + + expect(() => validateContainerAgents(spec, CONFIG_ROOT)).toThrow(/Dockerfile not found/); + expect(ensureMock).not.toHaveBeenCalled(); + }); + it('warns when Dockerfile uses deprecated bookworm base image', () => { mockedExistsSync.mockReturnValue(true); mockedReadFileSync.mockReturnValue( diff --git a/src/cli/operations/deploy/preflight.ts b/src/cli/operations/deploy/preflight.ts index ca7281c46..5bf91d434 100644 --- a/src/cli/operations/deploy/preflight.ts +++ b/src/cli/operations/deploy/preflight.ts @@ -212,22 +212,23 @@ export function validateContainerAgents(projectSpec: AgentCoreProjectSpec, confi errors.push( `Agent "${agent.name}": ${agent.dockerfile ?? DOCKERFILE_NAME} not found at ${dockerfilePath}. Container agents require a Dockerfile.` ); - } - warnDeprecatedBaseImage(dockerfilePath, agent.name); - - // Dockerfile validated. buildContextPath widens the docker build context (e.g. the whole repo); - // ensure a .dockerignore at that root so secrets/junk (.env, .git, agentcore/) are never baked - // into the local image. Both local and deploy honor this file; it is created only when absent - // (never overwrites the user's) and only AFTER validation — so a failing deploy leaves no stray - // file and this never masks the friendly "Dockerfile not found" error above. - if (agent.buildContextPath) { - const created = ensureBuildContextDockerignore(buildContext); - if (created) { - console.warn( - `Agent "${agent.name}": created ${created} with default secret exclusions because buildContextPath is ` + - `set (the entire context is sent to Docker/CodeBuild). Review and commit it; edit to include any ` + - `intentionally-bundled files.` - ); + } else { + warnDeprecatedBaseImage(dockerfilePath, agent.name); + + // Dockerfile validated. buildContextPath widens the docker build context (e.g. the whole repo); + // ensure a .dockerignore at that root so secrets/junk (.env, .git, agentcore/) are never baked + // into the local image. Both local and deploy honor this file; it is created only when absent + // (never overwrites the user's) and only AFTER validation — so a failing deploy leaves no stray + // file and this never masks the friendly "Dockerfile not found" error above. + if (agent.buildContextPath) { + const created = ensureBuildContextDockerignore(buildContext); + if (created) { + console.warn( + `Agent "${agent.name}": created ${created} with default secret exclusions because buildContextPath is ` + + `set (the entire context is sent to Docker/CodeBuild). Review and commit it; edit to include any ` + + `intentionally-bundled files.` + ); + } } } } diff --git a/src/schema/schemas/__tests__/agent-env.test.ts b/src/schema/schemas/__tests__/agent-env.test.ts index 750f1a0d0..076ec581a 100644 --- a/src/schema/schemas/__tests__/agent-env.test.ts +++ b/src/schema/schemas/__tests__/agent-env.test.ts @@ -566,6 +566,14 @@ describe('AgentEnvSpecSchema - dockerfile', () => { true ); }); + + it('rejects "." (names the build-context directory itself → broken `docker build -f .`)', () => { + expect(AgentEnvSpecSchema.safeParse({ ...validContainerAgent, dockerfile: '.' }).success).toBe(false); + }); + + it('accepts a "./"-prefixed filename (normalizes to the file)', () => { + expect(AgentEnvSpecSchema.safeParse({ ...validContainerAgent, dockerfile: './Dockerfile' }).success).toBe(true); + }); }); describe('AgentEnvSpecSchema - buildContextPath', () => { @@ -663,6 +671,13 @@ describe('AgentEnvSpecSchema - customDockerBuildArgs', () => { ).toBe(false); }); + it('rejects a build-arg key over 255 characters (env-var-name length limit via EnvVarNameSchema)', () => { + const longKey = 'A' + 'B'.repeat(255); // 256 chars + expect( + AgentEnvSpecSchema.safeParse({ ...validContainerAgent, customDockerBuildArgs: { [longKey]: 'v' } }).success + ).toBe(false); + }); + it.each([ 'IMAGE_URI', 'ECR_REGISTRY', diff --git a/src/schema/schemas/agent-env.ts b/src/schema/schemas/agent-env.ts index dc286134a..2a806be8f 100644 --- a/src/schema/schemas/agent-env.ts +++ b/src/schema/schemas/agent-env.ts @@ -101,7 +101,9 @@ const DOCKERFILE_PATH_ALLOWED_CHARS = /^[A-Za-z0-9._/-]+$/; /** * True when `p` is a safe relative Dockerfile path within the build context: allowed chars only, no - * leading slash (absolute), and no empty ('' from a leading/trailing/double slash) or '..' segments. + * leading slash (absolute), no empty ('' from a leading/trailing/double slash) or '..' segments, and + * it must name a file — not the build-context directory itself (a value like '.' or './' resolves to + * the context dir, producing a broken `docker build -f