From 72c9541fd85ac1dc08078202378af274559af8b5 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Tue, 14 Apr 2026 17:29:49 -0400 Subject: [PATCH] fix: only exclude root-level agentcore/ directory from packaging artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The EXCLUDED_ENTRIES set unconditionally excluded any directory named 'agentcore' at any depth during zip/copy operations. This silently dropped third-party dependency sub-modules that happen to use the same directory name (e.g., langgraph_checkpoint_aws/agentcore/), causing ImportError at runtime. Remove 'agentcore' from the flat EXCLUDED_ENTRIES set and instead thread the original rootDir through all recursive traversal functions. The agentcore directory is now only excluded when its resolved path matches join(rootDir, CONFIG_DIR) — i.e., it sits at the project root. Also remove the hand-written fflate type shim (src/lib/packaging/types/fflate.d.ts) that shadowed the package's own type declarations. The shim only declared zipSync, making all other fflate exports (including unzipSync) invisible to TypeScript. The real fflate v0.8.2 ships complete types that resolve correctly under moduleResolution: "bundler". Closes #843 --- src/lib/packaging/__tests__/helpers.test.ts | 128 ++++++++++++++++++++ src/lib/packaging/helpers.ts | 42 +++---- src/lib/packaging/types/fflate.d.ts | 13 -- 3 files changed, 149 insertions(+), 34 deletions(-) delete mode 100644 src/lib/packaging/types/fflate.d.ts diff --git a/src/lib/packaging/__tests__/helpers.test.ts b/src/lib/packaging/__tests__/helpers.test.ts index efb8e88c8..a554edb0e 100644 --- a/src/lib/packaging/__tests__/helpers.test.ts +++ b/src/lib/packaging/__tests__/helpers.test.ts @@ -16,7 +16,9 @@ import { resolveProjectPaths, resolveProjectPathsSync, } from '../helpers.js'; +import { unzipSync } from 'fflate'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import { readFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -388,3 +390,129 @@ describe('convertWindowsScriptsToLinux (shebang rewriting on non-Windows)', () = expect(existsSync(join(staging, 'bin'))).toBe(false); }); }); + +// ── Issue #843: nested agentcore directory exclusion ──────────────── + +describe('nested agentcore directory is preserved (issue #843)', () => { + let root: string; + + beforeAll(() => { + root = mkdtempSync(join(tmpdir(), 'helpers-nested-agentcore-')); + }); + + afterAll(() => { + rmSync(root, { recursive: true, force: true }); + }); + + /** + * Helper: build a source tree that mimics a real project with a + * top-level agentcore/ config dir AND a third-party dependency + * that ships its own agentcore/ sub-module. + * + * / + * main.py + * agentcore/ ← should be excluded (project config dir) + * config.yaml + * lib/ + * langgraph_checkpoint_aws/ + * __init__.py + * agentcore/ ← should be INCLUDED (dependency sub-module) + * __init__.py + * core.py + */ + function buildFixture(base: string): string { + const src = join(base, 'src'); + + // Top-level source file + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'main.py'), 'print("hello")'); + + // Top-level agentcore/ (project config — should be excluded) + mkdirSync(join(src, 'agentcore'), { recursive: true }); + writeFileSync(join(src, 'agentcore', 'config.yaml'), 'key: value'); + + // Nested dependency with its own agentcore/ sub-module + const nestedAgentcore = join(src, 'lib', 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(nestedAgentcore, { recursive: true }); + writeFileSync(join(src, 'lib', 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(nestedAgentcore, '__init__.py'), '# agentcore init'); + writeFileSync(join(nestedAgentcore, 'core.py'), 'class Core: pass'); + + return src; + } + + // ── copySourceTree (async) ── + + it('excludes top-level agentcore/ but includes nested agentcore/', async () => { + const src = buildFixture(join(root, 'copy-async')); + const dest = join(root, 'copy-async-dest'); + mkdirSync(dest, { recursive: true }); + + await copySourceTree(src, dest); + + // Top-level agentcore/ excluded + expect(existsSync(join(dest, 'agentcore'))).toBe(false); + + // Source file preserved + expect(existsSync(join(dest, 'main.py'))).toBe(true); + + // Nested agentcore/ inside dependency preserved + expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', '__init__.py'))).toBe(true); + expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', 'core.py'))).toBe(true); + }); + + // ── copySourceTreeSync ── + + it('sync: excludes top-level agentcore/ but includes nested agentcore/', () => { + const src = buildFixture(join(root, 'copy-sync')); + const dest = join(root, 'copy-sync-dest'); + mkdirSync(dest, { recursive: true }); + + copySourceTreeSync(src, dest); + + expect(existsSync(join(dest, 'agentcore'))).toBe(false); + expect(existsSync(join(dest, 'main.py'))).toBe(true); + expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', '__init__.py'))).toBe(true); + expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', 'core.py'))).toBe(true); + }); + + // ── createZipFromDir (async) ── + + it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => { + const src = buildFixture(join(root, 'zip-async')); + const zipPath = join(root, 'zip-async.zip'); + + await createZipFromDir(src, zipPath); + + const zipBuffer = await readFile(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + // Top-level agentcore/ should NOT appear + expect(entries.some(e => e === 'agentcore/config.yaml')).toBe(false); + expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); + + // Nested agentcore/ SHOULD appear + expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); + + // Regular files present + expect(entries).toContain('main.py'); + }); + + // ── createZipFromDirSync ── + + it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => { + const src = buildFixture(join(root, 'zip-sync')); + const zipPath = join(root, 'zip-sync.zip'); + + createZipFromDirSync(src, zipPath); + + const zipBuffer = readFileSync(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); + expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); + expect(entries).toContain('main.py'); + }); +}); diff --git a/src/lib/packaging/helpers.ts b/src/lib/packaging/helpers.ts index b2a2fe902..31c74b298 100644 --- a/src/lib/packaging/helpers.ts +++ b/src/lib/packaging/helpers.ts @@ -51,15 +51,7 @@ interface ResolvedPaths { artifactsDir: string; } -const EXCLUDED_ENTRIES = new Set([ - 'agentcore', - '.git', - '.venv', - '__pycache__', - '.pytest_cache', - '.DS_Store', - 'node_modules', -]); +const EXCLUDED_ENTRIES = new Set(['.git', '.venv', '__pycache__', '.pytest_cache', '.DS_Store', 'node_modules']); export const MAX_ZIP_SIZE_BYTES = 250 * 1024 * 1024; @@ -147,7 +139,7 @@ export async function ensureDirClean(dir: string): Promise { await mkdir(dir, { recursive: true }); } -async function copyEntry(source: string, destination: string): Promise { +async function copyEntry(source: string, destination: string, rootDir: string): Promise { const stats = await stat(source); if (stats.isDirectory()) { await mkdir(destination, { recursive: true }); @@ -156,7 +148,10 @@ async function copyEntry(source: string, destination: string): Promise { if (EXCLUDED_ENTRIES.has(entry)) { continue; } - await copyEntry(join(source, entry), join(destination, entry)); + if (entry === CONFIG_DIR && resolve(source) === resolve(rootDir)) { + continue; + } + await copyEntry(join(source, entry), join(destination, entry), rootDir); } return; } @@ -170,7 +165,7 @@ export async function copySourceTree(srcDir: string, destination: string): Promi if (!(await pathExists(srcDir))) { throw new MissingProjectFileError(srcDir); } - await copyEntry(srcDir, destination); + await copyEntry(srcDir, destination, srcDir); } export async function ensureBinaryAvailable(binary: string, installHint?: string): Promise { @@ -197,23 +192,24 @@ export async function createZipFromDir(sourceDir: string, outputZip: string): Pr await rm(outputZip, { force: true }); await mkdir(dirname(outputZip), { recursive: true }); - const files = await collectFiles(sourceDir); + const files = await collectFiles(sourceDir, sourceDir); const zipped = zipSync(files); await writeFile(outputZip, zipped); } -async function collectFiles(directory: string, basePath = ''): Promise { +async function collectFiles(directory: string, rootDir: string, basePath = ''): Promise { const result: Zippable = {}; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; + if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, await collectFiles(fullPath, zipPath)); + Object.assign(result, await collectFiles(fullPath, rootDir, zipPath)); } else if (entry.isFile()) { result[zipPath] = [await readFile(fullPath), { level: 6 }]; } @@ -286,7 +282,7 @@ export function ensureDirCleanSync(dir: string): void { mkdirSync(dir, { recursive: true }); } -function copyEntrySync(source: string, destination: string): void { +function copyEntrySync(source: string, destination: string, rootDir: string): void { const stats = statSync(source); if (stats.isDirectory()) { mkdirSync(destination, { recursive: true }); @@ -295,7 +291,10 @@ function copyEntrySync(source: string, destination: string): void { if (EXCLUDED_ENTRIES.has(entry)) { continue; } - copyEntrySync(join(source, entry), join(destination, entry)); + if (entry === CONFIG_DIR && resolve(source) === resolve(rootDir)) { + continue; + } + copyEntrySync(join(source, entry), join(destination, entry), rootDir); } return; } @@ -307,7 +306,7 @@ export function copySourceTreeSync(srcDir: string, destination: string): void { if (!pathExistsSync(srcDir)) { throw new MissingProjectFileError(srcDir); } - copyEntrySync(srcDir, destination); + copyEntrySync(srcDir, destination, srcDir); } export function ensureBinaryAvailableSync(binary: string, installHint?: string): void { @@ -326,18 +325,19 @@ export function ensureBinaryAvailableSync(binary: string, installHint?: string): throw new MissingDependencyError(binary, installHint); } -function collectFilesSync(directory: string, basePath = ''): Zippable { +function collectFilesSync(directory: string, rootDir: string, basePath = ''): Zippable { const result: Zippable = {}; const entries = readdirSync(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; + if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, collectFilesSync(fullPath, zipPath)); + Object.assign(result, collectFilesSync(fullPath, rootDir, zipPath)); } else if (entry.isFile()) { result[zipPath] = [readFileSync(fullPath), { level: 6 }]; } @@ -349,7 +349,7 @@ export function createZipFromDirSync(sourceDir: string, outputZip: string): void rmSync(outputZip, { force: true }); mkdirSync(dirname(outputZip), { recursive: true }); - const files = collectFilesSync(sourceDir); + const files = collectFilesSync(sourceDir, sourceDir); const zipped = zipSync(files); writeFileSync(outputZip, zipped); } diff --git a/src/lib/packaging/types/fflate.d.ts b/src/lib/packaging/types/fflate.d.ts deleted file mode 100644 index 32720ed88..000000000 --- a/src/lib/packaging/types/fflate.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module 'fflate' { - export interface ZipOptions { - level?: number; - } - - export type ZippableFile = Uint8Array | string | [Uint8Array | string, ZipOptions]; - - export interface Zippable { - [path: string]: ZippableFile | Zippable; - } - - export function zipSync(data: Zippable, options?: ZipOptions): Uint8Array; -}