fix(#338): Block YOLO mode in production
- Add isProductionEnvironment() check to prevent YOLO mode bypass - Log warning when YOLO mode request is blocked in production - Fall back to process.env.NODE_ENV when config service returns undefined - Add comprehensive tests for production blocking behavior SECURITY: YOLO mode bypasses all quality gates which is dangerous in production environments. This change ensures quality gates are always enforced when NODE_ENV=production. Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1288,5 +1288,222 @@ describe("QualityGatesService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("YOLO mode blocked in production (SEC-ORCH-13)", () => {
|
||||
const params = {
|
||||
taskId: "task-prod-123",
|
||||
agentId: "agent-prod-456",
|
||||
files: ["src/feature.ts"],
|
||||
diffSummary: "Production deployment",
|
||||
};
|
||||
|
||||
it("should block YOLO mode when NODE_ENV is production", async () => {
|
||||
// Enable YOLO mode but set production environment
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return "production";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const mockResponse: QualityCheckResponse = {
|
||||
approved: true,
|
||||
gate: "pre-commit",
|
||||
message: "All checks passed",
|
||||
};
|
||||
|
||||
vi.mocked(mockCoordinatorClient.checkQuality).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await service.preCommitCheck(params);
|
||||
|
||||
// Should call coordinator (YOLO mode blocked in production)
|
||||
expect(mockCoordinatorClient.checkQuality).toHaveBeenCalled();
|
||||
|
||||
// Should return coordinator response, not YOLO bypass
|
||||
expect(result.approved).toBe(true);
|
||||
expect(result.message).toBe("All checks passed");
|
||||
expect(result.details?.yoloMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should log warning when YOLO mode is blocked in production", async () => {
|
||||
// Enable YOLO mode but set production environment
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return "production";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const loggerWarnSpy = vi.spyOn(service["logger"], "warn");
|
||||
|
||||
const mockResponse: QualityCheckResponse = {
|
||||
approved: true,
|
||||
gate: "pre-commit",
|
||||
};
|
||||
|
||||
vi.mocked(mockCoordinatorClient.checkQuality).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await service.preCommitCheck(params);
|
||||
|
||||
// Should log warning about YOLO mode being blocked
|
||||
expect(loggerWarnSpy).toHaveBeenCalledWith(
|
||||
"YOLO mode blocked in production environment - quality gates will be enforced",
|
||||
expect.objectContaining({
|
||||
requestedYoloMode: true,
|
||||
environment: "production",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow YOLO mode in development environment", async () => {
|
||||
// Enable YOLO mode with development environment
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return "development";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const result = await service.preCommitCheck(params);
|
||||
|
||||
// Should NOT call coordinator (YOLO mode enabled)
|
||||
expect(mockCoordinatorClient.checkQuality).not.toHaveBeenCalled();
|
||||
|
||||
// Should return YOLO bypass result
|
||||
expect(result.approved).toBe(true);
|
||||
expect(result.message).toBe("Quality gates disabled (YOLO mode)");
|
||||
expect(result.details?.yoloMode).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow YOLO mode in test environment", async () => {
|
||||
// Enable YOLO mode with test environment
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return "test";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const result = await service.postCommitCheck(params);
|
||||
|
||||
// Should NOT call coordinator (YOLO mode enabled)
|
||||
expect(mockCoordinatorClient.checkQuality).not.toHaveBeenCalled();
|
||||
|
||||
// Should return YOLO bypass result
|
||||
expect(result.approved).toBe(true);
|
||||
expect(result.message).toBe("Quality gates disabled (YOLO mode)");
|
||||
expect(result.details?.yoloMode).toBe(true);
|
||||
});
|
||||
|
||||
it("should block YOLO mode for post-commit in production", async () => {
|
||||
// Enable YOLO mode but set production environment
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return "production";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const mockResponse: QualityCheckResponse = {
|
||||
approved: false,
|
||||
gate: "post-commit",
|
||||
message: "Coverage below threshold",
|
||||
details: {
|
||||
coverage: { current: 78, required: 85 },
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(mockCoordinatorClient.checkQuality).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await service.postCommitCheck(params);
|
||||
|
||||
// Should call coordinator and enforce quality gates
|
||||
expect(mockCoordinatorClient.checkQuality).toHaveBeenCalled();
|
||||
|
||||
// Should return coordinator rejection, not YOLO bypass
|
||||
expect(result.approved).toBe(false);
|
||||
expect(result.message).toBe("Coverage below threshold");
|
||||
expect(result.details?.coverage).toEqual({ current: 78, required: 85 });
|
||||
});
|
||||
|
||||
it("should work when NODE_ENV is not set (default to non-production)", async () => {
|
||||
// Enable YOLO mode without NODE_ENV set
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Also clear process.env.NODE_ENV
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
delete process.env.NODE_ENV;
|
||||
|
||||
try {
|
||||
const result = await service.preCommitCheck(params);
|
||||
|
||||
// Should allow YOLO mode when NODE_ENV not set
|
||||
expect(mockCoordinatorClient.checkQuality).not.toHaveBeenCalled();
|
||||
expect(result.approved).toBe(true);
|
||||
expect(result.details?.yoloMode).toBe(true);
|
||||
} finally {
|
||||
// Restore NODE_ENV
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
}
|
||||
});
|
||||
|
||||
it("should fall back to process.env.NODE_ENV when config not set", async () => {
|
||||
// Enable YOLO mode, config returns undefined but process.env is production
|
||||
vi.mocked(mockConfigService.get).mockImplementation((key: string) => {
|
||||
if (key === "orchestrator.yolo.enabled") {
|
||||
return true;
|
||||
}
|
||||
if (key === "NODE_ENV") {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Set process.env.NODE_ENV to production
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
try {
|
||||
const mockResponse: QualityCheckResponse = {
|
||||
approved: true,
|
||||
gate: "pre-commit",
|
||||
};
|
||||
|
||||
vi.mocked(mockCoordinatorClient.checkQuality).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await service.preCommitCheck(params);
|
||||
|
||||
// Should block YOLO mode (production via process.env)
|
||||
expect(mockCoordinatorClient.checkQuality).toHaveBeenCalled();
|
||||
expect(result.details?.yoloMode).toBeUndefined();
|
||||
} finally {
|
||||
// Restore NODE_ENV
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,10 +217,39 @@ export class QualityGatesService {
|
||||
* YOLO mode bypasses all quality gates.
|
||||
* Default: false (quality gates enabled)
|
||||
*
|
||||
* @returns True if YOLO mode is enabled
|
||||
* SECURITY: YOLO mode is blocked in production environments to prevent
|
||||
* bypassing quality gates in production deployments. This is a security
|
||||
* measure to ensure code quality standards are always enforced in production.
|
||||
*
|
||||
* @returns True if YOLO mode is enabled (always false in production)
|
||||
*/
|
||||
private isYoloModeEnabled(): boolean {
|
||||
return this.configService.get<boolean>("orchestrator.yolo.enabled") ?? false;
|
||||
const yoloRequested = this.configService.get<boolean>("orchestrator.yolo.enabled") ?? false;
|
||||
|
||||
// Block YOLO mode in production
|
||||
if (yoloRequested && this.isProductionEnvironment()) {
|
||||
this.logger.warn(
|
||||
"YOLO mode blocked in production environment - quality gates will be enforced",
|
||||
{
|
||||
requestedYoloMode: true,
|
||||
environment: "production",
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return yoloRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in production environment
|
||||
*
|
||||
* @returns True if NODE_ENV is 'production'
|
||||
*/
|
||||
private isProductionEnvironment(): boolean {
|
||||
const nodeEnv = this.configService.get<string>("NODE_ENV") ?? process.env.NODE_ENV;
|
||||
return nodeEnv === "production";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user