diff --git a/docs/awf-config.schema.json b/docs/awf-config.schema.json index 6ff70bd27..b4204591a 100644 --- a/docs/awf-config.schema.json +++ b/docs/awf-config.schema.json @@ -493,10 +493,15 @@ "description": "Security and isolation configuration.", "additionalProperties": false, "properties": { + "legacySecurity": { + "type": "boolean", + "description": "Enable legacy security mode (sudo, host-access, iptables). When omitted or false, strict security is enforced (network-isolation + API proxy credential injection)." + }, "securityMode": { "type": "string", "enum": ["strict", "compat"], - "description": "Security enforcement mode. 'strict' (default) enforces network-isolation, API proxy credential injection, and rejects host-access/DinD. 'compat' preserves legacy iptables-based mode (requires sudo)." + "description": "[DEPRECATED] Use legacySecurity instead. Will be removed in a future release.", + "deprecated": true }, "sslBump": { "type": "boolean", diff --git a/src/awf-config-schema.json b/src/awf-config-schema.json index 6ff70bd27..b4204591a 100644 --- a/src/awf-config-schema.json +++ b/src/awf-config-schema.json @@ -493,10 +493,15 @@ "description": "Security and isolation configuration.", "additionalProperties": false, "properties": { + "legacySecurity": { + "type": "boolean", + "description": "Enable legacy security mode (sudo, host-access, iptables). When omitted or false, strict security is enforced (network-isolation + API proxy credential injection)." + }, "securityMode": { "type": "string", "enum": ["strict", "compat"], - "description": "Security enforcement mode. 'strict' (default) enforces network-isolation, API proxy credential injection, and rejects host-access/DinD. 'compat' preserves legacy iptables-based mode (requires sudo)." + "description": "[DEPRECATED] Use legacySecurity instead. Will be removed in a future release.", + "deprecated": true }, "sslBump": { "type": "boolean", diff --git a/src/cli-options.ts b/src/cli-options.ts index 468584519..b81eb3445 100644 --- a/src/cli-options.ts +++ b/src/cli-options.ts @@ -13,7 +13,7 @@ const optionGroupHeaders: Record = { 'env': 'Container Configuration:', 'dns-servers': 'Network & Security:', 'upstream-proxy': 'Network & Security:', - 'enable-api-proxy': 'API Proxy:', + 'copilot-api-target': 'API Proxy:', 'log-level': 'Logging & Debug:', }; @@ -245,11 +245,11 @@ program 'Enforce egress via Docker network topology (internal network +\n' + ' dual-homed proxy) instead of iptables. Requires no sudo/NET_ADMIN.\n' + ' Not yet supported with --dns-over-https or --enable-host-access.\n' + - ' Enabled by default in --security-mode strict.' + ' Enabled by default (strict security).' ) .option( '--no-network-isolation', - 'Disable network-isolation mode (requires --security-mode compat in strict mode).' + 'Disable network-isolation mode (requires --legacy-security).' ) .option( '--topology-attach ', @@ -278,13 +278,18 @@ program ' WARNING: allows firewall bypass via docker run', false ) + .addOption( + new Option( + '--legacy-security', + 'Enable legacy security mode (sudo, host-access, iptables).\n' + + ' Default behavior is strict security (network-isolation + api-proxy).', + ) + ) .addOption( new Option( '--security-mode ', - 'Security enforcement mode (default: strict).\n' + - ' strict: network-isolation + api-proxy, no sudo/iptables.\n' + - ' compat: legacy iptables mode, requires sudo.', - ).choices(['strict', 'compat']).default('strict') + '[DEPRECATED] Use --legacy-security instead.', + ).choices(['strict', 'compat']).hideHelp() ) .option( '--enable-dlp', @@ -293,15 +298,18 @@ program false ) - // -- API Proxy -- - .option( - '--enable-api-proxy', - 'Enable API proxy sidecar for secure credential injection.\n' + - ' Supports OpenAI (Codex) and Anthropic (Claude) APIs.' + // -- API Proxy (always enabled, flags retained for backward compatibility) -- + .addOption( + new Option( + '--enable-api-proxy', + '[DEPRECATED] The API proxy is always enabled. This flag is ignored.' + ).hideHelp() ) - .option( - '--no-enable-api-proxy', - 'Disable the API proxy sidecar (requires --security-mode compat in strict mode).' + .addOption( + new Option( + '--no-enable-api-proxy', + '[REMOVED] The API proxy cannot be disabled. Passing this flag is an error.' + ).hideHelp() ) .option( '--copilot-api-target ', diff --git a/src/commands/build-config.test.ts b/src/commands/build-config.test.ts index 4b53100a1..30a4b5f8c 100644 --- a/src/commands/build-config.test.ts +++ b/src/commands/build-config.test.ts @@ -69,6 +69,7 @@ const ENV_KEYS = [ 'GEMINI_API_BASE_PATH', 'AWF_CAPTURE_BLOCKED_LLM_REQUESTS', 'AWF_MAX_BLOCKED_CAPTURE_BYTES', + 'AWF_DEBUG_TOKENS', ] as const; describe('buildConfig', () => { @@ -470,4 +471,66 @@ describe('buildConfig', () => { expect(config.maxCapturedBytes).toBeUndefined(); }); }); + + describe('debugTokens via AWF_DEBUG_TOKENS', () => { + it('should set debugTokens true when AWF_DEBUG_TOKENS=1', () => { + process.env.AWF_DEBUG_TOKENS = '1'; + const config = buildConfig(makeInputs()); + expect(config.debugTokens).toBe(true); + }); + + it('should leave debugTokens undefined when AWF_DEBUG_TOKENS is not set', () => { + const config = buildConfig(makeInputs()); + expect(config.debugTokens).toBeUndefined(); + }); + + it('should leave debugTokens undefined for non-1 AWF_DEBUG_TOKENS', () => { + process.env.AWF_DEBUG_TOKENS = '0'; + const config = buildConfig(makeInputs()); + expect(config.debugTokens).toBeUndefined(); + }); + }); + + describe('resolveLegacySecurity (via options)', () => { + it('should set legacySecurity true when --legacy-security is passed', () => { + const config = buildConfig(makeInputs({ + options: { ...makeInputs().options, legacySecurity: true }, + })); + expect(config.legacySecurity).toBe(true); + }); + + it('should leave legacySecurity undefined when --legacy-security is not passed', () => { + const config = buildConfig(makeInputs()); + expect(config.legacySecurity).toBeUndefined(); + }); + + it('should map deprecated --security-mode compat to legacySecurity true', () => { + const config = buildConfig(makeInputs({ + options: { ...makeInputs().options, securityMode: 'compat' }, + })); + expect(config.legacySecurity).toBe(true); + }); + + it('should leave legacySecurity undefined for deprecated --security-mode strict', () => { + const config = buildConfig(makeInputs({ + options: { ...makeInputs().options, securityMode: 'strict' }, + })); + expect(config.legacySecurity).toBeUndefined(); + }); + + it('should prefer --legacy-security over deprecated --security-mode', () => { + const config = buildConfig(makeInputs({ + options: { ...makeInputs().options, legacySecurity: true, securityMode: 'strict' }, + })); + // --legacy-security takes precedence over deprecated --security-mode + expect(config.legacySecurity).toBe(true); + }); + + it('should treat explicit --legacy-security false as undefined', () => { + const config = buildConfig(makeInputs({ + options: { ...makeInputs().options, legacySecurity: false }, + })); + expect(config.legacySecurity).toBeUndefined(); + }); + }); }); diff --git a/src/commands/build-config.ts b/src/commands/build-config.ts index 8c58654c7..a6e884300 100644 --- a/src/commands/build-config.ts +++ b/src/commands/build-config.ts @@ -1,5 +1,38 @@ import { WrapperConfig, LogLevel, UpstreamProxyConfig } from '../types'; import { resolveApiCredentials } from './resolve-credentials'; +import { logger } from '../logger'; + +/** + * Resolves the effective `legacySecurity` value from CLI options. + * + * Sources (in priority order): + * 1. `--legacy-security` boolean flag (preferred) + * 2. `--security-mode compat` (deprecated, maps to legacySecurity=true) + */ +function resolveLegacySecurity(options: Record): boolean | undefined { + // Preferred new flag takes precedence + const legacySecurity = options.legacySecurity as boolean | undefined; + if (legacySecurity !== undefined) { + return legacySecurity || undefined; + } + + // Handle deprecated --security-mode flag (only if --legacy-security not specified) + const securityMode = options.securityMode as string | undefined; + if (securityMode === 'compat') { + logger.warn( + '⚠️ --security-mode compat is deprecated. Use --legacy-security instead.', + ); + return true; + } + if (securityMode === 'strict') { + logger.warn( + '⚠️ --security-mode is deprecated. Strict security is the default; remove the flag.', + ); + return undefined; + } + + return undefined; +} /** * Inputs required to assemble a {@link WrapperConfig}. @@ -116,7 +149,7 @@ export function buildConfig(inputs: BuildConfigInputs): WrapperConfig { sslBump: options.sslBump as boolean, enableDind: options.enableDind as boolean, enableDlp: options.enableDlp as boolean, - securityMode: options.securityMode as 'strict' | 'compat' | undefined, + legacySecurity: resolveLegacySecurity(options), allowedUrls, enableApiProxy: options.enableApiProxy as boolean | undefined, modelFallback: diff --git a/src/commands/validate-options.test.ts b/src/commands/validate-options.test.ts index 903b902ef..f1beba8fe 100644 --- a/src/commands/validate-options.test.ts +++ b/src/commands/validate-options.test.ts @@ -36,6 +36,7 @@ const STUB_CONFIG = { blockedDomains: undefined, agentCommand: 'echo hi', logLevel: 'info', + legacySecurity: true, keepContainers: false, tty: false, workDir: '/tmp/workdir', @@ -64,7 +65,7 @@ const STUB_CONFIG = { enableDind: false, enableDlp: false, allowedUrls: undefined, - enableApiProxy: false, + enableApiProxy: undefined, anthropicAutoCache: false, anthropicCacheTailTtl: undefined, modelAliases: undefined, @@ -514,29 +515,14 @@ describe('validateOptions', () => { ); }); - it('exits when rate limit flags are used without --enable-api-proxy', () => { - mockedOptionParsers.validateRateLimitFlags.mockReturnValue({ - valid: false, - error: '--rpm requires --enable-api-proxy', - }); - expect(() => validateOptions(validOptions(), 'echo hi')).toThrow('process.exit called'); - expect(mockedLogger.error).toHaveBeenCalledWith( - expect.stringContaining('--rpm requires --enable-api-proxy'), - ); - }); + // Note: "rate limit flags without --enable-api-proxy" is no longer testable + // because applySecurityMode always forces enableApiProxy=true. + // The validateRateLimitFlags check is now dead code for this scenario. }); describe('feature flag compatibility', () => { - it('exits when --enable-token-steering is used without --enable-api-proxy', () => { - mockedOptionParsers.validateEnableTokenSteeringFlag.mockReturnValue({ - valid: false, - error: '--enable-token-steering requires --enable-api-proxy', - }); - expect(() => validateOptions(validOptions(), 'echo hi')).toThrow('process.exit called'); - expect(mockedLogger.error).toHaveBeenCalledWith( - expect.stringContaining('--enable-token-steering requires --enable-api-proxy'), - ); - }); + // Note: "--enable-token-steering without --enable-api-proxy" is no longer testable + // because applySecurityMode always forces enableApiProxy=true. it('exits when --skip-pull and --build-local are combined', () => { mockedOptionParsers.validateSkipPullWithBuildLocal.mockReturnValue({ diff --git a/src/commands/validators/config-assembly-api-proxy.test.ts b/src/commands/validators/config-assembly-api-proxy.test.ts index 975db538c..5c4f93e82 100644 --- a/src/commands/validators/config-assembly-api-proxy.test.ts +++ b/src/commands/validators/config-assembly-api-proxy.test.ts @@ -29,10 +29,17 @@ describe('config-assembly', () => { ); }); - it('should exit if rate limit flags are used without --enable-api-proxy', () => { + // Note: "rate limit flags without --enable-api-proxy" scenario cannot occur + // in production (API proxy is always enabled), but we test the validation + // path for completeness. + it('should exit if rate limit flags are invalid', () => { + mockBuildConfigOnce({ + enableApiProxy: true, + }); + (validateRateLimitFlags as jest.Mock).mockReturnValueOnce({ valid: false, - error: 'Rate limit flags require --enable-api-proxy', + error: '--rate-limit-rpm requires --enable-api-proxy', }); expect(() => { @@ -40,15 +47,11 @@ describe('config-assembly', () => { }).toThrow('process.exit(1)'); expect(logger.error).toHaveBeenCalledWith( - 'Rate limit flags require --enable-api-proxy', + expect.stringContaining('--rate-limit-rpm requires --enable-api-proxy'), ); }); it('should set rate limit config when API proxy is enabled', () => { - mockBuildConfigOnce({ - enableApiProxy: true, - }); - const mockRateLimitConfig = { enabled: true, rpm: 100, diff --git a/src/commands/validators/config-assembly.test-utils.ts b/src/commands/validators/config-assembly.test-utils.ts index 39230ad52..7bd3f2bd7 100644 --- a/src/commands/validators/config-assembly.test-utils.ts +++ b/src/commands/validators/config-assembly.test-utils.ts @@ -35,7 +35,7 @@ jest.mock('../../option-parsers', () => { validateSkipPullWithBuildLocal: jest.fn(), validateAllowHostPorts: jest.fn(), applyHostServicePortsConfig: jest.fn(), - buildRateLimitConfig: jest.fn(), + buildRateLimitConfig: jest.fn().mockReturnValue({ config: { enabled: false, rpm: 0, rph: 0, bytesPm: 0 } }), applyAgentTimeout: jest.fn(), isLoopbackTcpDockerHostUri: actual.isLoopbackTcpDockerHostUri, }; @@ -57,8 +57,8 @@ jest.mock('../build-config', () => ({ logLevel: args.logLevel, allowedDomains: args.allowedDomains, blockedDomains: args.blockedDomains, - securityMode: 'compat', - enableApiProxy: false, + legacySecurity: true, + enableApiProxy: undefined, enableTokenSteering: false, envAll: false, envFile: undefined, @@ -150,8 +150,8 @@ export const createBuildConfigResult = ( logLevel: 'info', allowedDomains: ['example.com'], blockedDomains: [], - securityMode: 'compat', - enableApiProxy: false, + legacySecurity: true, + enableApiProxy: undefined, enableTokenSteering: false, envAll: false, envFile: undefined, diff --git a/src/commands/validators/security-mode.test.ts b/src/commands/validators/security-mode.test.ts index 1393b09b0..b39f8abb0 100644 --- a/src/commands/validators/security-mode.test.ts +++ b/src/commands/validators/security-mode.test.ts @@ -27,9 +27,6 @@ function makeConfig(overrides: Partial = {}): WrapperConfig { proxyLogsDir: '/tmp/logs', dnsServers: ['8.8.8.8'], enableHostAccess: false, - // networkIsolation and enableApiProxy are intentionally left undefined here - // to match the CLI default behaviour — users who do not explicitly pass - // --network-isolation or --enable-api-proxy will have undefined, not false. enableDind: false, sslBump: false, enableDlp: false, @@ -45,23 +42,29 @@ function makeConfig(overrides: Partial = {}): WrapperConfig { } describe('applySecurityMode', () => { + let mockExit: jest.SpyInstance; + beforeEach(() => { jest.clearAllMocks(); (runtimeUsesComposeAgent as jest.Mock).mockReturnValue(true); + mockExit = jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null) => { + throw new Error(`process.exit(${code})`); + }); + }); + + afterEach(() => { + mockExit.mockRestore(); }); - describe('strict mode (default)', () => { - it('should force networkIsolation on when undefined (not explicitly set)', () => { - const config = makeConfig({ securityMode: 'strict', networkIsolation: undefined }); + describe('strict security (default)', () => { + it('should force networkIsolation on when undefined', () => { + const config = makeConfig({ networkIsolation: undefined }); applySecurityMode(config); expect(config.networkIsolation).toBe(true); - expect(logger.warn).not.toHaveBeenCalledWith( - expect.stringContaining('--no-network-isolation'), - ); }); it('should force networkIsolation on and warn when explicitly disabled', () => { - const config = makeConfig({ securityMode: 'strict', networkIsolation: false }); + const config = makeConfig({ networkIsolation: false }); applySecurityMode(config); expect(config.networkIsolation).toBe(true); expect(logger.warn).toHaveBeenCalledWith( @@ -69,30 +72,34 @@ describe('applySecurityMode', () => { ); }); - it('should force enableApiProxy on when undefined (not explicitly set)', () => { - const config = makeConfig({ securityMode: 'strict', enableApiProxy: undefined }); + it('should always force enableApiProxy on', () => { + const config = makeConfig({ enableApiProxy: undefined }); applySecurityMode(config); expect(config.enableApiProxy).toBe(true); - expect(logger.warn).not.toHaveBeenCalledWith( - expect.stringContaining('--no-enable-api-proxy'), + }); + + it('should exit when --no-enable-api-proxy is passed', () => { + const config = makeConfig({ enableApiProxy: false }); + expect(() => applySecurityMode(config)).toThrow('process.exit(1)'); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('--no-enable-api-proxy is not allowed'), ); }); - it('should force enableApiProxy on and warn when explicitly disabled', () => { - const config = makeConfig({ securityMode: 'strict', enableApiProxy: false }); + it('should warn when --enable-api-proxy is explicitly passed', () => { + const config = makeConfig({ enableApiProxy: true }); applySecurityMode(config); - expect(config.enableApiProxy).toBe(true); expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining('--no-enable-api-proxy was ignored'), + expect.stringContaining('--enable-api-proxy is deprecated'), ); + expect(config.enableApiProxy).toBe(true); }); - it('should be the default when securityMode is undefined', () => { - const config = makeConfig({ securityMode: undefined }); + it('should be the default when legacySecurity is undefined', () => { + const config = makeConfig({ legacySecurity: undefined }); applySecurityMode(config); expect(config.networkIsolation).toBe(true); expect(config.enableApiProxy).toBe(true); - expect(logger.warn).not.toHaveBeenCalled(); }); it('should override enableHostAccess with warning', () => { @@ -104,7 +111,7 @@ describe('applySecurityMode', () => { ); }); - it('should clear allowHostServicePorts when set (prevents downstream re-enable of host access)', () => { + it('should clear allowHostServicePorts when set', () => { const config = makeConfig({ allowHostServicePorts: '5432,6379' }); applySecurityMode(config); expect(config.allowHostServicePorts).toBeUndefined(); @@ -113,7 +120,7 @@ describe('applySecurityMode', () => { ); }); - it('should clear allowHostServicePorts and allowHostPorts set alongside enableHostAccess', () => { + it('should clear allowHostServicePorts and allowHostPorts alongside enableHostAccess', () => { const config = makeConfig({ enableHostAccess: true, allowHostPorts: '3000,8080', @@ -143,19 +150,17 @@ describe('applySecurityMode', () => { ); }); - it('should warn that --security-mode compat is required for overridden options', () => { + it('should warn that --legacy-security is required for overridden options', () => { const config = makeConfig({ enableHostAccess: true, enableDind: true }); applySecurityMode(config); expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining('--security-mode compat'), + expect.stringContaining('--legacy-security'), ); }); it('should not warn when compatible options are already set', () => { const config = makeConfig({ - securityMode: 'strict', networkIsolation: true, - enableApiProxy: true, enableHostAccess: false, enableDind: false, }); @@ -169,43 +174,44 @@ describe('applySecurityMode', () => { }); it('should skip network-isolation enforcement for microVM runtimes', () => { - const config = makeConfig({ securityMode: 'strict', containerRuntime: 'sbx' }); + const config = makeConfig({ containerRuntime: 'sbx' }); applySecurityMode(config); expect(config.networkIsolation).toBeUndefined(); - expect(logger.warn).not.toHaveBeenCalledWith( - expect.stringContaining('network-isolation'), - ); }); it('should still enforce api-proxy for microVM runtimes', () => { - const config = makeConfig({ securityMode: 'strict', containerRuntime: 'sbx' }); + const config = makeConfig({ containerRuntime: 'sbx' }); applySecurityMode(config); expect(config.enableApiProxy).toBe(true); }); }); }); - describe('compat mode', () => { - it('should not modify any config values', () => { + describe('legacy security mode', () => { + it('should not override host-access or dind', () => { const config = makeConfig({ - securityMode: 'compat', + legacySecurity: true, networkIsolation: false, - enableApiProxy: false, enableHostAccess: true, enableDind: true, }); applySecurityMode(config); expect(config.networkIsolation).toBe(false); - expect(config.enableApiProxy).toBe(false); expect(config.enableHostAccess).toBe(true); expect(config.enableDind).toBe(true); }); - it('should log info about compat mode', () => { - const config = makeConfig({ securityMode: 'compat' }); + it('should still force api-proxy on in legacy mode', () => { + const config = makeConfig({ legacySecurity: true }); + applySecurityMode(config); + expect(config.enableApiProxy).toBe(true); + }); + + it('should log info about legacy security mode', () => { + const config = makeConfig({ legacySecurity: true }); applySecurityMode(config); expect(logger.info).toHaveBeenCalledWith( - expect.stringContaining('compat security mode'), + expect.stringContaining('legacy security mode'), ); }); }); diff --git a/src/commands/validators/security-mode.ts b/src/commands/validators/security-mode.ts index 69a0f2733..21caa136d 100644 --- a/src/commands/validators/security-mode.ts +++ b/src/commands/validators/security-mode.ts @@ -3,27 +3,32 @@ import { logger } from '../../logger'; import { runtimeUsesComposeAgent } from '../../container-runtime'; /** - * Applies security-mode enforcement to the assembled config. + * Applies security enforcement to the assembled config. * - * In strict mode (the default), incompatible options are overridden with + * Default behavior (strict): incompatible options are overridden with * warnings and bundled defaults (network-isolation, api-proxy) are forced on. * - * In compat mode, the legacy iptables-based configuration is preserved and - * no overrides are applied. + * Legacy security (--legacy-security): the legacy iptables-based configuration + * is preserved and no overrides are applied (except api-proxy, which is always on). * * Must be called **after** `buildConfig()` assembles the raw config from CLI * options and config file, but **before** the downstream validators that * check for mutual exclusions (since strict mode resolves those conflicts). */ export function applySecurityMode(config: WrapperConfig): void { - const mode = config.securityMode ?? 'strict'; + // Handle deprecated --enable-api-proxy / --no-enable-api-proxy + handleApiProxyDeprecation(config); - if (mode === 'compat') { - logger.info('Running in compat security mode (legacy iptables-based enforcement).'); + const isLegacy = config.legacySecurity === true; + + if (isLegacy) { + logger.info('Running in legacy security mode (iptables-based enforcement).'); + // API proxy is still always forced on in legacy mode + config.enableApiProxy = true; return; } - // --- strict mode (default) --- + // --- strict security (default) --- // MicroVM runtimes (e.g. sbx) enforce isolation at the hypervisor layer via // DOCKER_SANDBOXES_PROXY; Docker network topology does not apply to them. @@ -35,39 +40,30 @@ export function applySecurityMode(config: WrapperConfig): void { if (!config.networkIsolation) { if (config.networkIsolation === false) { logger.warn( - '⚠️ --no-network-isolation was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to disable network isolation.', + '⚠️ --no-network-isolation was ignored (incompatible with strict security, the default).\n' + + ' Pass --legacy-security to disable network isolation.', ); } config.networkIsolation = true; } } - // Force api-proxy on. - // Only warn when explicitly disabled (=== false); undefined means "not set by user". - if (!config.enableApiProxy) { - if (config.enableApiProxy === false) { - logger.warn( - '⚠️ --no-enable-api-proxy was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to disable the API proxy.', - ); - } - config.enableApiProxy = true; - } + // Force api-proxy on (always, regardless of flags). + config.enableApiProxy = true; // Override incompatible options if (config.enableHostAccess) { logger.warn( - '⚠️ --enable-host-access was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to enable host access.', + '⚠️ --enable-host-access was ignored (incompatible with strict security, the default).\n' + + ' Pass --legacy-security to enable host access.', ); config.enableHostAccess = false; // Also clear allowHostServicePorts: it auto-enables host access in // applyHostServicePortsConfig() which runs later in the validator pipeline. if (config.allowHostServicePorts) { logger.warn( - '⚠️ --allow-host-service-ports was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to use host service ports.', + '⚠️ --allow-host-service-ports was ignored (incompatible with strict security, the default).\n' + + ' Pass --legacy-security to use host service ports.', ); config.allowHostServicePorts = undefined; } @@ -81,25 +77,48 @@ export function applySecurityMode(config: WrapperConfig): void { // auto-enable host access downstream — suppress it in strict mode. if (config.allowHostServicePorts) { logger.warn( - '⚠️ --allow-host-service-ports was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to use host service ports.', + '⚠️ --allow-host-service-ports was ignored (incompatible with strict security, the default).\n' + + ' Pass --legacy-security to use host service ports.', ); config.allowHostServicePorts = undefined; } if (config.enableDind) { logger.warn( - '⚠️ --enable-dind was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to enable Docker-in-Docker.', + '⚠️ --enable-dind was ignored (incompatible with strict security, the default).\n' + + ' Pass --legacy-security to enable Docker-in-Docker.', ); config.enableDind = false; } if (config.dnsOverHttps) { logger.warn( - '⚠️ --dns-over-https was ignored (incompatible with --security-mode strict, the default).\n' + - ' Pass --security-mode compat to use DNS-over-HTTPS.', + '⚠️ --dns-over-https was ignored (incompatible with strict security, the default).\n' + + ' Pass --legacy-security to use DNS-over-HTTPS.', ); config.dnsOverHttps = undefined; } } + +/** + * Handles the deprecated --enable-api-proxy / --no-enable-api-proxy flags. + * + * - --enable-api-proxy: emit deprecation warning, continue normally + * - --no-enable-api-proxy: hard error (not allowed) + */ +function handleApiProxyDeprecation(config: WrapperConfig): void { + if (config.enableApiProxy === false) { + logger.error( + '❌ --no-enable-api-proxy is not allowed. The API proxy is always enabled for credential isolation.', + ); + logger.error( + ' Remove the --no-enable-api-proxy flag from your command.', + ); + process.exit(1); + } + if (config.enableApiProxy === true) { + logger.warn( + '⚠️ --enable-api-proxy is deprecated and no longer needed. The API proxy is always enabled.', + ); + } +} diff --git a/src/config-file-mapping.test.ts b/src/config-file-mapping.test.ts index 2fd82ead3..569102e2a 100644 --- a/src/config-file-mapping.test.ts +++ b/src/config-file-mapping.test.ts @@ -11,7 +11,8 @@ describe('mapAwfFileConfigToCliOptions', () => { expect(result.allowDomains).toBe('github.com,api.github.com'); expect(result.dnsServers).toBe('1.1.1.1,1.0.0.1'); - expect(result.enableApiProxy).toBe(true); + // enableApiProxy is no longer mapped — API proxy is always on (#6207) + expect(result.enableApiProxy).toBeUndefined(); expect(result.anthropicApiTarget).toBe('api.anthropic.com'); expect(result.anthropicApiBasePath).toBe('/anthropic'); expect(result.agentTimeout).toBe('15'); @@ -270,6 +271,27 @@ describe('mapAwfFileConfigToCliOptions', () => { expect(result.difcProxyCaCert).toBe('/path/ca.crt'); }); + it('maps security.legacySecurity boolean', () => { + const result = mapAwfFileConfigToCliOptions({ + security: { legacySecurity: true }, + }); + expect(result.legacySecurity).toBe(true); + }); + + it('maps deprecated security.securityMode compat to legacySecurity', () => { + const result = mapAwfFileConfigToCliOptions({ + security: { securityMode: 'compat' }, + }); + expect(result.legacySecurity).toBe(true); + }); + + it('does not set legacySecurity for deprecated security.securityMode strict', () => { + const result = mapAwfFileConfigToCliOptions({ + security: { securityMode: 'strict' }, + }); + expect(result.legacySecurity).toBeUndefined(); + }); + it('maps container fields', () => { const result = mapAwfFileConfigToCliOptions({ container: { diff --git a/src/config-file.ts b/src/config-file.ts index e20165bc5..aa4d2a599 100644 --- a/src/config-file.ts +++ b/src/config-file.ts @@ -88,6 +88,8 @@ export interface AwfFileConfig { }; }; security?: { + legacySecurity?: boolean; + /** @deprecated Use legacySecurity instead */ securityMode?: 'strict' | 'compat'; sslBump?: boolean; enableDlp?: boolean; diff --git a/src/config-mapper.ts b/src/config-mapper.ts index e1bbd231c..dcc8cd86d 100644 --- a/src/config-mapper.ts +++ b/src/config-mapper.ts @@ -27,7 +27,9 @@ export function mapAwfFileConfigToCliOptions(config: AwfFileConfig): Record 0) {