diff --git a/eslint.config.mjs b/eslint.config.mjs index 54a47d63..77e3670f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -35,6 +35,9 @@ export default tseslint.config( ...importPlugin.configs.recommended.rules, ...react.configs.recommended.rules, ...security.configs.recommended.rules, + // CLI inherently works with dynamic file paths and Record lookups — these are false positives + 'security/detect-non-literal-fs-filename': 'off', + 'security/detect-object-injection': 'off', ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, 'react-hooks/preserve-manual-memoization': 'warn', diff --git a/src/cli/cdk/local-cdk-project.ts b/src/cli/cdk/local-cdk-project.ts index 9bc20d24..a0109cd0 100644 --- a/src/cli/cdk/local-cdk-project.ts +++ b/src/cli/cdk/local-cdk-project.ts @@ -32,7 +32,7 @@ export class LocalCdkProject { */ exists(): boolean { const packageJson = path.join(this.projectDir, 'package.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + return fs.existsSync(this.projectDir) && fs.existsSync(packageJson); } @@ -41,13 +41,12 @@ export class LocalCdkProject { * Throws an error if the project is missing or invalid. */ validate(): void { - // eslint-disable-next-line security/detect-non-literal-fs-filename if (!fs.existsSync(this.projectDir)) { throw new Error(`CDK project not found at ${this.projectDir}. Run 'agentcore create' first.`); } const packageJson = path.join(this.projectDir, 'package.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + if (!fs.existsSync(packageJson)) { throw new Error(`Invalid CDK project: missing package.json in ${this.projectDir}`); } diff --git a/src/cli/commands/add/__tests__/multi-agent-credentials.test.ts b/src/cli/commands/add/__tests__/multi-agent-credentials.test.ts index f99de24d..71261e66 100644 --- a/src/cli/commands/add/__tests__/multi-agent-credentials.test.ts +++ b/src/cli/commands/add/__tests__/multi-agent-credentials.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-non-literal-fs-filename */ import { runCLI } from '../../../../test-utils/index.js'; import { randomUUID } from 'node:crypto'; import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index 30bb6526..616aa0a0 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -58,7 +58,7 @@ export async function createProject(options: CreateProjectOptions): Promise { it('requires --yes to confirm teardown deploy when deployed state exists', async () => { // Write aws-targets.json so deploy can find the target const awsTargetsPath = join(projectDir, 'agentcore', 'aws-targets.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await writeFile( awsTargetsPath, JSON.stringify([{ name: 'default', account: '123456789012', region: 'us-east-1' }]) @@ -60,7 +60,7 @@ describe('deploy with empty agents and deployed state (teardown)', () => { const cliDir = join(projectDir, 'agentcore', '.cli'); await mkdir(cliDir, { recursive: true }); const deployedStatePath = join(cliDir, 'deployed-state.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await writeFile( deployedStatePath, JSON.stringify({ diff --git a/src/cli/commands/deploy/__tests__/deploy.test.ts b/src/cli/commands/deploy/__tests__/deploy.test.ts index 9e60dfa0..8327f7ca 100644 --- a/src/cli/commands/deploy/__tests__/deploy.test.ts +++ b/src/cli/commands/deploy/__tests__/deploy.test.ts @@ -28,7 +28,7 @@ describe('deploy without agents', () => { beforeAll(async () => { noAgentTestDir = join(tmpdir(), `agentcore-deploy-noagent-${randomUUID()}`); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await mkdir(noAgentTestDir, { recursive: true }); // Create project without any agents @@ -41,7 +41,7 @@ describe('deploy without agents', () => { // Write aws-targets.json directly (replaces old 'add target' command) const awsTargetsPath = join(noAgentProjectDir, 'agentcore', 'aws-targets.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await writeFile( awsTargetsPath, JSON.stringify([{ name: 'default', account: '123456789012', region: 'us-east-1' }]) diff --git a/src/cli/commands/remove/__tests__/remove-all.test.ts b/src/cli/commands/remove/__tests__/remove-all.test.ts index 4a006dfb..52fd5cd2 100644 --- a/src/cli/commands/remove/__tests__/remove-all.test.ts +++ b/src/cli/commands/remove/__tests__/remove-all.test.ts @@ -11,7 +11,7 @@ describe('remove all command', () => { beforeAll(async () => { testDir = join(tmpdir(), `agentcore-remove-all-${randomUUID()}`); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await mkdir(testDir, { recursive: true }); // Create project with agent @@ -53,7 +53,7 @@ describe('remove all command', () => { it('preserves aws-targets.json and deployed-state.json after remove all', async () => { // Write aws-targets.json so we can verify it's preserved const awsTargetsPath = join(projectDir, 'agentcore', 'aws-targets.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await writeFile( awsTargetsPath, JSON.stringify([{ name: 'default', account: '123456789012', region: 'us-east-1' }]) @@ -62,10 +62,10 @@ describe('remove all command', () => { // Simulate a deployed state entry so we can verify it is preserved // deployed-state.json lives in agentcore/.cli/ const cliDir = join(projectDir, 'agentcore', '.cli'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await mkdir(cliDir, { recursive: true }); const deployedStatePath = join(cliDir, 'deployed-state.json'); - // eslint-disable-next-line security/detect-non-literal-fs-filename + await writeFile( deployedStatePath, JSON.stringify({ targets: { default: { resources: { stackName: 'TestStack' } } } }) @@ -78,12 +78,12 @@ describe('remove all command', () => { expect(json.success).toBe(true); // Verify aws-targets.json is preserved (NOT reset to empty) - // eslint-disable-next-line security/detect-non-literal-fs-filename + const targetsAfter = JSON.parse(await readFile(awsTargetsPath, 'utf-8')); expect(targetsAfter.length, 'aws-targets.json should be preserved after remove all').toBe(1); // Verify deployed-state.json is preserved (NOT reset to empty) - // eslint-disable-next-line security/detect-non-literal-fs-filename + const deployedStateAfter = JSON.parse(await readFile(deployedStatePath, 'utf-8')); expect( Object.keys(deployedStateAfter.targets).length, @@ -91,7 +91,7 @@ describe('remove all command', () => { ).toBe(1); // Verify agentcore.json agents ARE cleared - // eslint-disable-next-line security/detect-non-literal-fs-filename + const schema = JSON.parse(await readFile(join(projectDir, 'agentcore', 'agentcore.json'), 'utf-8')); expect(schema.agents.length, 'Agents should be cleared after remove all').toBe(0); }); diff --git a/src/cli/commands/validate/__tests__/action.test.ts b/src/cli/commands/validate/__tests__/action.test.ts index c748662a..aa4652ff 100644 --- a/src/cli/commands/validate/__tests__/action.test.ts +++ b/src/cli/commands/validate/__tests__/action.test.ts @@ -150,7 +150,6 @@ describe('handleValidate', () => { it('formats ConfigValidationError with its message', async () => { mockFindConfigRoot.mockReturnValue('/project/agentcore'); const { ConfigValidationError } = await import('../../../../lib/index.js'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any mockReadProjectSpec.mockRejectedValue(new (ConfigValidationError as any)('field "name" is required')); const result = await handleValidate({}); diff --git a/src/cli/logging/remove-logger.ts b/src/cli/logging/remove-logger.ts index 3c61a18b..ebf97c40 100644 --- a/src/cli/logging/remove-logger.ts +++ b/src/cli/logging/remove-logger.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-object-injection, security/detect-non-literal-fs-filename */ import { CLI_LOGS_DIR, CLI_SYSTEM_DIR, CONFIG_DIR, findConfigRoot } from '../../lib'; import type { RemovalPreview } from '../operations/remove'; import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; diff --git a/src/cli/operations/agent/generate/__tests__/write-agent-to-project.test.ts b/src/cli/operations/agent/generate/__tests__/write-agent-to-project.test.ts index cea461e3..efb017d1 100644 --- a/src/cli/operations/agent/generate/__tests__/write-agent-to-project.test.ts +++ b/src/cli/operations/agent/generate/__tests__/write-agent-to-project.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-non-literal-fs-filename */ import type { GenerateConfig } from '../../../../tui/screens/generate/types.js'; import type { CredentialStrategy } from '../../../identity/create-identity.js'; import { randomUUID } from 'node:crypto'; diff --git a/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts b/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts index 5468cbd4..d699d95d 100644 --- a/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts +++ b/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts @@ -203,7 +203,6 @@ describe('createGatewayFromWizard', () => { } as Parameters[0]); expect(result.name).toBe('new-gw'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockWriteMcpSpec.mock.calls[0]![0].agentCoreGateways).toHaveLength(2); }); @@ -237,7 +236,6 @@ describe('createGatewayFromWizard', () => { }, } as Parameters[0]); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockWriteMcpSpec.mock.calls[0]![0].agentCoreGateways[0].authorizerConfiguration).toEqual({ customJwtAuthorizer: { discoveryUrl: 'https://example.com/.well-known/openid', diff --git a/src/cli/tui/components/StepProgress.tsx b/src/cli/tui/components/StepProgress.tsx index 8055f0f0..e7b7494c 100644 --- a/src/cli/tui/components/StepProgress.tsx +++ b/src/cli/tui/components/StepProgress.tsx @@ -12,10 +12,12 @@ export interface Step { info?: string; } +// eslint-disable-next-line react-refresh/only-export-components export function hasStepError(steps: Step[]): boolean { return steps.some(s => s.status === 'error'); } +// eslint-disable-next-line react-refresh/only-export-components export function areStepsComplete(steps: Step[]): boolean { if (steps.length === 0) return false; return steps.every(s => s.status === 'success' || s.status === 'error' || s.status === 'warn' || s.status === 'info'); diff --git a/src/cli/tui/context/LayoutContext.tsx b/src/cli/tui/context/LayoutContext.tsx index 114f35c1..e55a7366 100644 --- a/src/cli/tui/context/LayoutContext.tsx +++ b/src/cli/tui/context/LayoutContext.tsx @@ -13,6 +13,7 @@ const LayoutContext = createContext({ contentWidth: MAX_CONTENT_WIDTH, }); +// eslint-disable-next-line react-refresh/only-export-components export function useLayout(): LayoutContextValue { return useContext(LayoutContext); } @@ -22,6 +23,7 @@ export function useLayout(): LayoutContextValue { * The logo has fixed text " >_ AgentCore" on left and version on right, * with padding in between to fill the width. */ +// eslint-disable-next-line react-refresh/only-export-components export function buildLogo(width: number, version?: string): string { const left = '│ >_ AgentCore'; const right = version ? `v${version} │` : '│'; diff --git a/src/cli/tui/guards/project.tsx b/src/cli/tui/guards/project.tsx index 39d59432..79f2e8bb 100644 --- a/src/cli/tui/guards/project.tsx +++ b/src/cli/tui/guards/project.tsx @@ -8,6 +8,7 @@ import React from 'react'; * Check if the agentcore/ project directory exists. * Walks up from baseDir to find the agentcore directory. */ +// eslint-disable-next-line react-refresh/only-export-components export function projectExists(baseDir: string = getWorkingDirectory()): boolean { return findConfigRoot(baseDir) !== null; } @@ -17,6 +18,7 @@ export function projectExists(baseDir: string = getWorkingDirectory()): boolean * Returns the project root path if cwd is a subdirectory, or null if at the root. * Returns null if no project is found at all (use projectExists for that check). */ +// eslint-disable-next-line react-refresh/only-export-components export function getProjectRootMismatch(baseDir: string = getWorkingDirectory()): string | null { const configRoot = findConfigRoot(baseDir); if (!configRoot) { @@ -78,6 +80,7 @@ export function WrongDirectoryMessage({ projectRoot }: { projectRoot: string }) * * @param inTui - If true, shows "create" instead of "agentcore create" */ +// eslint-disable-next-line react-refresh/only-export-components export function requireProject(inTui = false): void { const cwd = getWorkingDirectory(); const configRoot = findConfigRoot(cwd); diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index 569a6a1b..2bb3b8ed 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -167,6 +167,7 @@ export function useDevServer(options: { workingDir: string; port: number; agentN config?.module, config?.directory, config?.isPython, + options.workingDir, targetPort, restartTrigger, envVars, diff --git a/src/cli/tui/hooks/useProject.ts b/src/cli/tui/hooks/useProject.ts index 752563ed..20a51af0 100644 --- a/src/cli/tui/hooks/useProject.ts +++ b/src/cli/tui/hooks/useProject.ts @@ -32,6 +32,7 @@ export interface UseProjectResult { * } */ export function useProject(): UseProjectResult { + // eslint-disable-next-line react-hooks/preserve-manual-memoization -- intentionally empty deps; findConfigRoot() result is stable for the process lifetime return useMemo(() => { const configRoot = findConfigRoot(); diff --git a/src/cli/tui/screens/add/AddSuccessScreen.tsx b/src/cli/tui/screens/add/AddSuccessScreen.tsx index 081db03b..50db3a19 100644 --- a/src/cli/tui/screens/add/AddSuccessScreen.tsx +++ b/src/cli/tui/screens/add/AddSuccessScreen.tsx @@ -4,6 +4,7 @@ import type { ReactNode } from 'react'; import React from 'react'; /** Next steps shown after successfully adding a resource */ +// eslint-disable-next-line react-refresh/only-export-components export function getAddSuccessSteps(showDevOption: boolean): NextStep[] { if (showDevOption) { return [ diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index 50cc3337..c88dd57d 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -293,6 +293,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState persistDeployedState, switchableIoHost, context?.isTeardownDeploy, + context?.awsTargets, ]); // Finalize logger and dispose toolkit when preflight fails diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index a80753f5..e405245d 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -150,6 +150,7 @@ export function GenerateWizardUI({ /** * Returns the appropriate help text for the current wizard step. */ +// eslint-disable-next-line react-refresh/only-export-components export function getWizardHelpText(step: GenerateStep): string { if (step === 'confirm') return 'Enter/Y confirm · Esc back'; if (step === 'projectName') return 'Enter submit · Esc cancel'; diff --git a/src/lib/packaging/__tests__/helpers.test.ts b/src/lib/packaging/__tests__/helpers.test.ts index 0958f075..b02e7ed0 100644 --- a/src/lib/packaging/__tests__/helpers.test.ts +++ b/src/lib/packaging/__tests__/helpers.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-non-literal-fs-filename */ import { MAX_ZIP_SIZE_BYTES, convertWindowsScriptsToLinux, diff --git a/src/lib/packaging/python.ts b/src/lib/packaging/python.ts index 2dd6734f..c2efd8df 100644 --- a/src/lib/packaging/python.ts +++ b/src/lib/packaging/python.ts @@ -23,6 +23,7 @@ import type { ArtifactResult, CodeZipPackager, PackageOptions, RuntimePackager } import { detectUnavailablePlatform } from './uv'; import { join } from 'path'; +// eslint-disable-next-line security/detect-unsafe-regex -- bounded input from RuntimeVersion enum, not user input const PYTHON_RUNTIME_REGEX = /PYTHON_(\d+)_?(\d+)?/; /** diff --git a/src/lib/schemas/io/__tests__/config-io.test.ts b/src/lib/schemas/io/__tests__/config-io.test.ts index ead1f46f..1a65a076 100644 --- a/src/lib/schemas/io/__tests__/config-io.test.ts +++ b/src/lib/schemas/io/__tests__/config-io.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-non-literal-fs-filename */ import { ConfigNotFoundError, ConfigParseError, ConfigValidationError } from '../../../errors/config.js'; import { ConfigIO } from '../config-io.js'; import { NoProjectError } from '../path-resolver.js'; diff --git a/src/lib/utils/__tests__/credentials.test.ts b/src/lib/utils/__tests__/credentials.test.ts index 2b58b18f..2b0e468d 100644 --- a/src/lib/utils/__tests__/credentials.test.ts +++ b/src/lib/utils/__tests__/credentials.test.ts @@ -162,7 +162,6 @@ describe('SecureCredentials', () => { it('Node.js inspect is safe', () => { const creds = new SecureCredentials({ SECRET: 'mypassword' }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const inspectFn = (creds as any)[Symbol.for('nodejs.util.inspect.custom')] as () => string; const str = inspectFn.call(creds); diff --git a/src/schema/schemas/agent-env.ts b/src/schema/schemas/agent-env.ts index 9f01a8e0..cc1d7052 100644 --- a/src/schema/schemas/agent-env.ts +++ b/src/schema/schemas/agent-env.ts @@ -41,7 +41,11 @@ export const GatewayNameSchema = z .string() .min(1) .max(100) - .regex(/^([0-9a-zA-Z][-]?){1,100}$/, 'Gateway name must be alphanumeric with optional hyphens (max 100 chars)'); + .regex( + // eslint-disable-next-line security/detect-unsafe-regex -- input bounded to 100 chars by .max(100) above + /^[0-9a-zA-Z](?:[0-9a-zA-Z-]*[0-9a-zA-Z])?$/, + 'Gateway name must be alphanumeric with optional hyphens (max 100 chars)' + ); // ============================================================================ // Common Types @@ -73,6 +77,7 @@ export const EntrypointSchema = z .string() .min(1) .regex( + // eslint-disable-next-line security/detect-unsafe-regex -- character class quantifiers don't cause backtracking /^[a-zA-Z0-9_][a-zA-Z0-9_/.-]*\.(py|ts|js)(:[a-zA-Z_][a-zA-Z0-9_]*)?$/, 'Must be a Python (.py) or TypeScript (.ts/.js) file path with optional handler (e.g., "main.py:handler" or "index.ts")' ) as unknown as z.ZodType; diff --git a/src/schema/schemas/mcp.ts b/src/schema/schemas/mcp.ts index 61eb05a6..e3890df1 100644 --- a/src/schema/schemas/mcp.ts +++ b/src/schema/schemas/mcp.ts @@ -138,6 +138,7 @@ const PythonEntrypointSchema = z .string() .min(1) .regex( + // eslint-disable-next-line security/detect-unsafe-regex -- character class quantifiers don't cause backtracking /^[a-zA-Z0-9_][a-zA-Z0-9_/.-]*\.py(:[a-zA-Z_][a-zA-Z0-9_]*)?$/, 'Must be a Python file path with optional handler (e.g., "main.py:agent" or "src/handler.py:app")' ) as unknown as z.ZodType; diff --git a/src/test-utils/config-reader.ts b/src/test-utils/config-reader.ts index 41cffd11..5d1aa936 100644 --- a/src/test-utils/config-reader.ts +++ b/src/test-utils/config-reader.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-non-literal-fs-filename */ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; diff --git a/src/test-utils/project-factory.ts b/src/test-utils/project-factory.ts index 66033e25..a57ac566 100644 --- a/src/test-utils/project-factory.ts +++ b/src/test-utils/project-factory.ts @@ -1,4 +1,3 @@ -/* eslint-disable security/detect-non-literal-fs-filename */ import { runCLI } from './cli-runner.js'; import { randomUUID } from 'node:crypto'; import { mkdir, rm } from 'node:fs/promises';