From 6bf489dd2018b13e988b4cc53c5612fd147ade32 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 00:35:52 +0100 Subject: [PATCH 1/9] feat(security): apply policy options in audit runs --- packages/core/src/execution/execute.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/core/src/execution/execute.ts b/packages/core/src/execution/execute.ts index dd7267a..ada89d4 100644 --- a/packages/core/src/execution/execute.ts +++ b/packages/core/src/execution/execute.ts @@ -6,6 +6,8 @@ import type { LoadedService } from '../service/service.types.js'; import { dockerBuild, dockerRun, isDockerAvailable } from '../runtime/docker-runtime.js'; import { getRuntimeConfig } from '../runtime/runtime-registry.js'; import { parseMetrics } from './metrics.js'; +import { DEFAULT_POLICY, loadPolicyFile, policyToDockerOptions } from '../security/index.js'; +import type { SecurityPolicy } from '../security/security.types.js'; @@ -14,6 +16,7 @@ export interface ExecuteOptions { env?: Record; skipBuild?: boolean; audit?: boolean; + policy?: SecurityPolicy; } interface ExecutionState { @@ -45,6 +48,13 @@ export async function executeService( logger.info(`Executing ${serviceName}...`); + let policy: SecurityPolicy | undefined; + if (options.audit) { + policy = options.policy ?? (await loadPolicyFile(service.servicePath)) ?? DEFAULT_POLICY; + } + + const securityOptions = policy ? policyToDockerOptions(policy) : undefined; + const runResult = await dockerRun({ imageName, containerName, @@ -65,13 +75,7 @@ export async function executeService( IGNITE_INPUT: options.input ? JSON.stringify(options.input) : '', NODE_ENV: 'production', }, - security: options.audit ? { - networkDisabled: true, - readOnlyRootfs: true, - dropCapabilities: true, - noNewPrivileges: true, - tmpfsPaths: ['/tmp'], - } : undefined, + security: options.audit ? securityOptions : undefined, }); const metrics = parseMetrics(runResult, isColdStart); From 17bbcf3a0fd361f1479b1de83de64a4afbd1a04b Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 00:36:04 +0100 Subject: [PATCH 2/9] feat(audit): export audit results in cli and api --- packages/cli/src/commands/run.ts | 26 +++++++++++++++++++++----- packages/cli/src/index.ts | 1 + packages/http/src/server.ts | 12 ++++++++++-- packages/http/src/types.ts | 2 ++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 45b22c1..1f0014e 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -1,4 +1,6 @@ -import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, isValidRuntime } from '@ignite/core'; +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, isValidRuntime, loadPolicyFile } from '@ignite/core'; import { logger, ConfigError } from '@ignite/shared'; interface RunOptions { @@ -7,6 +9,7 @@ interface RunOptions { json?: boolean; audit?: boolean; runtime?: string; + auditOutput?: string; } export async function runCommand(servicePath: string, options: RunOptions): Promise { @@ -45,18 +48,31 @@ export async function runCommand(servicePath: string, options: RunOptions): Prom process.exit(1); } - const metrics = await executeService(service, { input, skipBuild: true, audit: options.audit }); + const policy = options.audit + ? (await loadPolicyFile(service.servicePath)) ?? DEFAULT_POLICY + : undefined; + + const metrics = await executeService(service, { input, skipBuild: true, audit: options.audit, policy }); const report = createReport(preflightResult, metrics); + const audit = options.audit && policy + ? parseAuditFromOutput(metrics.stdout, metrics.stderr, policy) + : undefined; + + if (options.auditOutput && audit) { + const outputPath = join(process.cwd(), options.auditOutput); + await writeFile(outputPath, JSON.stringify(audit, null, 2)); + logger.success(`Audit saved to ${outputPath}`); + } if (options.json) { - console.log(JSON.stringify(report, null, 2)); + const payload = audit ? { ...report, securityAudit: audit } : report; + console.log(JSON.stringify(payload, null, 2)); } else { console.log(formatReportAsText(report)); } - if (options.audit) { - const audit = parseAuditFromOutput(metrics.stdout, metrics.stderr, DEFAULT_POLICY); + if (options.audit && audit && !options.json) { console.log(formatSecurityAudit(audit)); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8745ef4..6de0e01 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -30,6 +30,7 @@ program .option('--skip-preflight', 'Skip preflight checks before execution') .option('--json', 'Output results as JSON') .option('--audit', 'Run with security audit (blocks network, read-only filesystem)') + .option('--audit-output ', 'Write security audit to a JSON file') .action(runCommand); program diff --git a/packages/http/src/server.ts b/packages/http/src/server.ts index ef91ae1..ba1e838 100644 --- a/packages/http/src/server.ts +++ b/packages/http/src/server.ts @@ -1,7 +1,7 @@ import { Elysia, t } from 'elysia'; import { cors } from '@elysiajs/cors'; import { join, resolve } from 'node:path'; -import { loadService, executeService, runPreflight, getImageName, buildServiceImage } from '@ignite/core'; +import { loadService, executeService, runPreflight, getImageName, buildServiceImage, loadPolicyFile, DEFAULT_POLICY, parseAuditFromOutput } from '@ignite/core'; import { logger, validateDockerName } from '@ignite/shared'; import type { ServiceExecutionRequest, @@ -166,13 +166,21 @@ export function createServer(options: ServerOptions = {}) { } } - const metrics = await executeService(service, { input, skipBuild, audit }); + const policy = audit + ? (await loadPolicyFile(servicePath)) ?? DEFAULT_POLICY + : undefined; + + const metrics = await executeService(service, { input, skipBuild, audit, policy }); + const securityAudit = audit && policy + ? parseAuditFromOutput(metrics.stdout, metrics.stderr, policy) + : undefined; return { success: true, serviceName, metrics, preflight: preflightResult, + securityAudit, }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index 6e3394a..fa11972 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -1,4 +1,5 @@ import type { ExecutionMetrics, PreflightResult } from '@ignite/shared'; +import type { SecurityAudit } from '@ignite/core'; export interface ServiceExecutionRequest { input?: unknown; @@ -12,6 +13,7 @@ export interface ServiceExecutionResponse { serviceName: string; metrics?: ExecutionMetrics; preflight?: PreflightResult; + securityAudit?: SecurityAudit; error?: string; } From e6931f540281a591dff8e86da5906661ddc77d6d Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 00:36:14 +0100 Subject: [PATCH 3/9] feat(preflight): support configurable thresholds --- packages/core/src/preflight/analyze-image.ts | 30 ++++++---- packages/core/src/preflight/analyze-memory.ts | 38 +++++++----- .../core/src/preflight/analyze-timeout.ts | 28 +++++---- packages/core/src/preflight/preflight.ts | 2 +- packages/core/src/service/load-service.ts | 58 +++++++++++++++++++ packages/shared/src/types.ts | 23 ++++++++ 6 files changed, 143 insertions(+), 36 deletions(-) diff --git a/packages/core/src/preflight/analyze-image.ts b/packages/core/src/preflight/analyze-image.ts index 43e38b7..c18944c 100644 --- a/packages/core/src/preflight/analyze-image.ts +++ b/packages/core/src/preflight/analyze-image.ts @@ -1,11 +1,21 @@ import type { PreflightCheck } from '@ignite/shared'; import { getImageInfo } from '../runtime/docker-runtime.js'; -const IMAGE_SIZE_WARN_THRESHOLD_MB = 500; -const IMAGE_SIZE_FAIL_THRESHOLD_MB = 1000; +const DEFAULT_IMAGE_SIZE_WARN_MB = 500; +const DEFAULT_IMAGE_SIZE_FAIL_MB = 1000; -export async function analyzeImage(imageName: string): Promise { +export interface ImagePreflightConfig { + warnMb?: number; + failMb?: number; +} + +export async function analyzeImage( + imageName: string, + config?: ImagePreflightConfig +): Promise { const imageInfo = await getImageInfo(imageName); + const warnThresholdMb = config?.warnMb ?? DEFAULT_IMAGE_SIZE_WARN_MB; + const failThresholdMb = config?.failMb ?? DEFAULT_IMAGE_SIZE_FAIL_MB; if (!imageInfo) { return { @@ -17,23 +27,23 @@ export async function analyzeImage(imageName: string): Promise { const sizeMb = Math.round(imageInfo.size / 1024 / 1024); - if (sizeMb > IMAGE_SIZE_FAIL_THRESHOLD_MB) { + if (sizeMb > failThresholdMb) { return { name: 'image-size', status: 'fail', - message: `Image size ${sizeMb}MB exceeds ${IMAGE_SIZE_FAIL_THRESHOLD_MB}MB limit`, + message: `Image size ${sizeMb}MB exceeds ${failThresholdMb}MB limit`, value: sizeMb, - threshold: IMAGE_SIZE_FAIL_THRESHOLD_MB, + threshold: failThresholdMb, }; } - if (sizeMb > IMAGE_SIZE_WARN_THRESHOLD_MB) { + if (sizeMb > warnThresholdMb) { return { name: 'image-size', status: 'warn', - message: `Image size ${sizeMb}MB exceeds recommended ${IMAGE_SIZE_WARN_THRESHOLD_MB}MB`, + message: `Image size ${sizeMb}MB exceeds recommended ${warnThresholdMb}MB`, value: sizeMb, - threshold: IMAGE_SIZE_WARN_THRESHOLD_MB, + threshold: warnThresholdMb, }; } @@ -42,6 +52,6 @@ export async function analyzeImage(imageName: string): Promise { status: 'pass', message: `Image size ${sizeMb}MB is within limits`, value: sizeMb, - threshold: IMAGE_SIZE_WARN_THRESHOLD_MB, + threshold: warnThresholdMb, }; } diff --git a/packages/core/src/preflight/analyze-memory.ts b/packages/core/src/preflight/analyze-memory.ts index fdc696f..e27db60 100644 --- a/packages/core/src/preflight/analyze-memory.ts +++ b/packages/core/src/preflight/analyze-memory.ts @@ -1,31 +1,40 @@ import type { PreflightCheck } from '@ignite/shared'; import type { LoadedService } from '../service/service.types.js'; -const MEMORY_PER_DEP_MB = 2; -const BASE_MEMORY_MB = 50; +const DEFAULT_MEMORY_PER_DEP_MB = 2; +const DEFAULT_BASE_MEMORY_MB = 50; +const DEFAULT_WARN_RATIO = 1; +const DEFAULT_FAIL_RATIO = 0.8; export function analyzeMemory(service: LoadedService): PreflightCheck { const configuredMemoryMb = service.config.service.memoryMb; const depCount = service.dependencyCount ?? 0; - const estimatedMemoryMb = BASE_MEMORY_MB + depCount * MEMORY_PER_DEP_MB; + const memoryConfig = service.config.preflight?.memory; + const baseMemoryMb = memoryConfig?.baseMb ?? DEFAULT_BASE_MEMORY_MB; + const perDependencyMb = memoryConfig?.perDependencyMb ?? DEFAULT_MEMORY_PER_DEP_MB; + const warnRatio = memoryConfig?.warnRatio ?? DEFAULT_WARN_RATIO; + const failRatio = memoryConfig?.failRatio ?? DEFAULT_FAIL_RATIO; + const estimatedMemoryMb = baseMemoryMb + depCount * perDependencyMb; + const warnThreshold = estimatedMemoryMb * warnRatio; + const failThreshold = estimatedMemoryMb * failRatio; - if (configuredMemoryMb < estimatedMemoryMb * 0.8) { + if (configuredMemoryMb < failThreshold) { return { name: 'memory-allocation', status: 'fail', message: `Configured memory ${configuredMemoryMb}MB may be insufficient. Estimated need: ${estimatedMemoryMb}MB based on ${depCount} dependencies`, value: configuredMemoryMb, - threshold: estimatedMemoryMb, + threshold: Math.round(failThreshold), }; } - if (configuredMemoryMb < estimatedMemoryMb) { + if (configuredMemoryMb < warnThreshold) { return { name: 'memory-allocation', status: 'warn', message: `Configured memory ${configuredMemoryMb}MB is close to estimated need of ${estimatedMemoryMb}MB`, value: configuredMemoryMb, - threshold: estimatedMemoryMb, + threshold: Math.round(warnThreshold), }; } @@ -34,7 +43,7 @@ export function analyzeMemory(service: LoadedService): PreflightCheck { status: 'pass', message: `Configured memory ${configuredMemoryMb}MB exceeds estimated need of ${estimatedMemoryMb}MB`, value: configuredMemoryMb, - threshold: estimatedMemoryMb, + threshold: Math.round(warnThreshold), }; } @@ -43,24 +52,27 @@ export function analyzeDependencies(service: LoadedService): PreflightCheck { const nodeModulesSizeMb = service.nodeModulesSize ? Math.round(service.nodeModulesSize / 1024 / 1024) : 0; + const dependencyConfig = service.config.preflight?.dependencies; + const warnCount = dependencyConfig?.warnCount ?? 100; + const infoCount = dependencyConfig?.infoCount ?? 50; - if (depCount > 100) { + if (depCount > warnCount) { return { name: 'dependency-count', status: 'warn', message: `High dependency count (${depCount}). node_modules size: ${nodeModulesSizeMb}MB. Consider reducing dependencies for faster cold starts.`, value: depCount, - threshold: 100, + threshold: warnCount, }; } - if (depCount > 50) { + if (depCount > infoCount) { return { name: 'dependency-count', status: 'pass', message: `Moderate dependency count (${depCount}). node_modules size: ${nodeModulesSizeMb}MB`, value: depCount, - threshold: 50, + threshold: infoCount, }; } @@ -69,6 +81,6 @@ export function analyzeDependencies(service: LoadedService): PreflightCheck { status: 'pass', message: `Low dependency count (${depCount}). node_modules size: ${nodeModulesSizeMb}MB`, value: depCount, - threshold: 50, + threshold: infoCount, }; } diff --git a/packages/core/src/preflight/analyze-timeout.ts b/packages/core/src/preflight/analyze-timeout.ts index 5d1a9eb..f8b55c7 100644 --- a/packages/core/src/preflight/analyze-timeout.ts +++ b/packages/core/src/preflight/analyze-timeout.ts @@ -1,44 +1,48 @@ import type { PreflightCheck } from '@ignite/shared'; import type { LoadedService } from '../service/service.types.js'; -const MIN_TIMEOUT_MS = 100; -const MAX_TIMEOUT_MS = 30000; -const COLD_START_BUFFER_MS = 500; +const DEFAULT_MIN_TIMEOUT_MS = 100; +const DEFAULT_MAX_TIMEOUT_MS = 30000; +const DEFAULT_COLD_START_BUFFER_MS = 500; export function analyzeTimeout( service: LoadedService, lastExecutionMs?: number ): PreflightCheck { const configuredTimeoutMs = service.config.service.timeoutMs; + const timeoutConfig = service.config.preflight?.timeout; + const minTimeoutMs = timeoutConfig?.minMs ?? DEFAULT_MIN_TIMEOUT_MS; + const maxTimeoutMs = timeoutConfig?.maxMs ?? DEFAULT_MAX_TIMEOUT_MS; + const coldStartBufferMs = timeoutConfig?.coldStartBufferMs ?? DEFAULT_COLD_START_BUFFER_MS; - if (configuredTimeoutMs < MIN_TIMEOUT_MS) { + if (configuredTimeoutMs < minTimeoutMs) { return { name: 'timeout-config', status: 'fail', - message: `Timeout ${configuredTimeoutMs}ms is below minimum ${MIN_TIMEOUT_MS}ms`, + message: `Timeout ${configuredTimeoutMs}ms is below minimum ${minTimeoutMs}ms`, value: configuredTimeoutMs, - threshold: MIN_TIMEOUT_MS, + threshold: minTimeoutMs, }; } - if (configuredTimeoutMs > MAX_TIMEOUT_MS) { + if (configuredTimeoutMs > maxTimeoutMs) { return { name: 'timeout-config', status: 'warn', - message: `Timeout ${configuredTimeoutMs}ms exceeds recommended maximum ${MAX_TIMEOUT_MS}ms`, + message: `Timeout ${configuredTimeoutMs}ms exceeds recommended maximum ${maxTimeoutMs}ms`, value: configuredTimeoutMs, - threshold: MAX_TIMEOUT_MS, + threshold: maxTimeoutMs, }; } if (lastExecutionMs !== undefined) { - const estimatedWithColdStart = lastExecutionMs + COLD_START_BUFFER_MS; + const estimatedWithColdStart = lastExecutionMs + coldStartBufferMs; if (configuredTimeoutMs < estimatedWithColdStart) { return { name: 'timeout-config', status: 'warn', - message: `Timeout ${configuredTimeoutMs}ms may be too short. Last execution: ${lastExecutionMs}ms + ${COLD_START_BUFFER_MS}ms cold start buffer = ${estimatedWithColdStart}ms`, + message: `Timeout ${configuredTimeoutMs}ms may be too short. Last execution: ${lastExecutionMs}ms + ${coldStartBufferMs}ms cold start buffer = ${estimatedWithColdStart}ms`, value: configuredTimeoutMs, threshold: estimatedWithColdStart, }; @@ -50,6 +54,6 @@ export function analyzeTimeout( status: 'pass', message: `Timeout ${configuredTimeoutMs}ms is within acceptable range`, value: configuredTimeoutMs, - threshold: MAX_TIMEOUT_MS, + threshold: maxTimeoutMs, }; } diff --git a/packages/core/src/preflight/preflight.ts b/packages/core/src/preflight/preflight.ts index e1299ef..2248550 100644 --- a/packages/core/src/preflight/preflight.ts +++ b/packages/core/src/preflight/preflight.ts @@ -20,7 +20,7 @@ export async function runPreflight( checks.push(analyzeTimeout(service, options.lastExecutionMs)); if (options.imageName) { - checks.push(await analyzeImage(options.imageName)); + checks.push(await analyzeImage(options.imageName, service.config.preflight?.image)); } const overallStatus = determineOverallStatus(checks); diff --git a/packages/core/src/service/load-service.ts b/packages/core/src/service/load-service.ts index d5b5b9e..61efa0e 100644 --- a/packages/core/src/service/load-service.ts +++ b/packages/core/src/service/load-service.ts @@ -113,9 +113,67 @@ function validateServiceConfig(config: unknown): ServiceValidation { } } + const preflight = c['preflight']; + if (preflight !== undefined) { + if (typeof preflight !== 'object' || preflight === null) { + errors.push('preflight must be an object'); + } else { + const pf = preflight as Record; + + validatePreflightSection(pf['memory'], 'preflight.memory', errors, { + baseMb: 'positive', + perDependencyMb: 'positive', + warnRatio: 'positive', + failRatio: 'positive', + }); + + validatePreflightSection(pf['dependencies'], 'preflight.dependencies', errors, { + warnCount: 'positive', + infoCount: 'positive', + }); + + validatePreflightSection(pf['image'], 'preflight.image', errors, { + warnMb: 'positive', + failMb: 'positive', + }); + + validatePreflightSection(pf['timeout'], 'preflight.timeout', errors, { + minMs: 'positive', + maxMs: 'positive', + coldStartBufferMs: 'positive', + }); + } + } + return { valid: errors.length === 0, errors }; } +function validatePreflightSection( + section: unknown, + path: string, + errors: string[], + fields: Record +): void { + if (section === undefined) return; + if (typeof section !== 'object' || section === null) { + errors.push(`${path} must be an object`); + return; + } + + const record = section as Record; + for (const [key, rule] of Object.entries(fields)) { + const value = record[key]; + if (value === undefined) continue; + if (typeof value !== 'number' || !Number.isFinite(value)) { + errors.push(`${path}.${key} must be a number`); + continue; + } + if (rule === 'positive' && value <= 0) { + errors.push(`${path}.${key} must be a positive number`); + } + } +} + async function getDirectorySize(dirPath: string): Promise { let totalSize = 0; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 12a96af..9d84e0b 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -8,6 +8,29 @@ export interface ServiceConfig { timeoutMs: number; env?: Record; }; + preflight?: PreflightConfig; +} + +export interface PreflightConfig { + memory?: { + baseMb?: number; + perDependencyMb?: number; + warnRatio?: number; + failRatio?: number; + }; + dependencies?: { + warnCount?: number; + infoCount?: number; + }; + image?: { + warnMb?: number; + failMb?: number; + }; + timeout?: { + minMs?: number; + maxMs?: number; + coldStartBufferMs?: number; + }; } export interface RuntimeSpec { From c58412b289f9a92db14566907caadcbe6411e437 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 00:36:24 +0100 Subject: [PATCH 4/9] docs: document audit output and preflight config --- docs/api.md | 38 ++++++++++++++++++++++++++------------ docs/preflight.md | 22 ++++++++++++++++++---- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/api.md b/docs/api.md index 7721e80..a8b96f0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -99,6 +99,7 @@ ignite run [options] | `--skip-preflight` | `false` | Skip safety checks | | `--json` | `false` | Output results as JSON | | `--audit` | `false` | Run with security audit (blocks network, read-only filesystem) | +| `--audit-output ` | - | Write security audit to a JSON file | **Examples:** @@ -151,6 +152,8 @@ Filesystem ✗ Security Status: 2 VIOLATION(S) BLOCKED ``` +When `--json` is used with `--audit`, the JSON output includes a `securityAudit` field. + **Output:** ``` @@ -574,7 +577,8 @@ Execute a service. "data": [1, 2, 3], "operation": "sum" }, - "skipPreflight": false + "skipPreflight": false, + "audit": true } ``` @@ -582,6 +586,8 @@ Execute a service. |-------|------|----------|-------------| | `input` | object | No | Input data passed to service | | `skipPreflight` | boolean | No | Skip safety checks | +| `skipBuild` | boolean | No | Skip image build if already built | +| `audit` | boolean | No | Run with security audit | **Response:** @@ -597,6 +603,8 @@ Execute a service. } ``` +When `audit` is true, the response includes `securityAudit`. + **Errors:** | Status | Description | @@ -636,6 +644,23 @@ service: timeoutMs: number # Timeout (default: 30000) env: object # Environment variables dependencies: array # Explicit dependencies (auto-detected by default) + +preflight: + memory: + baseMb: number # Base memory estimate (default: 50) + perDependencyMb: number # Memory per dependency (default: 2) + warnRatio: number # Warning threshold ratio (default: 1) + failRatio: number # Failure threshold ratio (default: 0.8) + dependencies: + warnCount: number # Warn if dependency count exceeds (default: 100) + infoCount: number # Info threshold for moderate count (default: 50) + image: + warnMb: number # Image size warn threshold (default: 500) + failMb: number # Image size fail threshold (default: 1000) + timeout: + minMs: number # Minimum timeout (default: 100) + maxMs: number # Maximum recommended timeout (default: 30000) + coldStartBufferMs: number # Cold start buffer (default: 500) ``` **Supported Runtimes:** @@ -699,20 +724,9 @@ Create an `ignite.policy.yaml` file to customize security settings: security: network: enabled: false # Block all network (default) - allowedHosts: # Optional: allow specific hosts - - api.example.com - allowedPorts: # Optional: allow specific ports - - 443 filesystem: readOnly: true # Read-only root filesystem - allowedWritePaths: # Paths that can be written to - - /tmp - blockedReadPaths: # Paths blocked from reading - - /etc/passwd - - /etc/shadow - - /proc - - /sys process: allowSpawn: false # Block spawning child processes diff --git a/docs/preflight.md b/docs/preflight.md index 18e45c9..42a4513 100644 --- a/docs/preflight.md +++ b/docs/preflight.md @@ -59,12 +59,26 @@ Each check returns: ## Customizing Thresholds -Thresholds are currently hardcoded. Future versions will support configuration via `service.yaml`: +Thresholds can be configured via `service.yaml`: ```yaml service: name: my-service - preflight: - memory_buffer: 1.5 - max_dependencies: 150 + +preflight: + memory: + baseMb: 60 + perDependencyMb: 3 + warnRatio: 1 + failRatio: 0.85 + dependencies: + warnCount: 120 + infoCount: 60 + image: + warnMb: 600 + failMb: 1200 + timeout: + minMs: 200 + maxMs: 45000 + coldStartBufferMs: 750 ``` From af35113b8c6a9f6ad5cd8eeaf0af348c3417dd13 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 00:36:33 +0100 Subject: [PATCH 5/9] test: align hello-bun timeout --- examples/hello-bun/service.yaml | 2 +- packages/core/src/__tests__/load-service.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello-bun/service.yaml b/examples/hello-bun/service.yaml index 80285e8..4ef68b5 100644 --- a/examples/hello-bun/service.yaml +++ b/examples/hello-bun/service.yaml @@ -3,6 +3,6 @@ service: runtime: bun entry: index.ts memoryMb: 128 - timeoutMs: 5000 + timeoutMs: 30000 env: NODE_ENV: production diff --git a/packages/core/src/__tests__/load-service.test.ts b/packages/core/src/__tests__/load-service.test.ts index a02983b..ed5db0d 100644 --- a/packages/core/src/__tests__/load-service.test.ts +++ b/packages/core/src/__tests__/load-service.test.ts @@ -13,7 +13,7 @@ describe('loadService', () => { expect(service.config.service.runtime).toBe('bun'); expect(service.config.service.entry).toBe('index.ts'); expect(service.config.service.memoryMb).toBe(128); - expect(service.config.service.timeoutMs).toBe(5000); + expect(service.config.service.timeoutMs).toBe(30000); expect(service.servicePath).toBe(servicePath); }); From ec56e4de86710ebb5261b79168953dd5122b9d64 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 01:28:47 +0100 Subject: [PATCH 6/9] fix(cli): resolve audit output path --- packages/cli/src/commands/run.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 1f0014e..cf658e9 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -1,5 +1,5 @@ import { writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; +import { resolve } from 'node:path'; import { loadService, executeService, runPreflight, createReport, formatReportAsText, getImageName, buildServiceImage, parseAuditFromOutput, formatSecurityAudit, DEFAULT_POLICY, isValidRuntime, loadPolicyFile } from '@ignite/core'; import { logger, ConfigError } from '@ignite/shared'; @@ -60,7 +60,7 @@ export async function runCommand(servicePath: string, options: RunOptions): Prom : undefined; if (options.auditOutput && audit) { - const outputPath = join(process.cwd(), options.auditOutput); + const outputPath = resolve(process.cwd(), options.auditOutput); await writeFile(outputPath, JSON.stringify(audit, null, 2)); logger.success(`Audit saved to ${outputPath}`); } From 1c231c61544c246635bf74ddbc961630e18d7f15 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 01:28:57 +0100 Subject: [PATCH 7/9] fix(preflight): validate threshold ordering --- packages/core/src/service/load-service.ts | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/core/src/service/load-service.ts b/packages/core/src/service/load-service.ts index 61efa0e..4829f8b 100644 --- a/packages/core/src/service/load-service.ts +++ b/packages/core/src/service/load-service.ts @@ -127,6 +127,15 @@ function validateServiceConfig(config: unknown): ServiceValidation { failRatio: 'positive', }); + const memoryConfig = pf['memory'] as Record | undefined; + const warnRatio = memoryConfig?.['warnRatio']; + const failRatio = memoryConfig?.['failRatio']; + if (typeof warnRatio === 'number' && typeof failRatio === 'number') { + if (warnRatio >= failRatio) { + errors.push('preflight.memory.warnRatio must be less than preflight.memory.failRatio'); + } + } + validatePreflightSection(pf['dependencies'], 'preflight.dependencies', errors, { warnCount: 'positive', infoCount: 'positive', @@ -137,11 +146,29 @@ function validateServiceConfig(config: unknown): ServiceValidation { failMb: 'positive', }); + const imageConfig = pf['image'] as Record | undefined; + const warnMb = imageConfig?.['warnMb']; + const failMb = imageConfig?.['failMb']; + if (typeof warnMb === 'number' && typeof failMb === 'number') { + if (warnMb >= failMb) { + errors.push('preflight.image.warnMb must be less than preflight.image.failMb'); + } + } + validatePreflightSection(pf['timeout'], 'preflight.timeout', errors, { minMs: 'positive', maxMs: 'positive', coldStartBufferMs: 'positive', }); + + const timeoutConfig = pf['timeout'] as Record | undefined; + const minMs = timeoutConfig?.['minMs']; + const maxMs = timeoutConfig?.['maxMs']; + if (typeof minMs === 'number' && typeof maxMs === 'number') { + if (minMs >= maxMs) { + errors.push('preflight.timeout.minMs must be less than preflight.timeout.maxMs'); + } + } } } From c989eac617f30bbcc2a7e6160116d214aa30f073 Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 01:29:07 +0100 Subject: [PATCH 8/9] chore: bump versions to 0.7.1 --- package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/index.ts | 2 +- packages/core/package.json | 2 +- packages/http/package.json | 2 +- packages/shared/package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 74ee065..7f572a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ignite", - "version": "0.6.1", + "version": "0.7.1", "private": true, "description": "Secure JS/TS code execution in Docker with sandboxing for AI agents, untrusted code, and microservices", "workspaces": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index bbad981..b7fdfb6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/cli", - "version": "0.6.0", + "version": "0.7.1", "type": "module", "bin": { "ignite": "./dist/index.js" diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 6de0e01..f1708fe 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,7 +13,7 @@ const program = new Command(); program .name('ignite') .description('Secure sandbox for AI-generated code, untrusted scripts, and JS/TS execution') - .version('0.6.0'); + .version('0.7.1'); program .command('init ') diff --git a/packages/core/package.json b/packages/core/package.json index 1e8f733..1755694 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/core", - "version": "0.6.0", + "version": "0.7.1", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/http/package.json b/packages/http/package.json index 7a9c080..876d882 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/http", - "version": "0.6.0", + "version": "0.7.1", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/package.json b/packages/shared/package.json index 6749626..240c468 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/shared", - "version": "0.6.0", + "version": "0.7.1", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 2463865b3b7bb457c60f0c19b55f7dc8efcf539d Mon Sep 17 00:00:00 2001 From: dev-dami Date: Mon, 26 Jan 2026 02:02:25 +0100 Subject: [PATCH 9/9] fix(cli): handle audit file write errors gracefully - Wrap writeFile in try/catch to handle filesystem errors - Add cross-field validation for preflight.dependencies (infoCount < warnCount) - Bump versions to 0.7.2 --- package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/commands/run.ts | 8 ++++++-- packages/cli/src/index.ts | 2 +- packages/core/package.json | 2 +- packages/core/src/service/load-service.ts | 9 +++++++++ packages/http/package.json | 2 +- packages/shared/package.json | 2 +- 8 files changed, 21 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7f572a7..861550b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ignite", - "version": "0.7.1", + "version": "0.7.2", "private": true, "description": "Secure JS/TS code execution in Docker with sandboxing for AI agents, untrusted code, and microservices", "workspaces": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index b7fdfb6..780da9b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/cli", - "version": "0.7.1", + "version": "0.7.2", "type": "module", "bin": { "ignite": "./dist/index.js" diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index cf658e9..1b4be2c 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -61,8 +61,12 @@ export async function runCommand(servicePath: string, options: RunOptions): Prom if (options.auditOutput && audit) { const outputPath = resolve(process.cwd(), options.auditOutput); - await writeFile(outputPath, JSON.stringify(audit, null, 2)); - logger.success(`Audit saved to ${outputPath}`); + try { + await writeFile(outputPath, JSON.stringify(audit, null, 2)); + logger.success(`Audit saved to ${outputPath}`); + } catch (err) { + logger.error(`Failed to write audit to ${outputPath}: ${(err as Error).message}`); + } } if (options.json) { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f1708fe..7b3bfdc 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,7 +13,7 @@ const program = new Command(); program .name('ignite') .description('Secure sandbox for AI-generated code, untrusted scripts, and JS/TS execution') - .version('0.7.1'); + .version('0.7.2'); program .command('init ') diff --git a/packages/core/package.json b/packages/core/package.json index 1755694..42b02d5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/core", - "version": "0.7.1", + "version": "0.7.2", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/core/src/service/load-service.ts b/packages/core/src/service/load-service.ts index 4829f8b..0f95250 100644 --- a/packages/core/src/service/load-service.ts +++ b/packages/core/src/service/load-service.ts @@ -141,6 +141,15 @@ function validateServiceConfig(config: unknown): ServiceValidation { infoCount: 'positive', }); + const dependencyConfig = pf['dependencies'] as Record | undefined; + const warnCount = dependencyConfig?.['warnCount']; + const infoCount = dependencyConfig?.['infoCount']; + if (typeof warnCount === 'number' && typeof infoCount === 'number') { + if (infoCount >= warnCount) { + errors.push('preflight.dependencies.infoCount must be less than preflight.dependencies.warnCount'); + } + } + validatePreflightSection(pf['image'], 'preflight.image', errors, { warnMb: 'positive', failMb: 'positive', diff --git a/packages/http/package.json b/packages/http/package.json index 876d882..66f728f 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/http", - "version": "0.7.1", + "version": "0.7.2", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/package.json b/packages/shared/package.json index 240c468..526af52 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/shared", - "version": "0.7.1", + "version": "0.7.2", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts",