From d1a99317b44d3fb4926c2adf399c03afadcd00a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 18:57:44 +0700 Subject: [PATCH 01/89] chore(ops): refresh checkpoint and batch policy --- docs/plans/000-platform-program.md | 2 +- docs/plans/003-luna-handoff-runbook.md | 4 ++-- docs/plans/execution-orchestration.json | 14 +++++++------- .../repo-cli/src/check-execution-orchestration.mjs | 2 +- .../repo-cli/test/execution-orchestration.test.mjs | 7 ++++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/plans/000-platform-program.md b/docs/plans/000-platform-program.md index 2811f164..b86015c7 100644 --- a/docs/plans/000-platform-program.md +++ b/docs/plans/000-platform-program.md @@ -48,7 +48,7 @@ docs/plans/requirement-traceability.json records all 611 IDs with requested trac - `main` contains stable releases. `dev` is the integration branch. - New capabilities use `feat/`; corrections use `fix/`; operational and documentation work use conventional prefixes when more accurate. - Commit one coherent tested unit at a time. Do not combine unrelated applications or domains merely to reduce commit count. -- Feature/fix pull requests target `dev`, preferably contain 30–50 commits, and must not exceed 60 commits. Run local and hosted checks, but do not invoke CodeRabbit on these integration pull requests. +- Feature/fix pull requests target `dev`, normally contain 30–70 commits, and must remain under 100 commits. Run local and hosted checks, but do not invoke CodeRabbit on these integration pull requests. Focused review-gate fixes are the only intentional small-PR exception. - Merge a green integration pull request while preserving its atomic rollback commits, then immediately open a separate `dev` → `main` promotion pull request for that completed batch. - Invoke CodeRabbit exactly once on the otherwise-ready promotion pull request and request a full review. Do not invoke it if the promotion exceeds 280 changed files; split or revert the batch first. - Reproduce every CodeRabbit claim against the reviewed commit. Fix valid findings in focused commits, document rejected findings with evidence, rerun all affected gates, and never request a second CodeRabbit review on that pull request. diff --git a/docs/plans/003-luna-handoff-runbook.md b/docs/plans/003-luna-handoff-runbook.md index b9b3c579..8d112214 100644 --- a/docs/plans/003-luna-handoff-runbook.md +++ b/docs/plans/003-luna-handoff-runbook.md @@ -93,7 +93,7 @@ For each `#### TASK-ID —` item in `002-complete-execution-orchestration.md`: ## Pull-request and CodeRabbit protocol -1. Branch from current `origin/dev` using `feat/` or `fix/`. Keep atomic commits; preferred PR size is 30–50 commits and hard maximum is 60. Cut earlier at a coherent boundary when risk, dependency ordering, or reviewability warrants it. +1. Branch from current `origin/dev` using `feat/` or `fix/`. Keep atomic commits; preferred PR size is 30–70 commits and hard maximum is 99. Cut earlier only for a coherent boundary, a mandatory promotion-gate fix, or a safety constraint. 2. Before a feature PR, ensure it targets `dev`, has no unrelated commits, and passes local gates. Open it with requirement/task/evidence/rollback notes. CodeRabbit must not be invoked on this PR. 3. Wait for hosted checks. Diagnose failures; do not merge red or missing required checks. Merge with history that preserves the atomic rollback units, normally `--no-ff`/merge commit rather than squash. 4. Immediately compare `dev` and `main`. If `dev` contains the reviewed batch and no incompatible promotion is open, create `dev` → `main`. If the diff exceeds 280 changed files, do not invoke CodeRabbit: split/revert the feature batch or request user direction first. @@ -171,7 +171,7 @@ Live verified checkpoint: branch [BRANCH], HEAD [HEAD], origin/dev [DEV], origin Resume task [TASK_ID] only after proving its dependency/entry gate. Follow test-first atomic delivery: canonical contracts when the interface changes, failing domain/state tests, PostgreSQL migration/tenant/transaction/outbox tests when durable state changes, implementation through ports, vertical client/adapter coverage when the task involves client behavior, safe telemetry/recovery, traceability evidence, scoped checks, repo:check, repo:build, diff review, and one reversible commit. For documentation-only or other non-durable/non-client tasks, record why those conditional tests do not apply. Do not mark merged code verified without all evidence. -Git flow is fixed: feat/* or fix/* → PR to dev with hosted checks and no CodeRabbit; merge preserving atomic commits; immediately open dev→main; request exactly one CodeRabbit full review there; reproduce every comment, fix only valid findings, document rejected ones, never request a second review on that PR. Prefer 30–50 commits, hard cap 60, and do not invoke the promotion review over 280 changed files. +Git flow is fixed: feat/* or fix/* → PR to dev with hosted checks and no CodeRabbit; merge preserving atomic commits; immediately open dev→main; request exactly one CodeRabbit full review there; reproduce every comment, fix only valid findings, document rejected ones, never request a second review on that PR. Prefer 30–70 commits, hard cap 99, and do not invoke the promotion review over 280 changed files. Keep PostgreSQL authoritative, Redis ephemeral, tenant scope explicit, originals/versions immutable, Hybrid default, Local content/path out of cloud, workers without database credentials, Desktop/sidecar allowlisted, Android background payloads content-free, Vietnamese and English complete, and external providers replaceable. Stop and record state for destructive migration risk, unknown overlapping changes, privacy/security fail-open behavior, missing production/signing authority, or a spec conflict. End with the exact handoff record from the runbook. ``` diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index 0dcad477..de7dc3e8 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -15,11 +15,11 @@ } }, "checkpoint": { - "observedAt": "2026-08-02T10:08:12Z", - "remoteDev": "4d415494240abbc610574132678007a623c405e4", - "remoteMain": "d26e6be16ecadc07467b458b853eb8070940e846", - "lastFeaturePullRequest": 13, - "lastPromotionPullRequest": 11, + "observedAt": "2026-08-02T12:00:00Z", + "remoteDev": "92b3e9a4d581f3a6947b7a2bf58c8334f4ae0c18", + "remoteMain": "a2fcba34037c1ffd77816be16be75453abfb16fa", + "lastFeaturePullRequest": 18, + "lastPromotionPullRequest": 14, "openPullRequestsObserved": 0, "note": "Historical observation only; every session must fetch and recompute current state." }, @@ -38,8 +38,8 @@ }, "commitBudget": { "preferredMinimum": 30, - "preferredMaximum": 50, - "hardMaximum": 60 + "preferredMaximum": 70, + "hardMaximum": 99 } }, "statusVocabulary": [ diff --git a/tools/repo-cli/src/check-execution-orchestration.mjs b/tools/repo-cli/src/check-execution-orchestration.mjs index 15688116..5229f4e7 100644 --- a/tools/repo-cli/src/check-execution-orchestration.mjs +++ b/tools/repo-cli/src/check-execution-orchestration.mjs @@ -25,7 +25,7 @@ const expectedPlans = new Map([ ]); const expectedPriorityTotals = { P0: 444, P1: 154, P2: 13 }; const expectedReviewPolicy = { - commitBudget: { hardMaximum: 60, preferredMaximum: 50, preferredMinimum: 30 }, + commitBudget: { hardMaximum: 99, preferredMaximum: 70, preferredMinimum: 30 }, featurePullRequest: { base: 'dev', codeRabbit: false, mergeAfterHostedChecks: true }, promotionPullRequest: { base: 'main', diff --git a/tools/repo-cli/test/execution-orchestration.test.mjs b/tools/repo-cli/test/execution-orchestration.test.mjs index cdef1148..5497c2e3 100644 --- a/tools/repo-cli/test/execution-orchestration.test.mjs +++ b/tools/repo-cli/test/execution-orchestration.test.mjs @@ -142,8 +142,8 @@ test('handoff policy preserves the requested dev and main review flow', () => { }); assert.deepEqual(ledger.reviewPolicy.commitBudget, { preferredMinimum: 30, - preferredMaximum: 50, - hardMaximum: 60, + preferredMaximum: 70, + hardMaximum: 99, }); }); @@ -177,7 +177,8 @@ test('repository checker validates the committed orchestration package', () => { test('ledger records verified task evidence before advancing the next task', () => { const ledger = readJson('docs/plans/execution-orchestration.json'); assert.equal(ledger.nextTaskId, 'FND-003'); - assert.equal(ledger.checkpoint.lastFeaturePullRequest, 13); + assert.equal(ledger.checkpoint.lastFeaturePullRequest, 18); + assert.equal(ledger.checkpoint.lastPromotionPullRequest, 14); assert.deepEqual(ledger.taskState?.['FND-001']?.status, 'verified'); assert.match(ledger.taskState?.['FND-001']?.commit ?? '', /^[0-9a-f]{40}$/u); assert.ok( From 4eff6f51c82104fa47643556f23a9453702b9fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 18:59:42 +0700 Subject: [PATCH 02/89] feat(infra): add safe local service lifecycle --- package.json | 1 + tools/repo-cli/src/local-services-smoke.mjs | 111 +------ tools/repo-cli/src/local-services.mjs | 289 ++++++++++++++++++ .../test/local-infrastructure.test.mjs | 20 ++ 4 files changed, 312 insertions(+), 109 deletions(-) create mode 100644 tools/repo-cli/src/local-services.mjs diff --git a/package.json b/package.json index b46e0aba..535fcc24 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "format:check": "prettier --check .", "lint": "eslint . && node tools/repo-cli/src/check-dependency-boundaries.mjs", "local:smoke": "node tools/repo-cli/src/local-services-smoke.mjs", + "local:services": "node tools/repo-cli/src/local-services.mjs", "infra:check": "node tools/repo-cli/src/check-aws-infrastructure.mjs", "orchestration:check": "node tools/repo-cli/src/check-execution-orchestration.mjs", "repo:bootstrap": "corepack pnpm install --frozen-lockfile", diff --git a/tools/repo-cli/src/local-services-smoke.mjs b/tools/repo-cli/src/local-services-smoke.mjs index 44b89dc6..53cb413a 100644 --- a/tools/repo-cli/src/local-services-smoke.mjs +++ b/tools/repo-cli/src/local-services-smoke.mjs @@ -1,113 +1,6 @@ -import { existsSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; -import { setTimeout as delay } from 'node:timers/promises'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { main } from './local-services.mjs'; -const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); -const composeFile = path.join(repositoryRoot, 'infrastructure', 'local', 'compose.yml'); -const exampleEnvFile = path.join(repositoryRoot, 'infrastructure', 'local', '.env.example'); -const localEnvFile = path.join(repositoryRoot, 'infrastructure', 'local', '.env'); -const services = [ - 'postgres', - 'redis', - 'minio', - 'mailpit', - 'otel-collector', - 'otel-collector-health', -]; - -function usage() { - console.log(`Usage: pnpm local:smoke [-- --start] [-- --wait-seconds=60] - -Validates the local Compose file and polls health checks for the five required -services. The command never removes containers or named volumes. - - --start run docker compose up -d before polling - --wait-seconds=N maximum readiness wait (default: 60) - --help show this help`); -} - -function runDocker(args, { allowFailure = false } = {}) { - const result = spawnSync('docker', args, { cwd: repositoryRoot, encoding: 'utf8' }); - if (!allowFailure && result.status !== 0) { - const detail = (result.stderr || result.stdout || '').trim(); - throw new Error(`docker ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); - } - return result; -} - -function composeArgs() { - const envFile = existsSync(localEnvFile) ? localEnvFile : exampleEnvFile; - return ['compose', '--env-file', envFile, '-f', composeFile]; -} - -function inspectHealth(service) { - const idResult = runDocker([...composeArgs(), 'ps', '-q', service], { allowFailure: true }); - const id = idResult.stdout.trim(); - if (!id) return { state: 'missing', detail: 'no container' }; - - const inspect = runDocker([ - 'inspect', - '--format', - '{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health{{end}}', - id, - ]); - const [state, health] = inspect.stdout.trim().split('|'); - return { state, health, detail: `${state}/${health}` }; -} - -function parseArguments(argv) { - const options = { start: false, waitSeconds: 60 }; - for (const argument of argv) { - if (argument === '--help' || argument === '-h') { - usage(); - process.exit(0); - } - if (argument === '--start') { - options.start = true; - continue; - } - if (argument.startsWith('--wait-seconds=')) { - const value = Number(argument.slice('--wait-seconds='.length)); - if (!Number.isInteger(value) || value < 1 || value > 3600) { - throw new Error('--wait-seconds must be an integer from 1 to 3600'); - } - options.waitSeconds = value; - continue; - } - throw new Error(`unknown argument: ${argument}`); - } - return options; -} - -async function main() { - const options = parseArguments(process.argv.slice(2)); - runDocker([...composeArgs(), 'config', '--quiet']); - if (options.start) runDocker([...composeArgs(), 'up', '-d']); - - const deadline = Date.now() + options.waitSeconds * 1000; - let last = new Map(); - while (Date.now() <= deadline) { - last = new Map(services.map((service) => [service, inspectHealth(service)])); - if ( - [...last.values()].every(({ state, health }) => state === 'running' && health === 'healthy') - ) { - console.log(`Local services ready (${services.join(', ')}).`); - return; - } - const summary = services.map((service) => `${service}=${last.get(service).detail}`).join(' '); - process.stdout.write(`Waiting for local services: ${summary}\r`); - await delay(1000); - } - - console.error('\nLocal services did not become ready:'); - for (const service of services) - console.error(`- ${service}: ${last.get(service)?.detail ?? 'unknown'}`); - process.exitCode = 1; -} - -main().catch((error) => { +main(process.argv.slice(2)).catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; }); diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs new file mode 100644 index 00000000..eba64360 --- /dev/null +++ b/tools/repo-cli/src/local-services.mjs @@ -0,0 +1,289 @@ +import { existsSync, readFileSync, statfsSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import net from 'node:net'; +import path from 'node:path'; +import { setTimeout as delay } from 'node:timers/promises'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); +const localRoot = path.join(repositoryRoot, 'infrastructure', 'local'); +const composeFile = path.join(localRoot, 'compose.yml'); +const exampleEnvFile = path.join(localRoot, '.env.example'); +const localEnvFile = path.join(localRoot, '.env'); +const services = [ + 'postgres', + 'redis', + 'minio', + 'mailpit', + 'otel-collector', + 'otel-collector-health', +]; +const hostPorts = [ + { service: 'postgres', key: 'POSTGRES_PORT', fallback: 5432 }, + { service: 'redis', key: 'REDIS_PORT', fallback: 6379 }, + { service: 'minio', key: 'MINIO_API_PORT', fallback: 9000 }, + { service: 'minio', key: 'MINIO_CONSOLE_PORT', fallback: 9001 }, + { service: 'mailpit', key: 'MAILPIT_SMTP_PORT', fallback: 1025 }, + { service: 'mailpit', key: 'MAILPIT_UI_PORT', fallback: 8025 }, + { service: 'otel-collector', key: 'OTEL_GRPC_PORT', fallback: 4317 }, + { service: 'otel-collector', key: 'OTEL_HTTP_PORT', fallback: 4318 }, + { service: 'otel-collector', key: 'OTEL_HEALTH_PORT', fallback: 13133 }, +]; + +function usage() { + console.log(`Usage: pnpm local:services [options] + +Commands (all preserve named volumes): + check validate Compose, ports, Docker, and disk headroom + start validate, start services, and wait for healthy checks + stop stop containers without removing containers or volumes + reset recreate containers and networks, preserving volumes + restart-check restart running services and verify health/persistence + status print current container and health state + smoke legacy readiness command (use --start to start first) + +Options: + --start with smoke, start services before polling + --wait-seconds=N readiness timeout (default: 60, maximum: 3600) + --min-free-gib=N minimum host free space (default: 5) + --help show this help + +Smoke never removes containers or named volumes. Lifecycle reset recreates +containers but never passes --volumes to Compose. Use Docker directly only when +you explicitly intend to discard local development data.`); +} + +function fail(message) { + throw new Error(`Local infrastructure: ${message}`); +} + +function parseEnvFile(file) { + if (!existsSync(file)) return new Map(); + const values = new Map(); + for (const line of readFileSync(file, 'utf8').split(/\r?\n/u)) { + const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)\s*$/u.exec(line); + if (!match || match[1].startsWith('#')) continue; + values.set(match[1], match[2].replace(/^(['"])(.*)\1$/u, '$2')); + } + return values; +} + +function environment() { + const fileValues = parseEnvFile(existsSync(localEnvFile) ? localEnvFile : exampleEnvFile); + for (const definition of hostPorts) { + if (process.env[definition.key] !== undefined) fileValues.set(definition.key, process.env[definition.key]); + } + return fileValues; +} + +function portValue(definition, values) { + const value = Number(values.get(definition.key) ?? definition.fallback); + if (!Number.isInteger(value) || value < 1024 || value > 65535) { + fail(`${definition.key} must be an integer between 1024 and 65535`); + } + return value; +} + +function composeArgs(values = environment()) { + const envFile = existsSync(localEnvFile) ? localEnvFile : exampleEnvFile; + const project = values.get('COMPOSE_PROJECT_NAME') || process.env.COMPOSE_PROJECT_NAME || 'databreeze-local'; + return ['compose', '--project-name', project, '--env-file', envFile, '-f', composeFile]; +} + +function runDocker(args, { allowFailure = false } = {}) { + const result = spawnSync('docker', args, { cwd: repositoryRoot, encoding: 'utf8' }); + if (!allowFailure && (result.error || result.status !== 0)) { + if (result.error?.code === 'ENOENT') { + fail('Docker CLI is not installed or not on PATH; start Docker Desktop before using this command'); + } + const detail = (result.stderr || result.stdout || result.error?.message || '').trim(); + fail(`docker ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); + } + return result; +} + +function requireDocker() { + const result = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + if (result.error?.code === 'ENOENT') { + fail('Docker CLI is not installed or not on PATH; start Docker Desktop before using this command'); + } + if (result.status !== 0) { + fail('Docker daemon is unavailable; start Docker Desktop or another Docker Engine before using this command'); + } +} + +function validateCompose(values) { + runDocker([...composeArgs(values), 'config', '--quiet']); +} + +function ensureDiskSpace(minFreeGib) { + if (typeof statfsSync !== 'function') { + console.warn('Local infrastructure: disk free-space check is unavailable on this Node runtime'); + return; + } + const stats = statfsSync(repositoryRoot); + const freeBytes = Number(stats.bavail) * Number(stats.bsize); + const requiredBytes = minFreeGib * 1024 ** 3; + if (freeBytes < requiredBytes) { + fail(`host free space is ${(freeBytes / 1024 ** 3).toFixed(2)} GiB; at least ${minFreeGib} GiB is required`); + } +} + +function containerRunning(service, values) { + const id = runDocker([...composeArgs(values), 'ps', '-q', service], { allowFailure: true }).stdout.trim(); + if (!id) return false; + const state = runDocker(['inspect', '--format', '{{.State.Running}}', id], { allowFailure: true }); + return state.stdout.trim() === 'true'; +} + +function portAvailable(port) { + return new Promise((resolve) => { + const server = net.createServer(); + const finish = (available) => { + server.removeAllListeners(); + if (server.listening) server.close(() => resolve(available)); + else resolve(available); + }; + server.once('error', () => finish(false)); + server.listen({ host: '127.0.0.1', port }, () => finish(true)); + }); +} + +async function ensurePorts(values) { + const collisions = []; + for (const definition of hostPorts) { + if (containerRunning(definition.service, values)) continue; + const port = portValue(definition, values); + if (!(await portAvailable(port))) collisions.push(`${definition.key}=${port} (${definition.service})`); + } + if (collisions.length > 0) { + fail(`host ports are already in use: ${collisions.join(', ')}; set alternate ports in infrastructure/local/.env`); + } +} + +function inspectHealth(service, values) { + const idResult = runDocker([...composeArgs(values), 'ps', '-q', service], { allowFailure: true }); + const id = idResult.stdout.trim(); + if (!id) return { state: 'missing', health: 'unknown', detail: 'no container' }; + const inspect = runDocker([ + 'inspect', + '--format', + '{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health{{end}}', + id, + ]); + const [state, health] = inspect.stdout.trim().split('|'); + return { state, health, detail: `${state}/${health}` }; +} + +async function waitForReady(values, waitSeconds) { + const deadline = Date.now() + waitSeconds * 1000; + let last = new Map(); + while (Date.now() <= deadline) { + last = new Map(services.map((service) => [service, inspectHealth(service, values)])); + if ([...last.values()].every(({ state, health }) => state === 'running' && health === 'healthy')) { + console.log(`Local services ready (${services.join(', ')}).`); + return; + } + const summary = services.map((service) => `${service}=${last.get(service).detail}`).join(' '); + process.stdout.write(`Waiting for local services: ${summary}\r`); + await delay(1000); + } + console.error('\nLocal services did not become ready:'); + for (const service of services) console.error(`- ${service}: ${last.get(service)?.detail ?? 'unknown'}`); + fail(`readiness timeout after ${waitSeconds}s`); +} + +function parseArguments(argv) { + let command = 'smoke'; + const argumentsToParse = [...argv]; + if (argumentsToParse[0] && !argumentsToParse[0].startsWith('-')) command = argumentsToParse.shift(); + const options = { + start: false, + waitSeconds: 60, + minFreeGib: Number(process.env.DATABREEZE_MIN_FREE_GIB || 5), + }; + for (const argument of argumentsToParse) { + if (argument === '--help' || argument === '-h') return { command: 'help', options }; + if (argument === '--start') { + options.start = true; + continue; + } + if (argument.startsWith('--wait-seconds=')) { + options.waitSeconds = Number(argument.slice('--wait-seconds='.length)); + continue; + } + if (argument.startsWith('--min-free-gib=')) { + options.minFreeGib = Number(argument.slice('--min-free-gib='.length)); + continue; + } + fail(`unknown argument: ${argument}`); + } + if (!Number.isInteger(options.waitSeconds) || options.waitSeconds < 1 || options.waitSeconds > 3600) { + fail('--wait-seconds must be an integer from 1 to 3600'); + } + if (!Number.isFinite(options.minFreeGib) || options.minFreeGib < 0) { + fail('--min-free-gib must be a non-negative number'); + } + if (!['check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke'].includes(command)) { + fail(`unknown command: ${command}`); + } + return { command, options }; +} + +export async function main(argv = process.argv.slice(2)) { + const { command, options } = parseArguments(argv); + if (command === 'help') { + usage(); + return; + } + const values = environment(); + requireDocker(); + validateCompose(values); + + if (command === 'status') { + for (const service of services) console.log(`${service}: ${inspectHealth(service, values).detail}`); + return; + } + if (command === 'stop') { + runDocker([...composeArgs(values), 'stop']); + console.log('Local services stopped; named volumes and containers were preserved.'); + return; + } + + const shouldStart = command === 'start' || command === 'reset' || (command === 'smoke' && options.start); + if (shouldStart) { + ensureDiskSpace(options.minFreeGib); + await ensurePorts(values); + } + if (command === 'check') { + ensureDiskSpace(options.minFreeGib); + await ensurePorts(values); + console.log('Local Compose, Docker, port, and disk preflight passed.'); + return; + } + if (command === 'reset') { + runDocker([...composeArgs(values), 'down', '--remove-orphans']); + runDocker([...composeArgs(values), 'up', '-d']); + await waitForReady(values, options.waitSeconds); + console.log('Local services reset without removing named volumes.'); + return; + } + if (command === 'restart-check') { + runDocker([...composeArgs(values), 'restart']); + await waitForReady(values, options.waitSeconds); + console.log('Local service restart and health persistence check passed.'); + return; + } + if (shouldStart) runDocker([...composeArgs(values), 'up', '-d']); + await waitForReady(values, options.waitSeconds); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + }); +} diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 1610c9fa..297cb8ab 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -74,3 +74,23 @@ test('readiness smoke script exposes a non-destructive help command', () => { assert.match(result.stdout, /--start/); assert.match(result.stdout, /never removes containers or named volumes/i); }); + +test('local lifecycle commands fail safely around Docker, ports, disk, and volumes', () => { + const script = read('tools/repo-cli/src/local-services.mjs'); + const helpScript = path.join(repositoryRoot, 'tools', 'repo-cli', 'src', 'local-services.mjs'); + const result = spawnSync(process.execPath, [helpScript, '--help'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr); + for (const command of ['check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke']) { + assert.match(result.stdout, new RegExp(`^ ${command}\\s`, 'm')); + } + assert.match(script, /statfsSync/u); + assert.match(script, /portAvailable/u); + assert.match(script, /Docker CLI is not installed/u); + assert.match(script, /Docker daemon is unavailable/u); + assert.match(script, /down', '--remove-orphans/u); + assert.doesNotMatch(script, /down'[^\n]*--volumes/u); + assert.doesNotMatch(script, /down\s+--volumes/u); +}); From 8ba1d4eb2cbf62d4bf447e607252847b3810ab8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:00:26 +0700 Subject: [PATCH 03/89] docs(infra): record local foundation evidence --- ...ndation-local-infrastructure-2026-08-02.md | 43 +++++++++++++++++++ docs/plans/execution-orchestration.json | 12 ++++++ 2 files changed, 55 insertions(+) create mode 100644 docs/operations/foundation-local-infrastructure-2026-08-02.md diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md new file mode 100644 index 00000000..a8c48cc6 --- /dev/null +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -0,0 +1,43 @@ +# FND-003 local infrastructure evidence + +Observed at (UTC): 2026-08-02 +Branch: `feat/fnd003-local-infra-batch` +Task: `FND-003 — Close local infrastructure gaps` + +## Implemented boundaries + +- PostgreSQL 17.5, Redis 7.4.5, MinIO, Mailpit, and the OpenTelemetry collector + remain pinned in `infrastructure/local/compose.yml`. +- PostgreSQL initialization creates every module-owned schema and contains no + credentials, roles, or secret literals. +- `tools/repo-cli/src/local-services.mjs` provides `check`, `start`, `stop`, + `reset`, `restart-check`, `status`, and legacy `smoke` commands. +- Lifecycle commands preserve named volumes. `reset` uses Compose + `down --remove-orphans` without `--volumes`; data deletion is never implicit. +- Preflight reports missing Docker CLI/daemon, host port collisions, and + insufficient free disk space before starting containers. +- `restart-check` restarts the running stack and waits for every service health + check, providing the entry point for persistence evidence. + +## Verification + +Passed: + +- `node --test tools/repo-cli/test/local-infrastructure.test.mjs` +- `node tools/repo-cli/src/local-services.mjs --help` +- `node tools/repo-cli/src/local-services-smoke.mjs --help` +- `git diff --check` + +Environment-gated: + +- `node tools/repo-cli/src/local-services.mjs check` fails closed with + `Docker daemon is unavailable` because no Docker daemon is running here. +- Live `compose up`, health polling, port-collision simulation, disk-pressure + threshold validation, and restart-persistence checks must run on a machine + with Docker Desktop/Compose v2 before FND-003 can become `verified`. + +## Rollback + +Revert the lifecycle commit and retain the prior static Compose checks. No +containers, named volumes, host files, or credentials are modified by the +repository changes. diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index de7dc3e8..4eb6156d 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -77,6 +77,18 @@ "tools/repo-cli/test/android-shell.test.mjs" ], "note": "Room, WorkManager, Keystore, bilingual resources, generated contracts/tokens, backup/network policy, account isolation, and process-recreation evidence are complete. No product requirement status was promoted." + }, + "FND-003": { + "status": "in-progress", + "commit": "4eff6f5", + "evidence": [ + "infrastructure/local/compose.yml", + "infrastructure/local/postgres/init/001-create-module-schemas.sql", + "tools/repo-cli/src/local-services.mjs", + "tools/repo-cli/test/local-infrastructure.test.mjs", + "docs/operations/foundation-local-infrastructure-2026-08-02.md" + ], + "note": "Static Compose/bootstrap/lifecycle checks pass. Live Docker health, port-collision, disk-pressure, and restart-persistence evidence remains pending because the Docker daemon is unavailable in this environment." } }, "plans": [ From b0a53c14420573f527b9802e12964f398489b453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:01:12 +0700 Subject: [PATCH 04/89] fix(infra): reject duplicate local host ports --- tools/repo-cli/src/local-services.mjs | 9 ++++++++- tools/repo-cli/test/local-infrastructure.test.mjs | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index eba64360..2b2a2df9 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -154,9 +154,16 @@ function portAvailable(port) { async function ensurePorts(values) { const collisions = []; + const configured = new Map(); for (const definition of hostPorts) { - if (containerRunning(definition.service, values)) continue; const port = portValue(definition, values); + const prior = configured.get(port); + if (prior && prior.key !== definition.key) { + collisions.push(`${prior.key}=${port} and ${definition.key}=${port}`); + continue; + } + configured.set(port, definition); + if (containerRunning(definition.service, values)) continue; if (!(await portAvailable(port))) collisions.push(`${definition.key}=${port} (${definition.service})`); } if (collisions.length > 0) { diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 297cb8ab..adab493e 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -88,6 +88,7 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum } assert.match(script, /statfsSync/u); assert.match(script, /portAvailable/u); + assert.match(script, /configured = new Map/u); assert.match(script, /Docker CLI is not installed/u); assert.match(script, /Docker daemon is unavailable/u); assert.match(script, /down', '--remove-orphans/u); From 9ce6b6b57006595dee7a61209763176301565df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:01:27 +0700 Subject: [PATCH 05/89] test(infra): cover local preflight argument failures --- tools/repo-cli/test/local-infrastructure.test.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index adab493e..16c68826 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -94,4 +94,18 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.match(script, /down', '--remove-orphans/u); assert.doesNotMatch(script, /down'[^\n]*--volumes/u); assert.doesNotMatch(script, /down\s+--volumes/u); + + const invalidTimeout = spawnSync(process.execPath, [helpScript, 'check', '--wait-seconds=0'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(invalidTimeout.status, 0); + assert.match(`${invalidTimeout.stdout}\n${invalidTimeout.stderr}`, /--wait-seconds must be an integer/u); + + const invalidDisk = spawnSync(process.execPath, [helpScript, 'check', '--min-free-gib=-1'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(invalidDisk.status, 0); + assert.match(`${invalidDisk.stdout}\n${invalidDisk.stderr}`, /--min-free-gib must be a non-negative number/u); }); From 9f2d047e6fb21a602079848bbce14c47e9ed3c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:01:58 +0700 Subject: [PATCH 06/89] docs(infra): document safe local lifecycle --- infrastructure/local/README.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index 371b774e..697b116c 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -9,17 +9,33 @@ through adapters, so the same contracts work with managed services later. 1. Copy `.env.example` to `.env` and change the local-only values if needed. 2. Start Docker Desktop (or another Docker Engine with Compose v2). -3. Run `pnpm local:smoke -- --start` from the repository root. +3. Run `pnpm local:services start` from the repository root. 4. Open Mailpit at and MinIO Console at when you need to inspect local data. The stack is defined in [`compose.yml`](compose.yml). All state is held in named volumes prefixed by the Compose project name; no repository directory is mounted for database, object, or mail data. The volumes are disposable and are -not removed by the smoke script. Use `docker compose --env-file -infrastructure/local/.env -f infrastructure/local/compose.yml down` to stop -the containers. Remove the named volumes only when you explicitly want to -discard local state. +not removed by the lifecycle commands. Remove the named volumes only when you +explicitly want to discard local state. + +## Lifecycle commands + +Run these from the repository root: + +| Command | Effect | +| --- | --- | +| `pnpm local:services check` | Validate Compose, Docker, host ports, and free disk without starting anything. | +| `pnpm local:services start` | Run preflight, start the stack, and wait for every health check. | +| `pnpm local:services stop` | Stop containers while preserving containers and named volumes. | +| `pnpm local:services reset` | Recreate containers/networks while preserving named volumes; it never passes `--volumes`. | +| `pnpm local:services restart-check` | Restart the running stack and verify health after restart. | +| `pnpm local:services status` | Print container/health state without changing it. | + +The older `pnpm local:smoke -- --start` form remains supported. Port collisions +can be resolved by copying `.env.example` to `.env` and changing the host port +variables. `check` fails closed when Docker is missing, the daemon is stopped, +or free space is below the configured threshold (`--min-free-gib=N`). ## Services @@ -44,10 +60,10 @@ package manager while still making readiness observable. - These images and credentials are for local development. Never copy `.env` into a deployment or commit it. - The Compose health checks are the readiness contract for local consumers. - `pnpm local:smoke` validates the Compose file and reports the first unhealthy - service. Add `--start` to bring the stack up before polling. -- If a previous run left a stopped container, rerun the smoke command; it is - idempotent and does not delete volumes. + `pnpm local:services status` reports the current health and + `pnpm local:services restart-check` verifies restart persistence. +- If a previous run left a stopped container, rerun `pnpm local:services start`; + it is idempotent and does not delete volumes. - If Docker is unavailable, the static infrastructure tests still validate the service definitions, image release lines, volume names, and credential-free initialization files. From b60d942a706f0a5222743f47a116ce9746ba5de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:02:33 +0700 Subject: [PATCH 07/89] test(infra): enforce hosted safety boundaries --- tools/repo-cli/src/check-aws-infrastructure.mjs | 17 +++++++++++++++++ tools/repo-cli/test/aws-infrastructure.test.mjs | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index ab3dde11..33d6bef9 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -45,6 +45,23 @@ for (const requiredText of [ ]) { if (!allTerraform.includes(requiredText)) fail(`missing required declaration ${requiredText}`); } +for (const requiredBoundary of [ + 'block_public_policy', + 'versioning_configuration', + 'assign_public_ip = false', + 'deletion_protection', + 'backup_retention_period', + 'token.actions.githubusercontent.com:sub', + 'recovery_window_in_days = 30', +]) { + if (!allTerraform.includes(requiredBoundary)) fail(`missing required safety boundary ${requiredBoundary}`); +} +if (/ingress[\s\S]{0,400}cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u.test(allTerraform)) { + fail('a private service security group permits unrestricted ingress'); +} +if (/resource\s+"aws_s3_bucket_policy"[\s\S]*?Principal\s*=\s*"\*"/u.test(allTerraform)) { + fail('the Web bucket policy grants a wildcard principal'); +} if ( /AKIA[0-9A-Z]{16}|aws_secret_access_key\s*=|BEGIN (RSA|OPENSSH) PRIVATE KEY/.test(allTerraform) ) { diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index e8274103..b3754938 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -65,6 +65,9 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s ]) assert.match(sources, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); assert.doesNotMatch(sources, /AKIA[0-9A-Z]{16}|BEGIN (RSA|OPENSSH) PRIVATE KEY/); + assert.doesNotMatch(sources, /ingress[\s\S]{0,400}cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u); + assert.match(sources, /assign_public_ip\s*=\s*false/u); + assert.match(sources, /token\.actions\.githubusercontent\.com:sub/u); }); test('AWS validation script is non-applying and reports missing OpenTofu clearly', () => { @@ -79,4 +82,6 @@ test('AWS validation script is non-applying and reports missing OpenTofu clearly assert.match(source, /init', '-backend=false/); assert.match(source, /validate', '-no-color/); assert.match(source, /process\.exitCode \?\? 0/); + assert.match(source, /missing required safety boundary/u); + assert.doesNotMatch(source, /tofu',\s*\['apply'/u); }); From caa090a406c0ba5101467c9bbc2015d0ee20e467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:03:02 +0700 Subject: [PATCH 08/89] test(infra): verify production recovery defaults --- tools/repo-cli/test/aws-infrastructure.test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index b3754938..cd145999 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -85,3 +85,18 @@ test('AWS validation script is non-applying and reports missing OpenTofu clearly assert.match(source, /missing required safety boundary/u); assert.doesNotMatch(source, /tofu',\s*\['apply'/u); }); + +test('AWS production profile enables recovery and prevents public data paths', () => { + const production = read('infrastructure/aws/environments/alpha/production.tfvars.example'); + const versions = read('infrastructure/aws/environments/alpha/versions.tf'); + const data = read('infrastructure/aws/modules/data/main.tf'); + const compute = read('infrastructure/aws/modules/compute/main.tf'); + assert.match(production, /backup_retention_period\s*=\s*7/u); + assert.match(production, /deletion_protection\s*=\s*true/u); + assert.match(production, /database_multi_az\s*=\s*true/u); + assert.match(production, /redis_automatic_failover_enabled\s*=\s*true/u); + assert.match(versions, /required_version\s*=\s*">= 1\.8\.0, < 2\.0\.0"/u); + assert.doesNotMatch(data, /publicly_accessible\s*=\s*true/u); + assert.doesNotMatch(data, /skip_final_snapshot\s*=\s*true/u); + assert.match(compute, /assign_public_ip\s*=\s*false/u); +}); From 7d6d1e31391a0e1ac28c15b516cd8880069465cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:03:44 +0700 Subject: [PATCH 09/89] feat(infra): add daemon-free compose validation --- infrastructure/local/README.md | 1 + tools/repo-cli/src/local-services.mjs | 8 +++++++- tools/repo-cli/test/local-infrastructure.test.mjs | 8 +++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index 697b116c..3b3c5042 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -25,6 +25,7 @@ Run these from the repository root: | Command | Effect | | --- | --- | +| `pnpm local:services config` | Validate Compose syntax without requiring a running Docker daemon. | | `pnpm local:services check` | Validate Compose, Docker, host ports, and free disk without starting anything. | | `pnpm local:services start` | Run preflight, start the stack, and wait for every health check. | | `pnpm local:services stop` | Stop containers while preserving containers and named volumes. | diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 2b2a2df9..d064b5e0 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -34,6 +34,7 @@ function usage() { console.log(`Usage: pnpm local:services [options] Commands (all preserve named volumes): + config validate Compose syntax without requiring a Docker daemon check validate Compose, ports, Docker, and disk headroom start validate, start services, and wait for healthy checks stop stop containers without removing containers or volumes @@ -234,7 +235,7 @@ function parseArguments(argv) { if (!Number.isFinite(options.minFreeGib) || options.minFreeGib < 0) { fail('--min-free-gib must be a non-negative number'); } - if (!['check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke'].includes(command)) { + if (!['config', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke'].includes(command)) { fail(`unknown command: ${command}`); } return { command, options }; @@ -247,6 +248,11 @@ export async function main(argv = process.argv.slice(2)) { return; } const values = environment(); + if (command === 'config') { + validateCompose(values); + console.log('Local Compose configuration is valid.'); + return; + } requireDocker(); validateCompose(values); diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 16c68826..1e3beab2 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -83,7 +83,7 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); - for (const command of ['check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke']) { + for (const command of ['config', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke']) { assert.match(result.stdout, new RegExp(`^ ${command}\\s`, 'm')); } assert.match(script, /statfsSync/u); @@ -108,4 +108,10 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum }); assert.notEqual(invalidDisk.status, 0); assert.match(`${invalidDisk.stdout}\n${invalidDisk.stderr}`, /--min-free-gib must be a non-negative number/u); + + const composeConfig = spawnSync(process.execPath, [helpScript, 'config'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + if (composeConfig.status === 0) assert.match(composeConfig.stdout, /Compose configuration is valid/u); }); From 77deac37fc7312de9ea7893334b8ab27e974f306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:03:56 +0700 Subject: [PATCH 10/89] docs(infra): record compose validation evidence --- docs/operations/foundation-local-infrastructure-2026-08-02.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index a8c48cc6..b8994001 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -26,6 +26,7 @@ Passed: - `node --test tools/repo-cli/test/local-infrastructure.test.mjs` - `node tools/repo-cli/src/local-services.mjs --help` - `node tools/repo-cli/src/local-services-smoke.mjs --help` +- `node tools/repo-cli/src/local-services.mjs config` - `git diff --check` Environment-gated: From 5de31b07a98edfe1db0b71d1ff3866bd37c75c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:05:03 +0700 Subject: [PATCH 11/89] ci: include infrastructure validation in repo checks --- docs/development/README.md | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/development/README.md b/docs/development/README.md index 3f3c9583..abb037a2 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -10,7 +10,8 @@ Android companion, API, and Python engine. OpenTofu when infrastructure validation is needed. The repository pins the package manager and checks runtime versions. 2. Run `corepack pnpm repo:bootstrap`. -3. Run `corepack pnpm repo:check` and `corepack pnpm repo:build`. A clean +3. Run `corepack pnpm repo:check` and `corepack pnpm repo:build`. The check + includes non-applying local/AWS infrastructure validation. A clean checkout is the baseline; do not hide a failure with an untracked local configuration file. 4. Copy `infrastructure/local/.env.example` to a local-only `.env` if needed, diff --git a/package.json b/package.json index 535fcc24..b62a858b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "orchestration:check": "node tools/repo-cli/src/check-execution-orchestration.mjs", "repo:bootstrap": "corepack pnpm install --frozen-lockfile", "repo:build": "corepack pnpm build", - "repo:check": "corepack pnpm format:check && corepack pnpm lint && corepack pnpm typecheck && corepack pnpm orchestration:check && corepack pnpm requirements:check && corepack pnpm contracts:check && corepack pnpm test", + "repo:check": "corepack pnpm format:check && corepack pnpm lint && corepack pnpm typecheck && corepack pnpm orchestration:check && corepack pnpm requirements:check && corepack pnpm contracts:check && corepack pnpm infra:check && corepack pnpm test", "repo:dev": "turbo run dev --parallel", "repo:test": "corepack pnpm test", "requirements:check": "node tools/repo-cli/src/generate-requirement-index.mjs --check", From 917e3132eb26cb0a24323b77c92a1d964254351c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:05:22 +0700 Subject: [PATCH 12/89] docs(operations): update foundation verification gates --- docs/operations/engineering-foundation-verification.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/operations/engineering-foundation-verification.md b/docs/operations/engineering-foundation-verification.md index 3b74b1f0..f1d18326 100644 --- a/docs/operations/engineering-foundation-verification.md +++ b/docs/operations/engineering-foundation-verification.md @@ -12,13 +12,13 @@ environment limits that remain explicit. | Area | Command | Result | |---|---|---| -| JavaScript workspace | `corepack pnpm repo:check` | Pass; formatting, lint, typecheck, requirements, contracts, repository tests, and 19 Turborepo test tasks passed. | +| JavaScript workspace | `corepack pnpm repo:check` | Pass; formatting, lint, typecheck, requirements, contracts, infrastructure policy, repository tests, and 19 Turborepo test tasks passed. | | JavaScript builds | `corepack pnpm repo:build` | Pass; API, Web, Desktop, shared packages, and engine package build completed. | | Contracts | `corepack pnpm --filter @databreeze/contracts contract:check` | Pass; generated models, compatibility baseline, and 28-case TypeScript/Python/Kotlin parity passed. | | Engine | `uv sync --locked --offline`; engine test/lint/typecheck/build through the root gates | Pass; locked Python 3.13 environment and package build verified. | | Android | `apps/android/gradlew :app:testDebugUnitTest --no-daemon` | Pass; 27 Gradle tasks completed. | | Brand | `corepack pnpm brand:check` and design-token build checks | Pass; canonical legacy asset checksums and derivatives remain unchanged. | -| AWS static validation | `corepack pnpm infra:check` | Pass; static checks passed and no infrastructure was applied. | +| AWS/static infrastructure | `corepack pnpm infra:check`; `node tools/repo-cli/src/local-services.mjs config` | Pass; static AWS checks and daemon-free local Compose validation passed; no infrastructure or containers were applied. | The contract drift check ignores only `uv`/Hatch editable-install products under `generated/python/build` and `generated/python/*.egg-info`; a regression @@ -31,8 +31,10 @@ products are never committed. executed locally. CI must run them with the pinned OpenTofu release before an infrastructure change is approved. - Docker was not required for the repository gates and live local dependency - startup was not claimed. Run `corepack pnpm local:smoke` on a machine with a - healthy Docker daemon before dogfood acceptance. + startup was not claimed. Run `corepack pnpm local:services check` and then + `corepack pnpm local:services restart-check` on a machine with a healthy + Docker daemon before dogfood acceptance. See + `foundation-local-infrastructure-2026-08-02.md` for the FND-003 boundary. - Android instrumentation/emulator and signed release packaging are separate production-readiness gates; the shell unit test is the evidence recorded here. From 69c4459f49f4c71c93bff0d6ecca4d20f59a3ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:05:46 +0700 Subject: [PATCH 13/89] fix(telemetry): ignore accessor-backed diagnostics --- packages/telemetry/src/v1.ts | 14 ++++++++++++-- packages/telemetry/test/telemetry-v1.test.mjs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/telemetry/src/v1.ts b/packages/telemetry/src/v1.ts index 95118273..5977b628 100644 --- a/packages/telemetry/src/v1.ts +++ b/packages/telemetry/src/v1.ts @@ -166,12 +166,21 @@ function safeScalar(key: string, value: unknown): TelemetryScalarV1 | undefined return safeString(key, value); } +function ownDataEntries(input: Record): Array<[string, unknown]> { + const entries: Array<[string, unknown]> = []; + for (const key of Object.keys(input)) { + const descriptor = Object.getOwnPropertyDescriptor(input, key); + if (descriptor && 'value' in descriptor) entries.push([key, descriptor.value]); + } + return entries; +} + export function sanitizeTelemetryAttributesV1( input: Record, ): SafeTelemetryAttributesV1 { if (input === null || typeof input !== 'object' || Array.isArray(input)) return {}; const output: SafeTelemetryAttributesV1 = {}; - for (const [key, value] of Object.entries(input)) { + for (const [key, value] of ownDataEntries(input)) { assertBoundedKey(key); if (!safeAttributeSet.has(key)) continue; const scalar = safeScalar(key, value); @@ -188,7 +197,8 @@ export function assertSafeTelemetryAttributesV1( if (forbiddenKeyPattern.test(key) || !safeAttributeSet.has(key)) { throw new UnsafeTelemetryAttributeErrorV1(key); } - if (safeScalar(key, input[key]) === undefined) { + const descriptor = Object.getOwnPropertyDescriptor(input, key); + if (!descriptor || !('value' in descriptor) || safeScalar(key, descriptor.value) === undefined) { throw new UnsafeTelemetryAttributeErrorV1(key); } } diff --git a/packages/telemetry/test/telemetry-v1.test.mjs b/packages/telemetry/test/telemetry-v1.test.mjs index 3ff031d3..3c7daf31 100644 --- a/packages/telemetry/test/telemetry-v1.test.mjs +++ b/packages/telemetry/test/telemetry-v1.test.mjs @@ -55,6 +55,22 @@ test('strict assertions reject secrets, paths, content, and unbounded values', ( ); }); +test('telemetry never executes accessor-backed attributes', () => { + let accessed = false; + const hostile = {}; + Object.defineProperty(hostile, 'outcome', { + enumerable: true, + get() { + accessed = true; + throw new Error('hostile getter'); + }, + }); + + assert.deepEqual(sanitizeTelemetryAttributesV1(hostile), {}); + assert.throws(() => assertSafeTelemetryAttributesV1(hostile), UnsafeTelemetryAttributeErrorV1); + assert.equal(accessed, false); +}); + test('correlation headers round-trip without accepting malformed identifiers', () => { const context = createCorrelationContextV1({ correlationId, From b51195c74a54ca0f885526485f2c284cebbf3bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:06:06 +0700 Subject: [PATCH 14/89] docs(telemetry): describe hostile field handling --- packages/telemetry/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/telemetry/README.md b/packages/telemetry/README.md index 192da062..50a7462c 100644 --- a/packages/telemetry/README.md +++ b/packages/telemetry/README.md @@ -14,6 +14,10 @@ Unknown keys are dropped. Sensitive keys and values (tokens, secrets, paths, filenames, source values, prompts, evidence, contact data, and content) are rejected by the strict assertion helper and never serialized. +Sanitization reads only own data properties. Accessor-backed or hostile +diagnostic fields are ignored (or rejected by the strict helper) without +executing their getters. + Use `createStructuredLoggerV1` at runtime boundaries and pass only the correlation headers produced by `correlationHeadersV1`. A logger sink receives one JSON object per event; it never receives a source payload or an exception From da3d7325791dd83ad672cc388becc59adbcd0629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:06:25 +0700 Subject: [PATCH 15/89] ci: route infrastructure changes through shared gates --- tools/repo-cli/src/detect-change-scope.mjs | 1 + tools/repo-cli/test/change-scope.test.mjs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tools/repo-cli/test/change-scope.test.mjs diff --git a/tools/repo-cli/src/detect-change-scope.mjs b/tools/repo-cli/src/detect-change-scope.mjs index fe8bb452..d2f46294 100644 --- a/tools/repo-cli/src/detect-change-scope.mjs +++ b/tools/repo-cli/src/detect-change-scope.mjs @@ -14,6 +14,7 @@ const shared = matches([ 'tools/', 'docs/specs/', 'docs/plans/', + 'infrastructure/', '.github/', 'package.json', 'pnpm-lock.yaml', diff --git a/tools/repo-cli/test/change-scope.test.mjs b/tools/repo-cli/test/change-scope.test.mjs new file mode 100644 index 00000000..deb02318 --- /dev/null +++ b/tools/repo-cli/test/change-scope.test.mjs @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + +test('infrastructure changes are shared quality-gate inputs', () => { + const source = readFileSync( + path.join(repositoryRoot, 'tools', 'repo-cli', 'src', 'detect-change-scope.mjs'), + 'utf8', + ); + assert.match(source, /'infrastructure\/'/u); + assert.match(source, /infrastructure: matches\(\['infrastructure\/'\]\)/u); +}); From 7fc973b86e20aa1f440fad29b1a9cf1c9d428dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:06:37 +0700 Subject: [PATCH 16/89] fix(ci): correct infrastructure scope assertion --- tools/repo-cli/test/change-scope.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/repo-cli/test/change-scope.test.mjs b/tools/repo-cli/test/change-scope.test.mjs index deb02318..ba376772 100644 --- a/tools/repo-cli/test/change-scope.test.mjs +++ b/tools/repo-cli/test/change-scope.test.mjs @@ -12,5 +12,5 @@ test('infrastructure changes are shared quality-gate inputs', () => { 'utf8', ); assert.match(source, /'infrastructure\/'/u); - assert.match(source, /infrastructure: matches\(\['infrastructure\/'\]\)/u); + assert.match(source, /infrastructure: shared \|\| matches\(\['infrastructure\/'\]\)/u); }); From fcabfb3be52b637d6740b30a826aa2de1d27a206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:06:54 +0700 Subject: [PATCH 17/89] docs(operations): add foundation batch handoff --- .../foundation-batch-handoff-2026-08-02.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/operations/foundation-batch-handoff-2026-08-02.md diff --git a/docs/operations/foundation-batch-handoff-2026-08-02.md b/docs/operations/foundation-batch-handoff-2026-08-02.md new file mode 100644 index 00000000..356a98b6 --- /dev/null +++ b/docs/operations/foundation-batch-handoff-2026-08-02.md @@ -0,0 +1,50 @@ +# Foundation batch handoff + +Observed at (UTC): 2026-08-02 +Canonical repository: `databreeze-platform` +Branch: `feat/fnd003-local-infra-batch` +Base: `origin/dev` at `92b3e9a4d581f3a6947b7a2bf58c8334f4ae0c18` +Promotion base: `origin/main` at `a2fcba34037c1ffd77816be16be75453abfb16fa` + +## Active delivery boundary + +- FND-003 local infrastructure is `in-progress`. Static Compose, bootstrap, + lifecycle, port, Docker-diagnostic, disk-preflight, and daemon-free config + checks are committed. +- Live Docker health, port-collision simulation, disk-pressure threshold, and + restart-persistence evidence remain environment-gated because the Docker + daemon is unavailable on the current machine. +- The batch also hardens AWS static checks, content-safe telemetry handling, + infrastructure path-aware CI, and the updated 30–70 commit policy. These + changes are not marked as verified foundation tasks until their own gates are + recorded. + +## Verification already run + +- Local infrastructure, AWS infrastructure, CI policy, change-scope, telemetry, + orchestration, and diff checks pass in their scoped commands. +- `node tools/repo-cli/src/local-services.mjs config` passes without a Docker + daemon; `check` fails closed with a clear daemon-unavailable reason. +- Android lifecycle review fixes were promoted through PRs #17 and #18; the + existing PR #14 promotion was merged to `main` after hosted checks and its + single CodeRabbit review/incremental dispositions. + +## Git batching rule + +This is a normal feature batch. Keep atomic commits and do not open the feature +PR until the branch reaches at least 30 commits, targeting approximately 70 and +never reaching 100. The only small-PR exceptions are focused promotion-review +fixes required to close an already-open `dev`→`main` gate. + +## Safest next command + +```powershell +git status --short --branch +git fetch origin dev main +node tools/repo-cli/src/check-execution-orchestration.mjs +node --test tools/repo-cli/test/**/*.test.mjs +``` + +After the normal batch reaches its commit boundary, push this branch, open one +PR to `dev` without CodeRabbit, merge after hosted checks, then create the +separate `dev`→`main` promotion PR and invoke CodeRabbit exactly once there. From e424f1a7a618b9679ad4f84f96c7b6056b242552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:07:03 +0700 Subject: [PATCH 18/89] docs(orchestration): record foundation batch checkpoint --- docs/plans/execution-orchestration.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index 4eb6156d..efe43425 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -80,7 +80,7 @@ }, "FND-003": { "status": "in-progress", - "commit": "4eff6f5", + "commit": "fcabfb3", "evidence": [ "infrastructure/local/compose.yml", "infrastructure/local/postgres/init/001-create-module-schemas.sql", @@ -88,7 +88,7 @@ "tools/repo-cli/test/local-infrastructure.test.mjs", "docs/operations/foundation-local-infrastructure-2026-08-02.md" ], - "note": "Static Compose/bootstrap/lifecycle checks pass. Live Docker health, port-collision, disk-pressure, and restart-persistence evidence remains pending because the Docker daemon is unavailable in this environment." + "note": "Static Compose/bootstrap/lifecycle checks, daemon-free config validation, AWS safety checks, telemetry redaction tests, and infrastructure path-aware CI pass. Live Docker health, port-collision, disk-pressure, and restart-persistence evidence remains pending because the Docker daemon is unavailable in this environment." } }, "plans": [ From 76c1882a797c9418e4c02789a70b7d06b131b787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:07:25 +0700 Subject: [PATCH 19/89] test(infra): verify local volume and bucket safety --- tools/repo-cli/test/local-infrastructure.test.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 1e3beab2..d7167928 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -31,6 +31,9 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, new RegExp(`^ ${volume}:`, 'm')); } assert.equal((compose.match(/healthcheck:/g) ?? []).length, 6); + assert.match(compose, /minio-init:[\s\S]*depends_on:[\s\S]*condition: service_healthy/u); + assert.match(compose, /minio-init:[\s\S]*restart: 'no'/u); + assert.match(compose, /postgres-data:[\s\S]*name: \$\{COMPOSE_PROJECT_NAME/u); }); test('local bootstrap is credential-free and creates every owned module schema', () => { @@ -61,6 +64,8 @@ test('local bootstrap is credential-free and creates every owned module schema', const bucketScript = read('infrastructure/local/minio/bootstrap-buckets.sh'); assert.match(bucketScript, /MINIO_ROOT_PASSWORD/); + assert.match(bucketScript, /mc mb --ignore-existing/u); + assert.match(bucketScript, /mc anonymous set none/u); assert.doesNotMatch(bucketScript, /databreeze-local-change-me/); }); From 185fa1d440af79ee24d730304c603551d2533af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:07:34 +0700 Subject: [PATCH 20/89] test(infra): reject destructive local bootstrap SQL --- tools/repo-cli/test/local-infrastructure.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index d7167928..7fba45ae 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -61,6 +61,8 @@ test('local bootstrap is credential-free and creates every owned module schema', ]; for (const schema of expectedSchemas) assert.match(sql, new RegExp(`'${schema}'`)); assert.doesNotMatch(sql, /password|secret|BEGIN\s+;|CREATE\s+ROLE/i); + assert.match(sql, /CREATE SCHEMA IF NOT EXISTS/u); + assert.doesNotMatch(sql, /DROP\s+SCHEMA|DROP\s+DATABASE|TRUNCATE/u); const bucketScript = read('infrastructure/local/minio/bootstrap-buckets.sh'); assert.match(bucketScript, /MINIO_ROOT_PASSWORD/); From 7bd479b5bc15fddf4b7134b6dbe1532dfa527c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:07:48 +0700 Subject: [PATCH 21/89] test(infra): guard lifecycle against destructive Docker cleanup --- tools/repo-cli/test/local-infrastructure.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 7fba45ae..85dc4ca9 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -101,6 +101,7 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.match(script, /down', '--remove-orphans/u); assert.doesNotMatch(script, /down'[^\n]*--volumes/u); assert.doesNotMatch(script, /down\s+--volumes/u); + assert.doesNotMatch(script, /docker\s+(?:rm|volume\s+rm|system\s+prune)/iu); const invalidTimeout = spawnSync(process.execPath, [helpScript, 'check', '--wait-seconds=0'], { cwd: repositoryRoot, From 8e5071fa1345f838e43c8a299ef6ec160d51b35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:07:57 +0700 Subject: [PATCH 22/89] test(infra): pin GitHub OIDC deployment subject --- tools/repo-cli/test/aws-infrastructure.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index cd145999..405fb17f 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -68,6 +68,8 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s assert.doesNotMatch(sources, /ingress[\s\S]{0,400}cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u); assert.match(sources, /assign_public_ip\s*=\s*false/u); assert.match(sources, /token\.actions\.githubusercontent\.com:sub/u); + assert.match(sources, /repo:\$\{var\.github_repository\}:ref:refs\/heads\/dev/u); + assert.doesNotMatch(sources, /refs\/pull|refs\/tags|repo:\*\//u); }); test('AWS validation script is non-applying and reports missing OpenTofu clearly', () => { From 5fa01f640e5cff7bfd6fc0c8a871a29955a5cb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:08:08 +0700 Subject: [PATCH 23/89] test(infra): keep hosted state outside source --- tools/repo-cli/test/aws-infrastructure.test.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 405fb17f..a3b93bd2 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -102,3 +102,15 @@ test('AWS production profile enables recovery and prevents public data paths', ( assert.doesNotMatch(data, /skip_final_snapshot\s*=\s*true/u); assert.match(compute, /assign_public_ip\s*=\s*false/u); }); + +test('AWS foundation keeps state and apply outside the repository', () => { + const readme = read('infrastructure/aws/README.md'); + const sources = [ + read('infrastructure/aws/environments/alpha/main.tf'), + read('infrastructure/aws/environments/alpha/versions.tf'), + ].join('\n'); + assert.match(readme, /plan-only/u); + assert.match(readme, /remote encrypted state backend/u); + assert.doesNotMatch(sources, /^\s*backend\s+"/mu); + assert.doesNotMatch(sources, /terraform\.tfstate|\.tfstate\.backup/u); +}); From 49f82db969998489fae70e9932ffb8f2c559333e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:08:36 +0700 Subject: [PATCH 24/89] feat(infra): configure local disk headroom --- infrastructure/local/.env.example | 1 + tools/repo-cli/test/local-infrastructure.test.mjs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/infrastructure/local/.env.example b/infrastructure/local/.env.example index 92729a75..4d0f8617 100644 --- a/infrastructure/local/.env.example +++ b/infrastructure/local/.env.example @@ -2,6 +2,7 @@ # These values are intentionally local placeholders, not deployable secrets. COMPOSE_PROJECT_NAME=databreeze-local +DATABREEZE_MIN_FREE_GIB=5 POSTGRES_IMAGE=docker.io/library/postgres:17.5-alpine POSTGRES_DB=databreeze diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 85dc4ca9..dc50e616 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -10,6 +10,8 @@ const read = (relativePath) => readFileSync(path.join(repositoryRoot, relativePa test('local compose defines pinned, healthy disposable dependencies', () => { const compose = read('infrastructure/local/compose.yml'); + const envExample = read('infrastructure/local/.env.example'); + assert.match(envExample, /^DATABREEZE_MIN_FREE_GIB=5$/m); for (const service of [ 'postgres:', 'redis:', From 03b79e063b2f7783174502141b58d922661c70f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:08:52 +0700 Subject: [PATCH 25/89] feat(infra): enable init supervision for local services --- infrastructure/local/compose.yml | 7 +++++++ tools/repo-cli/test/local-infrastructure.test.mjs | 1 + 2 files changed, 8 insertions(+) diff --git a/infrastructure/local/compose.yml b/infrastructure/local/compose.yml index 00868cea..361150b4 100644 --- a/infrastructure/local/compose.yml +++ b/infrastructure/local/compose.yml @@ -3,6 +3,7 @@ name: ${COMPOSE_PROJECT_NAME:-databreeze-local} services: postgres: image: ${POSTGRES_IMAGE:-docker.io/library/postgres:17.5-alpine} + init: true environment: POSTGRES_DB: ${POSTGRES_DB:-databreeze} POSTGRES_USER: ${POSTGRES_USER:-databreeze} @@ -24,6 +25,7 @@ services: redis: image: ${REDIS_IMAGE:-docker.io/library/redis:7.4.5-alpine} + init: true command: ['redis-server', '--appendonly', 'yes'] ports: - '${REDIS_PORT:-6379}:6379' @@ -38,6 +40,7 @@ services: minio: image: ${MINIO_IMAGE:-quay.io/minio/minio:RELEASE.2025-06-13T11-33-47Z} + init: true command: ['server', '/data', '--console-address', ':9001'] environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-databreeze} @@ -57,6 +60,7 @@ services: minio-init: image: ${MINIO_MC_IMAGE:-quay.io/minio/mc:RELEASE.2025-06-13T11-33-47Z} + init: true depends_on: minio: condition: service_healthy @@ -72,6 +76,7 @@ services: mailpit: image: ${MAILPIT_IMAGE:-axllent/mailpit:v1.21.8} + init: true environment: MP_MAX_MESSAGES: ${MAILPIT_MAX_MESSAGES:-5000} MP_DATABASE: /data/mailpit.db @@ -89,6 +94,7 @@ services: otel-collector: image: ${OTEL_IMAGE:-otel/opentelemetry-collector-contrib:0.128.0} + init: true command: ['--config=/etc/otelcol-contrib/config.yaml'] ports: - '${OTEL_GRPC_PORT:-4317}:4317' @@ -109,6 +115,7 @@ services: otel-collector-health: image: ${HEALTHCHECK_IMAGE:-curlimages/curl:8.14.1} + init: true depends_on: otel-collector: condition: service_started diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index dc50e616..2073fa6b 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -33,6 +33,7 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, new RegExp(`^ ${volume}:`, 'm')); } assert.equal((compose.match(/healthcheck:/g) ?? []).length, 6); + assert.equal((compose.match(/^ init: true$/gmu) ?? []).length, 7); assert.match(compose, /minio-init:[\s\S]*depends_on:[\s\S]*condition: service_healthy/u); assert.match(compose, /minio-init:[\s\S]*restart: 'no'/u); assert.match(compose, /postgres-data:[\s\S]*name: \$\{COMPOSE_PROJECT_NAME/u); From d5c04383dcb384b3b1f2c498c2652ac9bf556366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:09:14 +0700 Subject: [PATCH 26/89] feat(infra): isolate local service network --- infrastructure/local/compose.yml | 11 +++++++++++ tools/repo-cli/test/local-infrastructure.test.mjs | 2 ++ 2 files changed, 13 insertions(+) diff --git a/infrastructure/local/compose.yml b/infrastructure/local/compose.yml index 361150b4..05f0cd74 100644 --- a/infrastructure/local/compose.yml +++ b/infrastructure/local/compose.yml @@ -10,6 +10,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-databreeze-local-change-me} ports: - '${POSTGRES_PORT:-5432}:5432' + networks: [local] volumes: - postgres-data:/var/lib/postgresql/data - ./postgres/init:/docker-entrypoint-initdb.d:ro @@ -29,6 +30,7 @@ services: command: ['redis-server', '--appendonly', 'yes'] ports: - '${REDIS_PORT:-6379}:6379' + networks: [local] volumes: - redis-data:/data healthcheck: @@ -48,6 +50,7 @@ services: ports: - '${MINIO_API_PORT:-9000}:9000' - '${MINIO_CONSOLE_PORT:-9001}:9001' + networks: [local] volumes: - minio-data:/data healthcheck: @@ -64,6 +67,7 @@ services: depends_on: minio: condition: service_healthy + networks: [local] environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-databreeze} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-databreeze-local-change-me} @@ -83,6 +87,7 @@ services: ports: - '${MAILPIT_SMTP_PORT:-1025}:1025' - '${MAILPIT_UI_PORT:-8025}:8025' + networks: [local] volumes: - mailpit-data:/data healthcheck: @@ -100,6 +105,7 @@ services: - '${OTEL_GRPC_PORT:-4317}:4317' - '${OTEL_HTTP_PORT:-4318}:4318' - '${OTEL_HEALTH_PORT:-13133}:13133' + networks: [local] volumes: - ./otel/collector.yaml:/etc/otelcol-contrib/config.yaml:ro # The collector image is intentionally distroless and has no shell or @@ -119,6 +125,7 @@ services: depends_on: otel-collector: condition: service_started + networks: [local] command: ['sh', '-c', "trap 'exit 0' TERM INT; while :; do sleep 3600; done"] healthcheck: test: [CMD, curl, --fail, --silent, http://otel-collector:13133/] @@ -137,3 +144,7 @@ volumes: name: ${COMPOSE_PROJECT_NAME:-databreeze-local}-minio-data mailpit-data: name: ${COMPOSE_PROJECT_NAME:-databreeze-local}-mailpit-data + +networks: + local: + name: ${COMPOSE_PROJECT_NAME:-databreeze-local}-network diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 2073fa6b..06b19e2c 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -37,6 +37,8 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, /minio-init:[\s\S]*depends_on:[\s\S]*condition: service_healthy/u); assert.match(compose, /minio-init:[\s\S]*restart: 'no'/u); assert.match(compose, /postgres-data:[\s\S]*name: \$\{COMPOSE_PROJECT_NAME/u); + assert.match(compose, /networks: \[local\]/u); + assert.match(compose, /name: \$\{COMPOSE_PROJECT_NAME:-databreeze-local\}-network/u); }); test('local bootstrap is credential-free and creates every owned module schema', () => { From 8f0b50d72a337327dd66ac3ec8c733b93cf87880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:09:29 +0700 Subject: [PATCH 27/89] docs(operations): reconcile local infrastructure limits --- docs/operations/foundation-reconciliation-2026-08-02.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/operations/foundation-reconciliation-2026-08-02.md b/docs/operations/foundation-reconciliation-2026-08-02.md index 0f393ff2..e223229a 100644 --- a/docs/operations/foundation-reconciliation-2026-08-02.md +++ b/docs/operations/foundation-reconciliation-2026-08-02.md @@ -33,7 +33,7 @@ This record was created from the current merged `dev` checkpoint, not from an ea | Task 15 | reconciled | Electron main/preload/renderer shell, context isolation, CSP/navigation policy, and IPC boundary tests pass. | | Task 16 | reconciled | Python `uv` project, typed action registry, framed entry points, deterministic processor, and engine checks/build pass. | | Task 17 | reconciled | Kotlin/Compose shell, generated resources, Room/WorkManager/Keystore ports, network security, backup policy, and debug unit tests pass. | -| Task 18 | reconciled | PostgreSQL/Redis/MinIO/Mailpit/OpenTelemetry Compose definitions, health checks, and static readiness checks pass; live Docker startup is separately environment-gated. | +| Task 18 | reconciled-with-limit | PostgreSQL/Redis/MinIO/Mailpit/OpenTelemetry Compose definitions, health checks, explicit network/volume isolation, safe lifecycle commands, port/disk/Docker preflight, and daemon-free config checks pass; live Docker startup/restart persistence is separately environment-gated in FND-003. | | Task 19 | reconciled-with-limit | AWS OpenTofu modules, alpha composition, encryption/private-network/OIDC source checks, and non-applying infrastructure check pass; OpenTofu itself is not installed locally, so format/validate remain hosted-check obligations. | | Task 20 | reconciled | Content-safe telemetry package, correlation propagation, redaction allowlists, and hostile-attribute tests are present and covered by repository checks. | | Task 21 | reconciled | Path-aware quality/security/release workflows, pinned actions, least-privilege permissions, SBOM/provenance, and scan definitions are present and hosted checks pass. | @@ -59,7 +59,7 @@ The repository checks include generated-contract drift, brand checksum/derivativ ## Known environment limits - OpenTofu is not installed on this workstation. No AWS infrastructure was applied; hosted CI must run the pinned OpenTofu format/validate checks before an infrastructure PR is accepted. -- Docker is installed but live local dependency startup was not required for this reconciliation. Run `corepack pnpm local:smoke` on a healthy Docker daemon before dogfood acceptance. +- Docker is installed but its daemon is unavailable for this reconciliation. Run `corepack pnpm local:services check`, `start`, and `restart-check` on a healthy Docker daemon before dogfood acceptance. - Android instrumentation/emulator testing and signed release packaging remain Plan 400 gates; the debug unit suite passed with the local SDK path supplied through `ANDROID_HOME`/`ANDROID_SDK_ROOT`. - No customer data or credentials were used. Ignored dependency caches, virtual environments, build output, and Gradle state remain untracked. From 5300d95fa6330fa70d5765be7c492ccc99570192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:09:42 +0700 Subject: [PATCH 28/89] docs(plan): specify local infrastructure lifecycle --- docs/plans/010-engineering-foundation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plans/010-engineering-foundation.md b/docs/plans/010-engineering-foundation.md index 7cfa17bb..9dc207e5 100644 --- a/docs/plans/010-engineering-foundation.md +++ b/docs/plans/010-engineering-foundation.md @@ -91,7 +91,7 @@ Create the Gradle wrapper/version catalog and Kotlin/Compose application with bi ### Task 18: Local development infrastructure -Create Docker Compose definitions for PostgreSQL 17, Redis 7.4, MinIO, Mailpit, and an OpenTelemetry collector. Add health checks, named development volumes, `.env.example`, initialization scripts without credentials, and a smoke script that validates readiness. Commit as `feat(infra): add portable local dependencies`. +Create Docker Compose definitions for PostgreSQL 17, Redis 7.4, MinIO, Mailpit, and an OpenTelemetry collector. Add health checks, an isolated project network, named development volumes, `.env.example`, initialization scripts without credentials, and lifecycle commands for daemon-free config validation, safe start/stop/reset, port and disk preflight, status, and restart-persistence checks. Commit as `feat(infra): add portable local dependencies`. ### Task 19: AWS OpenTofu foundation From f941357c6f97e9a20440d27664404fbcec96f866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:09:51 +0700 Subject: [PATCH 29/89] docs(infra): document non-applying AWS validation --- infrastructure/aws/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index ba101ea8..9a02811e 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -42,3 +42,10 @@ separate deployment ADR. The modules expose IDs and endpoints only as outputs. Database credentials are never output; the security module creates named Secrets Manager records for later provider-managed rotation. + +`pnpm infra:check` is intentionally non-applying. It checks module presence, +Singapore region defaults, private-network and encryption boundaries, OIDC +subject scoping, production recovery preconditions, and then runs OpenTofu +format/initialization/validation when the pinned tool is installed. Missing +OpenTofu is reported as an explicit environment gate rather than silently +treated as a production validation pass. From bdaf5f5202f678298373258503432cc2fd8fb9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:10:06 +0700 Subject: [PATCH 30/89] docs(operations): define telemetry safety boundary --- docs/operations/telemetry-safety.md | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/operations/telemetry-safety.md diff --git a/docs/operations/telemetry-safety.md b/docs/operations/telemetry-safety.md new file mode 100644 index 00000000..b7e860a0 --- /dev/null +++ b/docs/operations/telemetry-safety.md @@ -0,0 +1,32 @@ +# Content-safe telemetry boundary + +DataBreeze telemetry is operational metadata, not a source-data transport. +Every runtime emits the versioned `@databreeze/telemetry/v1` shape and the +Python/Kotlin mirrors are checked against its allowlist. + +## Allowed data + +- correlation, trace, workspace, job, artifact, dataset, and device IDs +- bounded route/operation/outcome/reason/provider tokens +- status, duration, queue, retry, item, byte, and redaction counters +- an explicit sampling boolean + +## Prohibited data + +Paths, filenames, source values, formulas, document text, previews, evidence +snippets, questions/prompts, transcripts, contact data, secrets, tokens, +provider causes, and raw exception messages never enter ordinary telemetry. +Unknown attributes are dropped. Strict assertion helpers reject unsafe records +at adapter boundaries. JavaScript sanitization reads only own data properties, +so accessor-backed diagnostics cannot execute arbitrary getters during logging. + +## Failure behavior + +Malformed correlation or trace headers fail closed. Ambiguous duplicate headers +are rejected. Invalid or oversized values are omitted by the permissive +sanitizer and rejected by strict mode. Providers and exporters remain +replaceable; a collector outage cannot become domain authority or block durable +jobs and audit writes. + +See `packages/telemetry/README.md` and the TypeScript/Python/Android parity +tests before adding an attribute or event. From a5485849b3e76413d4372336b8ba80326ffc39be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:10:30 +0700 Subject: [PATCH 31/89] feat(infra): bound hosted web object retention --- infrastructure/aws/modules/web/main.tf | 19 +++++++++++++++++++ .../repo-cli/test/aws-infrastructure.test.mjs | 3 +++ 2 files changed, 22 insertions(+) diff --git a/infrastructure/aws/modules/web/main.tf b/infrastructure/aws/modules/web/main.tf index 7cf3001d..ed3f12d2 100644 --- a/infrastructure/aws/modules/web/main.tf +++ b/infrastructure/aws/modules/web/main.tf @@ -29,6 +29,25 @@ resource "aws_s3_bucket_versioning" "web" { } } +resource "aws_s3_bucket_lifecycle_configuration" "web" { + bucket = aws_s3_bucket.web.id + + rule { + id = "bounded-version-retention" + status = "Enabled" + + noncurrent_version_expiration { + noncurrent_days = 90 + } + + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + } + + depends_on = [aws_s3_bucket_versioning.web] +} + resource "aws_s3_bucket_server_side_encryption_configuration" "web" { bucket = aws_s3_bucket.web.id rule { diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index a3b93bd2..d813f226 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -70,6 +70,9 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s assert.match(sources, /token\.actions\.githubusercontent\.com:sub/u); assert.match(sources, /repo:\$\{var\.github_repository\}:ref:refs\/heads\/dev/u); assert.doesNotMatch(sources, /refs\/pull|refs\/tags|repo:\*\//u); + assert.match(sources, /aws_s3_bucket_lifecycle_configuration/u); + assert.match(sources, /noncurrent_version_expiration/u); + assert.match(sources, /abort_incomplete_multipart_upload/u); }); test('AWS validation script is non-applying and reports missing OpenTofu clearly', () => { From 340110d6fef9907c8562d8f8dc5ef5ca06fa773f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:10:43 +0700 Subject: [PATCH 32/89] feat(infra): harden hosted task containers --- infrastructure/aws/modules/compute/main.tf | 8 ++++++++ tools/repo-cli/test/aws-infrastructure.test.mjs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/infrastructure/aws/modules/compute/main.tf b/infrastructure/aws/modules/compute/main.tf index 756b5593..4e57d883 100644 --- a/infrastructure/aws/modules/compute/main.tf +++ b/infrastructure/aws/modules/compute/main.tf @@ -83,6 +83,10 @@ locals { name = "api" image = var.api_image essential = true + readonlyRootFilesystem = true + privileged = false + user = "10001" + stopTimeout = 30 cpu = var.api_cpu memory = var.api_memory portMappings = [{ @@ -133,6 +137,10 @@ resource "aws_ecs_task_definition" "worker" { name = "worker" image = var.worker_image essential = true + readonlyRootFilesystem = true + privileged = false + user = "10001" + stopTimeout = 30 logConfiguration = { logDriver = "awslogs" options = { diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index d813f226..e4148b2a 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -104,6 +104,9 @@ test('AWS production profile enables recovery and prevents public data paths', ( assert.doesNotMatch(data, /publicly_accessible\s*=\s*true/u); assert.doesNotMatch(data, /skip_final_snapshot\s*=\s*true/u); assert.match(compute, /assign_public_ip\s*=\s*false/u); + assert.equal((compute.match(/readonlyRootFilesystem\s*=\s*true/g) ?? []).length, 2); + assert.equal((compute.match(/privileged\s*=\s*false/g) ?? []).length, 2); + assert.equal((compute.match(/user\s*=\s*"10001"/g) ?? []).length, 2); }); test('AWS foundation keeps state and apply outside the repository', () => { From d9f1ad1448c4351cf8e71c571bcda73b55028b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:11:03 +0700 Subject: [PATCH 33/89] feat(infra): enable production database insights --- infrastructure/aws/modules/data/main.tf | 2 ++ tools/repo-cli/test/aws-infrastructure.test.mjs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/infrastructure/aws/modules/data/main.tf b/infrastructure/aws/modules/data/main.tf index b8a12a6b..dc731da4 100644 --- a/infrastructure/aws/modules/data/main.tf +++ b/infrastructure/aws/modules/data/main.tf @@ -38,6 +38,8 @@ resource "aws_db_instance" "postgres" { final_snapshot_identifier = "databreeze-${var.name}-final" auto_minor_version_upgrade = true copy_tags_to_snapshot = true + performance_insights_enabled = var.environment == "production" + performance_insights_kms_key_id = var.environment == "production" ? var.kms_key_arn : null tags = merge(local.common_tags, { Name = "databreeze-${var.name}" }) } diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index e4148b2a..3f6b0fd2 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -103,6 +103,8 @@ test('AWS production profile enables recovery and prevents public data paths', ( assert.match(versions, /required_version\s*=\s*">= 1\.8\.0, < 2\.0\.0"/u); assert.doesNotMatch(data, /publicly_accessible\s*=\s*true/u); assert.doesNotMatch(data, /skip_final_snapshot\s*=\s*true/u); + assert.match(data, /performance_insights_enabled\s*=\s*var\.environment == "production"/u); + assert.match(data, /performance_insights_kms_key_id/u); assert.match(compute, /assign_public_ip\s*=\s*false/u); assert.equal((compute.match(/readonlyRootFilesystem\s*=\s*true/g) ?? []).length, 2); assert.equal((compute.match(/privileged\s*=\s*false/g) ?? []).length, 2); From bb8a55b4553545e747dcfc60107c3fffedb56653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:11:24 +0700 Subject: [PATCH 34/89] docs(infra): document production database insights --- infrastructure/aws/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index 9a02811e..7794ee7f 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -39,6 +39,9 @@ redundant API/worker capacity, PITR/backups, Multi-AZ data, NAT, and CloudFront explicitly. Production must use a remote encrypted state backend approved in a separate deployment ADR. +The production profile also enables RDS Performance Insights with the platform +KMS key; alpha keeps it disabled unless explicitly selected. + The modules expose IDs and endpoints only as outputs. Database credentials are never output; the security module creates named Secrets Manager records for later provider-managed rotation. From 6d3368cdeb9da773716214c994adb9ba2d1b7e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:11:46 +0700 Subject: [PATCH 35/89] ci: disable checkout credential persistence --- .github/workflows/quality.yml | 7 +++++++ .github/workflows/release.yml | 2 ++ .github/workflows/security.yml | 2 ++ tools/repo-cli/src/check-ci-policy.mjs | 3 +++ tools/repo-cli/test/ci-policy.test.mjs | 5 +++++ 5 files changed, 19 insertions(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f8483a67..c8ee4fa2 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -27,6 +27,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: fetch-depth: 0 + persist-credentials: false - name: Detect change scope id: scope shell: bash @@ -53,6 +54,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with: @@ -89,6 +92,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with: @@ -117,6 +122,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Set up Java 21 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e15023c4..c32d5257 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index f41ad0da..767ffd4c 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index b67cb913..a63ad454 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -36,6 +36,9 @@ function assertLeastPrivilege(text, filename) { if (/AWS_(?:ACCESS_KEY_ID|SECRET_ACCESS_KEY)\s*:/iu.test(text)) { throw new Error(`${filename} must not define long-lived AWS key environment variables`); } + if (/uses:\s*actions\/checkout@/iu.test(text) && !/persist-credentials:\s*false/iu.test(text)) { + throw new Error(`${filename} must disable checkout credential persistence`); + } } export function checkCiPolicy(root = process.cwd()) { diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index 7c1066af..e07341e8 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -10,6 +10,11 @@ test('repository workflows are present, pinned, and least privilege', () => { assert.deepEqual(checkCiPolicy(process.cwd()), { workflowCount: 3 }); }); +test('CI policy requires checkout credentials to be discarded', () => { + const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows/quality.yml'), 'utf8'); + assert.match(workflow, /persist-credentials:\s*false/u); +}); + test('CI policy rejects floating actions and pull request target execution', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'databreeze-ci-policy-')); fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true }); From 7811dbca6ff045940d79ac55a06bd1914c4dce1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:12:24 +0700 Subject: [PATCH 36/89] fix(telemetry): fail closed on hostile Python mappings --- .../engine/src/databreeze_engine/telemetry.py | 32 ++++++++++++------- services/engine/tests/test_telemetry.py | 10 ++++++ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/services/engine/src/databreeze_engine/telemetry.py b/services/engine/src/databreeze_engine/telemetry.py index e48c2606..bc2b817a 100644 --- a/services/engine/src/databreeze_engine/telemetry.py +++ b/services/engine/src/databreeze_engine/telemetry.py @@ -162,23 +162,33 @@ def sanitize_attributes(attributes: dict[str, Any]) -> dict[str, str | int | flo if not isinstance(attributes, Mapping): return {} safe: dict[str, str | int | float | bool] = {} - for raw_key, value in attributes.items(): - key = _validate_key(raw_key) - if key not in SAFE_ATTRIBUTE_KEYS: - continue - scalar = _safe_scalar(key, value) - if scalar is not None: - safe[key] = scalar + try: + for raw_key, value in attributes.items(): + key = _validate_key(raw_key) + if key not in SAFE_ATTRIBUTE_KEYS: + continue + scalar = _safe_scalar(key, value) + if scalar is not None: + safe[key] = scalar + except Exception: + # Diagnostics must fail closed without reflecting provider causes or + # executing a hostile Mapping implementation again. + return safe return safe def assert_safe_attributes(attributes: Mapping[str, Any]) -> None: """Raise when a record contains an unsafe or unknown attribute.""" - for raw_key, value in attributes.items(): - key = _validate_key(raw_key) - if key not in SAFE_ATTRIBUTE_KEYS or _safe_scalar(key, value) is None: - raise ValueError(f"telemetry attribute is not allowed: {key}") + try: + for raw_key, value in attributes.items(): + key = _validate_key(raw_key) + if key not in SAFE_ATTRIBUTE_KEYS or _safe_scalar(key, value) is None: + raise ValueError(f"telemetry attribute is not allowed: {key}") + except ValueError: + raise + except Exception: + raise ValueError("telemetry attributes are not readable") from None def _correlation_id(value: str) -> str: diff --git a/services/engine/tests/test_telemetry.py b/services/engine/tests/test_telemetry.py index 2edc1968..050dfc4a 100644 --- a/services/engine/tests/test_telemetry.py +++ b/services/engine/tests/test_telemetry.py @@ -68,6 +68,16 @@ def test_engine_rejects_ambiguous_or_zero_trace_headers() -> None: ) +def test_engine_telemetry_does_not_execute_hostile_mapping_items() -> None: + class HostileMapping(dict[str, object]): + def items(self): # type: ignore[override] + raise RuntimeError("provider cause must not escape") + + assert sanitize_attributes(HostileMapping()) == {} + with pytest.raises(ValueError, match="not readable"): + assert_safe_attributes(HostileMapping()) + + def test_engine_accepts_mixed_case_header_names() -> None: context = CorrelationContext( "00000000-0000-4000-8000-000000000001", From ced1568ad5bdf7316c59a56d7eb7fd563dc177e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:12:41 +0700 Subject: [PATCH 37/89] fix(infra): make object lifecycle filter explicit --- infrastructure/aws/modules/web/main.tf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infrastructure/aws/modules/web/main.tf b/infrastructure/aws/modules/web/main.tf index ed3f12d2..8a8e80f9 100644 --- a/infrastructure/aws/modules/web/main.tf +++ b/infrastructure/aws/modules/web/main.tf @@ -36,6 +36,8 @@ resource "aws_s3_bucket_lifecycle_configuration" "web" { id = "bounded-version-retention" status = "Enabled" + filter {} + noncurrent_version_expiration { noncurrent_days = 90 } From ad12965c976e22ae5e1e01e8d797730e260b5cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:13:02 +0700 Subject: [PATCH 38/89] docs(infra): clarify alpha plan-only workflow --- infrastructure/aws/environments/alpha/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infrastructure/aws/environments/alpha/README.md b/infrastructure/aws/environments/alpha/README.md index 74e33421..58c4f750 100644 --- a/infrastructure/aws/environments/alpha/README.md +++ b/infrastructure/aws/environments/alpha/README.md @@ -10,6 +10,11 @@ Before planning, install the pinned OpenTofu release required by the root planning. RDS manages its master password through AWS Secrets Manager. Never commit `.terraform/`, `*.tfstate`, `terraform.tfvars`, or provider credentials. +Use `tofu init -backend=false` followed by `tofu validate` for a credential-free +syntax check. A real `tofu plan` requires an approved account, region, state +backend, and injected credentials; no apply or destroy command belongs in this +alpha workflow. + Production changes require a separate reviewed variable set enabling at least: - two availability zones with one NAT gateway per AZ; From 52fc370a63e69875f81f9d61127082c16f574680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:13:26 +0700 Subject: [PATCH 39/89] feat(infra): expose daemon-free local preflight --- infrastructure/local/README.md | 1 + tools/repo-cli/src/local-services.mjs | 10 +++++++++- tools/repo-cli/test/local-infrastructure.test.mjs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index 3b3c5042..829b932a 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -26,6 +26,7 @@ Run these from the repository root: | Command | Effect | | --- | --- | | `pnpm local:services config` | Validate Compose syntax without requiring a running Docker daemon. | +| `pnpm local:services preflight` | Validate Compose, host ports, and disk headroom without starting containers. | | `pnpm local:services check` | Validate Compose, Docker, host ports, and free disk without starting anything. | | `pnpm local:services start` | Run preflight, start the stack, and wait for every health check. | | `pnpm local:services stop` | Stop containers while preserving containers and named volumes. | diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index d064b5e0..566f58a3 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -35,6 +35,7 @@ function usage() { Commands (all preserve named volumes): config validate Compose syntax without requiring a Docker daemon + preflight validate Compose, ports, and disk without a Docker daemon check validate Compose, ports, Docker, and disk headroom start validate, start services, and wait for healthy checks stop stop containers without removing containers or volumes @@ -235,7 +236,7 @@ function parseArguments(argv) { if (!Number.isFinite(options.minFreeGib) || options.minFreeGib < 0) { fail('--min-free-gib must be a non-negative number'); } - if (!['config', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke'].includes(command)) { + if (!['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke'].includes(command)) { fail(`unknown command: ${command}`); } return { command, options }; @@ -253,6 +254,13 @@ export async function main(argv = process.argv.slice(2)) { console.log('Local Compose configuration is valid.'); return; } + if (command === 'preflight') { + validateCompose(values); + ensureDiskSpace(options.minFreeGib); + await ensurePorts(values); + console.log('Local Compose, port, and disk preflight passed without starting services.'); + return; + } requireDocker(); validateCompose(values); diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 06b19e2c..89e93b7b 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -95,7 +95,7 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); - for (const command of ['config', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke']) { + for (const command of ['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke']) { assert.match(result.stdout, new RegExp(`^ ${command}\\s`, 'm')); } assert.match(script, /statfsSync/u); From a0e4d756216337011169144ed7d52848e25f0b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:15:52 +0700 Subject: [PATCH 40/89] docs(infra): record daemon-free preflight evidence --- .../foundation-local-infrastructure-2026-08-02.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index b8994001..bc828589 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -10,8 +10,9 @@ Task: `FND-003 — Close local infrastructure gaps` remain pinned in `infrastructure/local/compose.yml`. - PostgreSQL initialization creates every module-owned schema and contains no credentials, roles, or secret literals. -- `tools/repo-cli/src/local-services.mjs` provides `check`, `start`, `stop`, - `reset`, `restart-check`, `status`, and legacy `smoke` commands. +- `tools/repo-cli/src/local-services.mjs` provides daemon-free `config` and + `preflight` commands plus `check`, `start`, `stop`, `reset`, + `restart-check`, `status`, and legacy `smoke` commands. - Lifecycle commands preserve named volumes. `reset` uses Compose `down --remove-orphans` without `--volumes`; data deletion is never implicit. - Preflight reports missing Docker CLI/daemon, host port collisions, and @@ -27,12 +28,16 @@ Passed: - `node tools/repo-cli/src/local-services.mjs --help` - `node tools/repo-cli/src/local-services-smoke.mjs --help` - `node tools/repo-cli/src/local-services.mjs config` +- `node tools/repo-cli/src/local-services.mjs preflight --min-free-gib=0` - `git diff --check` Environment-gated: - `node tools/repo-cli/src/local-services.mjs check` fails closed with `Docker daemon is unavailable` because no Docker daemon is running here. +- The daemon-free `preflight` command completes Compose, port, and disk checks + without starting containers; the evidence run used a zero-GiB threshold so + it remains independent of the workstation's available disk headroom. - Live `compose up`, health polling, port-collision simulation, disk-pressure threshold validation, and restart-persistence checks must run on a machine with Docker Desktop/Compose v2 before FND-003 can become `verified`. From b989fa5e58fc46961befe1cca85d9d35805901f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:16:07 +0700 Subject: [PATCH 41/89] test(infra): pin local telemetry signal pipelines --- tools/repo-cli/test/local-infrastructure.test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 89e93b7b..3deb93ff 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -76,6 +76,21 @@ test('local bootstrap is credential-free and creates every owned module schema', assert.doesNotMatch(bucketScript, /databreeze-local-change-me/); }); +test('local OpenTelemetry collector keeps every signal on the bounded local pipeline', () => { + const collector = read('infrastructure/local/otel/collector.yaml'); + for (const section of ['receivers:', 'processors:', 'exporters:', 'extensions:', 'service:', 'pipelines:']) { + assert.match(collector, new RegExp(`^${section}`, 'm')); + } + for (const signal of ['traces:', 'metrics:', 'logs:']) { + assert.match(collector, new RegExp(`^ ${signal}`, 'm')); + assert.match(collector, new RegExp(`${signal}[\\s\\S]*receivers: \\[otlp\\]`, 'u')); + assert.match(collector, new RegExp(`${signal}[\\s\\S]*processors: \\[memory_limiter, batch\\]`, 'u')); + assert.match(collector, new RegExp(`${signal}[\\s\\S]*exporters: \\[debug\\]`, 'u')); + } + assert.match(collector, /health_check:[\\s\\S]*endpoint: 0\.0\.0\.0:13133/u); + assert.doesNotMatch(collector, /filelog|otlphttp|s3|http:\/\\//iu); +}); + test('readiness smoke script exposes a non-destructive help command', () => { const script = path.join(repositoryRoot, 'tools', 'repo-cli', 'src', 'local-services-smoke.mjs'); const result = spawnSync(process.execPath, [script, '--help'], { From b6062dc702eea96b9107cca35d014550a70730e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:16:16 +0700 Subject: [PATCH 42/89] fix(infra): match indented collector pipeline config --- tools/repo-cli/test/local-infrastructure.test.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 3deb93ff..6fd0d707 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -78,8 +78,15 @@ test('local bootstrap is credential-free and creates every owned module schema', test('local OpenTelemetry collector keeps every signal on the bounded local pipeline', () => { const collector = read('infrastructure/local/otel/collector.yaml'); - for (const section of ['receivers:', 'processors:', 'exporters:', 'extensions:', 'service:', 'pipelines:']) { - assert.match(collector, new RegExp(`^${section}`, 'm')); + for (const [section, indentation] of [ + ['receivers:', ''], + ['processors:', ''], + ['exporters:', ''], + ['extensions:', ''], + ['service:', ''], + ['pipelines:', ' '], + ]) { + assert.match(collector, new RegExp(`^${indentation}${section}`, 'm')); } for (const signal of ['traces:', 'metrics:', 'logs:']) { assert.match(collector, new RegExp(`^ ${signal}`, 'm')); From 8cd55cc1f471c3716fda56330555d54a71613771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:16:30 +0700 Subject: [PATCH 43/89] fix(infra): correct collector safety assertions --- tools/repo-cli/test/local-infrastructure.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 6fd0d707..7393605a 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -94,8 +94,8 @@ test('local OpenTelemetry collector keeps every signal on the bounded local pipe assert.match(collector, new RegExp(`${signal}[\\s\\S]*processors: \\[memory_limiter, batch\\]`, 'u')); assert.match(collector, new RegExp(`${signal}[\\s\\S]*exporters: \\[debug\\]`, 'u')); } - assert.match(collector, /health_check:[\\s\\S]*endpoint: 0\.0\.0\.0:13133/u); - assert.doesNotMatch(collector, /filelog|otlphttp|s3|http:\/\\//iu); + assert.match(collector, /health_check:[\s\S]*endpoint: 0\.0\.0\.0:13133/u); + assert.doesNotMatch(collector, /filelog|otlphttp|s3|https?:\/\//iu); }); test('readiness smoke script exposes a non-destructive help command', () => { From 5755d79b3d6907532adb6978037b11cc7c5aa5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:17:09 +0700 Subject: [PATCH 44/89] fix(infra): require immutable production task images --- infrastructure/aws/modules/compute/main.tf | 14 ++++++++++++++ tools/repo-cli/test/aws-infrastructure.test.mjs | 3 +++ 2 files changed, 17 insertions(+) diff --git a/infrastructure/aws/modules/compute/main.tf b/infrastructure/aws/modules/compute/main.tf index 4e57d883..4f1ca581 100644 --- a/infrastructure/aws/modules/compute/main.tf +++ b/infrastructure/aws/modules/compute/main.tf @@ -123,6 +123,13 @@ resource "aws_ecs_task_definition" "api" { task_role_arn = aws_iam_role.task.arn container_definitions = jsonencode([local.api_container]) tags = merge(local.common_tags, { Name = "databreeze-${var.name}-api" }) + + lifecycle { + precondition { + condition = var.environment != "production" || can(regex("@sha256:[0-9a-f]{64}$", var.api_image)) + error_message = "Production API deployments must use an immutable image digest." + } + } } resource "aws_ecs_task_definition" "worker" { @@ -151,6 +158,13 @@ resource "aws_ecs_task_definition" "worker" { } }]) tags = merge(local.common_tags, { Name = "databreeze-${var.name}-worker" }) + + lifecycle { + precondition { + condition = var.environment != "production" || can(regex("@sha256:[0-9a-f]{64}$", var.worker_image)) + error_message = "Production worker deployments must use an immutable image digest." + } + } } resource "aws_ecs_service" "api" { diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 3f6b0fd2..34fcf990 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -109,6 +109,9 @@ test('AWS production profile enables recovery and prevents public data paths', ( assert.equal((compute.match(/readonlyRootFilesystem\s*=\s*true/g) ?? []).length, 2); assert.equal((compute.match(/privileged\s*=\s*false/g) ?? []).length, 2); assert.equal((compute.match(/user\s*=\s*"10001"/g) ?? []).length, 2); + assert.match(compute, /Production API deployments must use an immutable image digest\./u); + assert.match(compute, /Production worker deployments must use an immutable image digest\./u); + assert.match(compute, /@sha256:\[0-9a-f\]\{64\}/u); }); test('AWS foundation keeps state and apply outside the repository', () => { From d951d4edcba8e511d3cab553db5c235e1064bf0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:17:26 +0700 Subject: [PATCH 45/89] docs(infra): document digest-pinned production images --- infrastructure/aws/README.md | 6 ++++++ tools/repo-cli/test/aws-infrastructure.test.mjs | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index 7794ee7f..aa816b61 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -42,6 +42,12 @@ separate deployment ADR. The production profile also enables RDS Performance Insights with the platform KMS key; alpha keeps it disabled unless explicitly selected. +Before a production plan, set `api_image` and `worker_image` to immutable +registry references ending in a 64-character SHA-256 digest (for example, +`ghcr.io/example/databreeze-api@sha256:`). Mutable tags are accepted +for alpha development only; the ECS task definitions have a production +precondition that rejects them. + The modules expose IDs and endpoints only as outputs. Database credentials are never output; the security module creates named Secrets Manager records for later provider-managed rotation. diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 34fcf990..84e769d5 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -125,3 +125,10 @@ test('AWS foundation keeps state and apply outside the repository', () => { assert.doesNotMatch(sources, /^\s*backend\s+"/mu); assert.doesNotMatch(sources, /terraform\.tfstate|\.tfstate\.backup/u); }); + +test('AWS production guidance requires digest-pinned application images', () => { + const readme = read('infrastructure/aws/README.md'); + assert.match(readme, /api_image.*worker_image[\s\S]*immutable/u); + assert.match(readme, /64-character SHA-256 digest/u); + assert.match(readme, /Mutable tags are accepted for alpha development only/u); +}); From 93a6df6545fd1b656ee171669270baf3bcb2db65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:17:33 +0700 Subject: [PATCH 46/89] fix(infra): allow wrapped image guidance text --- tools/repo-cli/test/aws-infrastructure.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 84e769d5..aef0f0fe 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -130,5 +130,5 @@ test('AWS production guidance requires digest-pinned application images', () => const readme = read('infrastructure/aws/README.md'); assert.match(readme, /api_image.*worker_image[\s\S]*immutable/u); assert.match(readme, /64-character SHA-256 digest/u); - assert.match(readme, /Mutable tags are accepted for alpha development only/u); + assert.match(readme, /Mutable tags are accepted\s+for alpha development only/u); }); From a78e135ff382c687737bf29d2f31c7df0a8a5c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:17:54 +0700 Subject: [PATCH 47/89] feat(infra): parameterize worker task capacity --- infrastructure/aws/environments/alpha/main.tf | 2 ++ .../aws/environments/alpha/variables.tf | 10 +++++++++ infrastructure/aws/modules/compute/main.tf | 4 ++-- .../aws/modules/compute/variables.tf | 22 +++++++++++++++++++ .../repo-cli/test/aws-infrastructure.test.mjs | 7 ++++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/infrastructure/aws/environments/alpha/main.tf b/infrastructure/aws/environments/alpha/main.tf index 8dd3efc2..21062cfa 100644 --- a/infrastructure/aws/environments/alpha/main.tf +++ b/infrastructure/aws/environments/alpha/main.tf @@ -66,5 +66,7 @@ module "compute" { private_egress_enabled = var.enable_nat_gateway api_desired_count = var.api_desired_count worker_desired_count = var.worker_desired_count + worker_cpu = var.worker_cpu + worker_memory = var.worker_memory tags = local.tags } diff --git a/infrastructure/aws/environments/alpha/variables.tf b/infrastructure/aws/environments/alpha/variables.tf index 37080882..370cf456 100644 --- a/infrastructure/aws/environments/alpha/variables.tf +++ b/infrastructure/aws/environments/alpha/variables.tf @@ -108,3 +108,13 @@ variable "worker_desired_count" { type = number default = 1 } + +variable "worker_cpu" { + type = number + default = 1024 +} + +variable "worker_memory" { + type = number + default = 2048 +} diff --git a/infrastructure/aws/modules/compute/main.tf b/infrastructure/aws/modules/compute/main.tf index 4f1ca581..64129fb7 100644 --- a/infrastructure/aws/modules/compute/main.tf +++ b/infrastructure/aws/modules/compute/main.tf @@ -136,8 +136,8 @@ resource "aws_ecs_task_definition" "worker" { family = "databreeze-${var.name}-worker" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" - cpu = "1024" - memory = "2048" + cpu = tostring(var.worker_cpu) + memory = tostring(var.worker_memory) execution_role_arn = aws_iam_role.execution.arn task_role_arn = aws_iam_role.task.arn container_definitions = jsonencode([{ diff --git a/infrastructure/aws/modules/compute/variables.tf b/infrastructure/aws/modules/compute/variables.tf index dca538c3..e54afd04 100644 --- a/infrastructure/aws/modules/compute/variables.tf +++ b/infrastructure/aws/modules/compute/variables.tf @@ -76,6 +76,28 @@ variable "api_memory" { default = 1024 } +variable "worker_cpu" { + type = number + description = "Fargate task CPU units reserved for the worker pool." + default = 1024 + + validation { + condition = contains([256, 512, 1024, 2048, 4096], var.worker_cpu) + error_message = "worker_cpu must be a supported Fargate CPU size." + } +} + +variable "worker_memory" { + type = number + description = "Fargate task memory (MiB) reserved for the worker pool." + default = 2048 + + validation { + condition = var.worker_memory >= 512 && var.worker_memory <= 30720 + error_message = "worker_memory must be between 512 and 30720 MiB." + } +} + variable "tags" { type = map(string) default = {} diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index aef0f0fe..c1056018 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -112,6 +112,13 @@ test('AWS production profile enables recovery and prevents public data paths', ( assert.match(compute, /Production API deployments must use an immutable image digest\./u); assert.match(compute, /Production worker deployments must use an immutable image digest\./u); assert.match(compute, /@sha256:\[0-9a-f\]\{64\}/u); + assert.match(compute, /cpu\s*=\s*tostring\(var\.worker_cpu\)/u); + assert.match(compute, /memory\s*=\s*tostring\(var\.worker_memory\)/u); + const computeVariables = read('infrastructure/aws/modules/compute/variables.tf'); + assert.match(computeVariables, /variable "worker_cpu"/u); + assert.match(computeVariables, /variable "worker_memory"/u); + assert.match(computeVariables, /supported Fargate CPU size/u); + assert.match(computeVariables, /between 512 and 30720 MiB/u); }); test('AWS foundation keeps state and apply outside the repository', () => { From 199ce77df71fb5da406cfacfe3063fea80c08045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:18:16 +0700 Subject: [PATCH 48/89] fix(infra): remove unused ECS task secret permissions --- infrastructure/aws/modules/compute/main.tf | 13 ------------- tools/repo-cli/test/aws-infrastructure.test.mjs | 2 ++ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/infrastructure/aws/modules/compute/main.tf b/infrastructure/aws/modules/compute/main.tf index 64129fb7..0bfcb70b 100644 --- a/infrastructure/aws/modules/compute/main.tf +++ b/infrastructure/aws/modules/compute/main.tf @@ -65,19 +65,6 @@ resource "aws_iam_role" "task" { tags = merge(local.common_tags, { Name = "databreeze-${var.name}-ecs-task" }) } -resource "aws_iam_role_policy" "task" { - name = "databreeze-${var.name}-ecs-task-minimal" - role = aws_iam_role.task.id - policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Action = ["secretsmanager:GetSecretValue", "kms:Decrypt"] - Resource = [var.database_secret_arn, var.application_secret_arn, var.kms_key_arn] - }] - }) -} - locals { api_container = { name = "api" diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index c1056018..8f42d243 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -73,6 +73,8 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s assert.match(sources, /aws_s3_bucket_lifecycle_configuration/u); assert.match(sources, /noncurrent_version_expiration/u); assert.match(sources, /abort_incomplete_multipart_upload/u); + assert.doesNotMatch(sources, /aws_iam_role_policy" task/u); + assert.doesNotMatch(sources, /ecs-task-minimal/u); }); test('AWS validation script is non-applying and reports missing OpenTofu clearly', () => { From f291fd57e6709efc883fc6b457e14ea78a6a7a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:18:57 +0700 Subject: [PATCH 49/89] ci: bound every runner job with a timeout --- .github/workflows/quality.yml | 2 + tools/repo-cli/src/check-ci-policy.mjs | 9 +++++ tools/repo-cli/test/ci-policy.test.mjs | 52 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c8ee4fa2..99be5621 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -16,6 +16,7 @@ jobs: scope: name: Detect affected runtimes runs-on: ubuntu-24.04 + timeout-minutes: 10 outputs: shared: ${{ steps.scope.outputs.shared }} web: ${{ steps.scope.outputs.web }} @@ -89,6 +90,7 @@ jobs: needs: scope if: needs.scope.outputs.engine == 'true' runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index a63ad454..cb4949b0 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -41,6 +41,14 @@ function assertLeastPrivilege(text, filename) { } } +function assertBoundedJobs(text, filename) { + const jobCount = (text.match(/^\s+runs-on:\s*\S+/gim) ?? []).length; + const timeoutCount = (text.match(/^\s+timeout-minutes:\s*\d+/gim) ?? []).length; + if (jobCount > timeoutCount) { + throw new Error(`${filename} must bound every runner job with timeout-minutes`); + } +} + export function checkCiPolicy(root = process.cwd()) { const workflows = Object.fromEntries( REQUIRED_WORKFLOWS.map((name) => [name, readWorkflow(root, name)]), @@ -48,6 +56,7 @@ export function checkCiPolicy(root = process.cwd()) { for (const [name, text] of Object.entries(workflows)) { assertPinnedActions(text, name); assertLeastPrivilege(text, name); + assertBoundedJobs(text, name); } const security = workflows['security.yml']; for (const required of [ diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index e07341e8..2212ff3c 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -37,3 +37,55 @@ test('CI policy rejects floating actions and pull request target execution', () fs.writeFileSync(path.join(root, '.github/workflows', name), text); assert.throws(() => checkCiPolicy(root), /unpinned action/u); }); + +test('CI policy rejects runner jobs without a timeout', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'databreeze-ci-timeout-')); + fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true }); + const checkout = 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683'; + const workflows = { + 'quality.yml': [ + 'name: q', + 'permissions:', + ' contents: read', + 'jobs:', + ' check:', + ' runs-on: ubuntu-24.04', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ].join('\n'), + 'security.yml': [ + 'name: s', + 'permissions:', + ' contents: read', + 'jobs:', + ' scan:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ' - run: pnpm audit && check-secret-patterns.mjs check-license-policy.mjs check-container-policy.mjs generate-sbom.mjs', + ].join('\n'), + 'release.yml': [ + 'name: r', + 'permissions:', + ' contents: read', + ' id-token: write', + 'jobs:', + ' release:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ' - run: generate-provenance.mjs', + ].join('\n'), + }; + for (const [name, text] of Object.entries(workflows)) + fs.writeFileSync(path.join(root, '.github/workflows', name), text); + assert.throws(() => checkCiPolicy(root), /timeout-minutes/u); +}); From 385dae77423815d440458ba3a957f935ba02daa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:19:04 +0700 Subject: [PATCH 50/89] fix(ci): bound release provenance runtime --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c32d5257..db13253c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ jobs: name: Build and record release provenance runs-on: ubuntu-24.04 environment: release + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 From d3cf73ce617faee0638606682f81b3738e15024b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:19:31 +0700 Subject: [PATCH 51/89] ci: fail when release artifacts are missing --- tools/repo-cli/src/check-ci-policy.mjs | 10 +++++ tools/repo-cli/test/ci-policy.test.mjs | 61 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index cb4949b0..b095d70b 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -49,6 +49,15 @@ function assertBoundedJobs(text, filename) { } } +function assertArtifactOutputs(text, filename) { + const artifactSteps = text.match(/-\s+uses:\s+actions\/upload-artifact@[^\n]+[\s\S]*?(?=\n\s+-\s+name:|\n\s+\w+:\s*$|$)/gim) ?? []; + for (const step of artifactSteps) { + if (!/if-no-files-found:\s*error/iu.test(step)) { + throw new Error(`${filename} artifact uploads must fail when an output is missing`); + } + } +} + export function checkCiPolicy(root = process.cwd()) { const workflows = Object.fromEntries( REQUIRED_WORKFLOWS.map((name) => [name, readWorkflow(root, name)]), @@ -57,6 +66,7 @@ export function checkCiPolicy(root = process.cwd()) { assertPinnedActions(text, name); assertLeastPrivilege(text, name); assertBoundedJobs(text, name); + assertArtifactOutputs(text, name); } const security = workflows['security.yml']; for (const required of [ diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index 2212ff3c..0d0df2b1 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -15,6 +15,11 @@ test('CI policy requires checkout credentials to be discarded', () => { assert.match(workflow, /persist-credentials:\s*false/u); }); +test('CI policy fails closed when an artifact output is missing', () => { + const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows/release.yml'), 'utf8'); + assert.match(workflow, /actions\/upload-artifact@[0-9a-f]{40}[\s\S]*if-no-files-found:\s*error/iu); +}); + test('CI policy rejects floating actions and pull request target execution', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'databreeze-ci-policy-')); fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true }); @@ -89,3 +94,59 @@ test('CI policy rejects runner jobs without a timeout', () => { fs.writeFileSync(path.join(root, '.github/workflows', name), text); assert.throws(() => checkCiPolicy(root), /timeout-minutes/u); }); + +test('CI policy rejects artifact steps without missing-output failure', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'databreeze-ci-artifact-')); + fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true }); + const checkout = 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683'; + const workflows = { + 'quality.yml': [ + 'name: q', + 'permissions:', + ' contents: read', + 'jobs:', + ' check:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ].join('\n'), + 'security.yml': [ + 'name: s', + 'permissions:', + ' contents: read', + 'jobs:', + ' scan:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ' - run: pnpm audit && check-secret-patterns.mjs check-license-policy.mjs check-container-policy.mjs generate-sbom.mjs', + ].join('\n'), + 'release.yml': [ + 'name: r', + 'permissions:', + ' contents: read', + ' id-token: write', + 'jobs:', + ' release:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ' - run: generate-provenance.mjs', + ` - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02`, + ' with:', + ' path: output.json', + ].join('\n'), + }; + for (const [name, text] of Object.entries(workflows)) + fs.writeFileSync(path.join(root, '.github/workflows', name), text); + assert.throws(() => checkCiPolicy(root), /artifact uploads must fail/u); +}); From 0cacf2f50d4c27d467d432615b493979c1e82fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:20:14 +0700 Subject: [PATCH 52/89] feat(infra): add bounded local log diagnostics --- ...ndation-local-infrastructure-2026-08-02.md | 3 ++- infrastructure/local/README.md | 4 +++ tools/repo-cli/src/local-services.mjs | 27 ++++++++++++++++++- .../test/local-infrastructure.test.mjs | 19 ++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index bc828589..bc106c5f 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -12,7 +12,8 @@ Task: `FND-003 — Close local infrastructure gaps` credentials, roles, or secret literals. - `tools/repo-cli/src/local-services.mjs` provides daemon-free `config` and `preflight` commands plus `check`, `start`, `stop`, `reset`, - `restart-check`, `status`, and legacy `smoke` commands. + `restart-check`, `status`, bounded read-only `logs`, and legacy `smoke` + commands. - Lifecycle commands preserve named volumes. `reset` uses Compose `down --remove-orphans` without `--volumes`; data deletion is never implicit. - Preflight reports missing Docker CLI/daemon, host port collisions, and diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index 829b932a..7f27517e 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -33,6 +33,7 @@ Run these from the repository root: | `pnpm local:services reset` | Recreate containers/networks while preserving named volumes; it never passes `--volumes`. | | `pnpm local:services restart-check` | Restart the running stack and verify health after restart. | | `pnpm local:services status` | Print container/health state without changing it. | +| `pnpm local:services logs --tail=100` | Print bounded, read-only logs for known local services. | The older `pnpm local:smoke -- --start` form remains supported. Port collisions can be resolved by copying `.env.example` to `.env` and changing the host port @@ -64,6 +65,9 @@ package manager while still making readiness observable. - The Compose health checks are the readiness contract for local consumers. `pnpm local:services status` reports the current health and `pnpm local:services restart-check` verifies restart persistence. +- `pnpm local:services logs --service=postgres --tail=100` is read-only and + accepts only known service names. Logs are local diagnostics; review them + before sharing because provider messages can still contain development data. - If a previous run left a stopped container, rerun `pnpm local:services start`; it is idempotent and does not delete volumes. - If Docker is unavailable, the static infrastructure tests still validate the diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 566f58a3..8c77dc13 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -18,6 +18,7 @@ const services = [ 'otel-collector', 'otel-collector-health', ]; +const logServices = [...services, 'minio-init']; const hostPorts = [ { service: 'postgres', key: 'POSTGRES_PORT', fallback: 5432 }, { service: 'redis', key: 'REDIS_PORT', fallback: 6379 }, @@ -42,11 +43,14 @@ Commands (all preserve named volumes): reset recreate containers and networks, preserving volumes restart-check restart running services and verify health/persistence status print current container and health state + logs print bounded local container logs (read-only) smoke legacy readiness command (use --start to start first) Options: --start with smoke, start services before polling --wait-seconds=N readiness timeout (default: 60, maximum: 3600) + --tail=N log lines per service (default: 100, maximum: 1000) + --service=NAME limit logs to one known local service --min-free-gib=N minimum host free space (default: 5) --help show this help @@ -212,6 +216,8 @@ function parseArguments(argv) { const options = { start: false, waitSeconds: 60, + tail: 100, + service: undefined, minFreeGib: Number(process.env.DATABREEZE_MIN_FREE_GIB || 5), }; for (const argument of argumentsToParse) { @@ -224,6 +230,14 @@ function parseArguments(argv) { options.waitSeconds = Number(argument.slice('--wait-seconds='.length)); continue; } + if (argument.startsWith('--tail=')) { + options.tail = Number(argument.slice('--tail='.length)); + continue; + } + if (argument.startsWith('--service=')) { + options.service = argument.slice('--service='.length); + continue; + } if (argument.startsWith('--min-free-gib=')) { options.minFreeGib = Number(argument.slice('--min-free-gib='.length)); continue; @@ -233,10 +247,16 @@ function parseArguments(argv) { if (!Number.isInteger(options.waitSeconds) || options.waitSeconds < 1 || options.waitSeconds > 3600) { fail('--wait-seconds must be an integer from 1 to 3600'); } + if (!Number.isInteger(options.tail) || options.tail < 1 || options.tail > 1000) { + fail('--tail must be an integer from 1 to 1000'); + } + if (options.service !== undefined && !logServices.includes(options.service)) { + fail(`--service must name one of: ${logServices.join(', ')}`); + } if (!Number.isFinite(options.minFreeGib) || options.minFreeGib < 0) { fail('--min-free-gib must be a non-negative number'); } - if (!['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke'].includes(command)) { + if (!['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'logs', 'smoke'].includes(command)) { fail(`unknown command: ${command}`); } return { command, options }; @@ -268,6 +288,11 @@ export async function main(argv = process.argv.slice(2)) { for (const service of services) console.log(`${service}: ${inspectHealth(service, values).detail}`); return; } + if (command === 'logs') { + const selected = options.service ? [options.service] : logServices; + runDocker([...composeArgs(values), 'logs', '--no-color', `--tail=${options.tail}`, ...selected]); + return; + } if (command === 'stop') { runDocker([...composeArgs(values), 'stop']); console.log('Local services stopped; named volumes and containers were preserved.'); diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 7393605a..50178f46 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -117,7 +117,7 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); - for (const command of ['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'smoke']) { + for (const command of ['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'logs', 'smoke']) { assert.match(result.stdout, new RegExp(`^ ${command}\\s`, 'm')); } assert.match(script, /statfsSync/u); @@ -144,9 +144,26 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.notEqual(invalidDisk.status, 0); assert.match(`${invalidDisk.stdout}\n${invalidDisk.stderr}`, /--min-free-gib must be a non-negative number/u); + const invalidTail = spawnSync(process.execPath, [helpScript, 'logs', '--tail=0'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(invalidTail.status, 0); + assert.match(`${invalidTail.stdout}\n${invalidTail.stderr}`, /--tail must be an integer/u); + + const invalidService = spawnSync(process.execPath, [helpScript, 'logs', '--service=unknown'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(invalidService.status, 0); + assert.match(`${invalidService.stdout}\n${invalidService.stderr}`, /--service must name one of/u); + const composeConfig = spawnSync(process.execPath, [helpScript, 'config'], { cwd: repositoryRoot, encoding: 'utf8', }); if (composeConfig.status === 0) assert.match(composeConfig.stdout, /Compose configuration is valid/u); + assert.match(script, /logs', '--no-color/u); + assert.match(script, /--service must name one of/u); + assert.match(script, /--tail must be an integer/u); }); From e69e5cd5a9c83fdef086572826494c8be9c6bc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:20:35 +0700 Subject: [PATCH 53/89] fix(infra): validate local compose project names --- tools/repo-cli/src/local-services.mjs | 10 +++++++++- tools/repo-cli/test/local-infrastructure.test.mjs | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 8c77dc13..ed633b36 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -82,6 +82,14 @@ function environment() { return fileValues; } +function projectName(values) { + const value = values.get('COMPOSE_PROJECT_NAME') || process.env.COMPOSE_PROJECT_NAME || 'databreeze-local'; + if (!/^[a-z0-9][a-z0-9_-]{0,62}$/u.test(value)) { + fail('COMPOSE_PROJECT_NAME must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, or underscores'); + } + return value; +} + function portValue(definition, values) { const value = Number(values.get(definition.key) ?? definition.fallback); if (!Number.isInteger(value) || value < 1024 || value > 65535) { @@ -92,7 +100,7 @@ function portValue(definition, values) { function composeArgs(values = environment()) { const envFile = existsSync(localEnvFile) ? localEnvFile : exampleEnvFile; - const project = values.get('COMPOSE_PROJECT_NAME') || process.env.COMPOSE_PROJECT_NAME || 'databreeze-local'; + const project = projectName(values); return ['compose', '--project-name', project, '--env-file', envFile, '-f', composeFile]; } diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 50178f46..b7009355 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -158,6 +158,14 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.notEqual(invalidService.status, 0); assert.match(`${invalidService.stdout}\n${invalidService.stderr}`, /--service must name one of/u); + const invalidProject = spawnSync(process.execPath, [helpScript, 'config'], { + cwd: repositoryRoot, + encoding: 'utf8', + env: { ...process.env, COMPOSE_PROJECT_NAME: '../unsafe-project' }, + }); + assert.notEqual(invalidProject.status, 0); + assert.match(`${invalidProject.stdout}\n${invalidProject.stderr}`, /COMPOSE_PROJECT_NAME must start/u); + const composeConfig = spawnSync(process.execPath, [helpScript, 'config'], { cwd: repositoryRoot, encoding: 'utf8', @@ -166,4 +174,5 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.match(script, /logs', '--no-color/u); assert.match(script, /--service must name one of/u); assert.match(script, /--tail must be an integer/u); + assert.match(script, /COMPOSE_PROJECT_NAME must start/u); }); From 560f5d77eab812c5d9b193bfed46c5ce8849a066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:20:47 +0700 Subject: [PATCH 54/89] fix(infra): honor safe project override from environment --- tools/repo-cli/src/local-services.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index ed633b36..7a3dcd6e 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -76,6 +76,9 @@ function parseEnvFile(file) { function environment() { const fileValues = parseEnvFile(existsSync(localEnvFile) ? localEnvFile : exampleEnvFile); + if (process.env.COMPOSE_PROJECT_NAME !== undefined) { + fileValues.set('COMPOSE_PROJECT_NAME', process.env.COMPOSE_PROJECT_NAME); + } for (const definition of hostPorts) { if (process.env[definition.key] !== undefined) fileValues.set(definition.key, process.env[definition.key]); } From 6a0efe793607f501817f8d5c6d083e9dd796ea8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:21:31 +0700 Subject: [PATCH 55/89] ci: require every provenance artifact --- tools/repo-cli/src/generate-provenance.mjs | 18 +++++-- tools/repo-cli/test/provenance.test.mjs | 57 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 tools/repo-cli/test/provenance.test.mjs diff --git a/tools/repo-cli/src/generate-provenance.mjs b/tools/repo-cli/src/generate-provenance.mjs index 49026f34..e3540724 100644 --- a/tools/repo-cli/src/generate-provenance.mjs +++ b/tools/repo-cli/src/generate-provenance.mjs @@ -3,12 +3,20 @@ import fs from 'node:fs'; import path from 'node:path'; const output = process.argv[process.argv.indexOf('--output') + 1] || 'artifacts/provenance.json'; -const artifactArguments = process.argv.reduce((result, value, index, values) => { - if (value === '--artifact' && values[index + 1]) result.push(values[index + 1]); - return result; -}, []); +const artifactArguments = []; +for (let index = 0; index < process.argv.length; index += 1) { + if (process.argv[index] !== '--artifact') continue; + const artifact = process.argv[index + 1]; + if (!artifact || artifact.startsWith('--')) { + throw new Error('--artifact requires a file path'); + } + artifactArguments.push(artifact); +} +const missingArtifacts = artifactArguments.filter((file) => !fs.existsSync(file)); +if (missingArtifacts.length > 0) { + throw new Error(`Provenance artifact(s) do not exist: ${missingArtifacts.join(', ')}`); +} const artifacts = artifactArguments - .filter((file) => fs.existsSync(file)) .map((file) => { const bytes = fs.readFileSync(file); return { diff --git a/tools/repo-cli/test/provenance.test.mjs b/tools/repo-cli/test/provenance.test.mjs new file mode 100644 index 00000000..56a3a26f --- /dev/null +++ b/tools/repo-cli/test/provenance.test.mjs @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; + +const repositoryRoot = path.resolve(import.meta.dirname, '..', '..', '..'); +const script = path.join(repositoryRoot, 'tools/repo-cli/src/generate-provenance.mjs'); + +test('provenance generation records sorted artifact digests', () => { + const directory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-provenance-')); + try { + const first = path.join(directory, 'z-output.json'); + const second = path.join(directory, 'a-output.json'); + const output = path.join(directory, 'provenance.json'); + writeFileSync(first, 'z'); + writeFileSync(second, 'a'); + const result = spawnSync( + process.execPath, + [script, '--output', output, '--artifact', first, '--artifact', second], + { cwd: repositoryRoot, encoding: 'utf8' }, + ); + assert.equal(result.status, 0, result.stderr); + const provenance = JSON.parse(readFileSync(output, 'utf8')); + assert.deepEqual( + provenance.subject.map((subject) => path.posix.basename(subject.path)), + ['a-output.json', 'z-output.json'], + ); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test('provenance generation fails closed when an artifact is missing', () => { + const directory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-provenance-missing-')); + try { + const result = spawnSync( + process.execPath, + [script, '--output', path.join(directory, 'provenance.json'), '--artifact', path.join(directory, 'missing.json')], + { cwd: repositoryRoot, encoding: 'utf8' }, + ); + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}\n${result.stderr}`, /Provenance artifact\(s\) do not exist/u); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test('provenance generation rejects an artifact flag without a path', () => { + const result = spawnSync(process.execPath, [script, '--artifact'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}\n${result.stderr}`, /--artifact requires a file path/u); +}); From 291d3d30144e5778046bea2500404bed7cdfb04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:21:47 +0700 Subject: [PATCH 56/89] fix(ci): validate provenance output arguments --- tools/repo-cli/src/generate-provenance.mjs | 6 +++++- tools/repo-cli/test/provenance.test.mjs | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/repo-cli/src/generate-provenance.mjs b/tools/repo-cli/src/generate-provenance.mjs index e3540724..46aa86b8 100644 --- a/tools/repo-cli/src/generate-provenance.mjs +++ b/tools/repo-cli/src/generate-provenance.mjs @@ -2,7 +2,11 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; -const output = process.argv[process.argv.indexOf('--output') + 1] || 'artifacts/provenance.json'; +const outputIndex = process.argv.indexOf('--output'); +const output = outputIndex === -1 ? 'artifacts/provenance.json' : process.argv[outputIndex + 1]; +if (!output || output.startsWith('--')) { + throw new Error('--output requires a file path'); +} const artifactArguments = []; for (let index = 0; index < process.argv.length; index += 1) { if (process.argv[index] !== '--artifact') continue; diff --git a/tools/repo-cli/test/provenance.test.mjs b/tools/repo-cli/test/provenance.test.mjs index 56a3a26f..40a50201 100644 --- a/tools/repo-cli/test/provenance.test.mjs +++ b/tools/repo-cli/test/provenance.test.mjs @@ -55,3 +55,12 @@ test('provenance generation rejects an artifact flag without a path', () => { assert.notEqual(result.status, 0); assert.match(`${result.stdout}\n${result.stderr}`, /--artifact requires a file path/u); }); + +test('provenance generation rejects an output flag without a path', () => { + const result = spawnSync(process.execPath, [script, '--output'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}\n${result.stderr}`, /--output requires a file path/u); +}); From 38a46707b6019a329abf6153fbd552324a4e659d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:22:04 +0700 Subject: [PATCH 57/89] ci: validate SBOM output arguments --- tools/repo-cli/src/generate-sbom.mjs | 6 ++++- tools/repo-cli/test/sbom.test.mjs | 33 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tools/repo-cli/test/sbom.test.mjs diff --git a/tools/repo-cli/src/generate-sbom.mjs b/tools/repo-cli/src/generate-sbom.mjs index bf5cd75f..2536da38 100644 --- a/tools/repo-cli/src/generate-sbom.mjs +++ b/tools/repo-cli/src/generate-sbom.mjs @@ -2,7 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; const root = process.cwd(); -const output = process.argv[process.argv.indexOf('--output') + 1] || 'artifacts/sbom.cdx.json'; +const outputIndex = process.argv.indexOf('--output'); +const output = outputIndex === -1 ? 'artifacts/sbom.cdx.json' : process.argv[outputIndex + 1]; +if (!output || output.startsWith('--')) { + throw new Error('--output requires a file path'); +} const ignored = new Set([ 'node_modules', '.git', diff --git a/tools/repo-cli/test/sbom.test.mjs b/tools/repo-cli/test/sbom.test.mjs new file mode 100644 index 00000000..4d0159a4 --- /dev/null +++ b/tools/repo-cli/test/sbom.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; + +const repositoryRoot = path.resolve(import.meta.dirname, '..', '..', '..'); +const script = path.join(repositoryRoot, 'tools/repo-cli/src/generate-sbom.mjs'); + +test('SBOM generation rejects an output flag without a path', () => { + const result = spawnSync(process.execPath, [script, '--output'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}\n${result.stderr}`, /--output requires a file path/u); +}); + +test('SBOM generation writes to an explicit output path', () => { + const directory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-sbom-')); + try { + const output = path.join(directory, 'sbom.json'); + const result = spawnSync(process.execPath, [script, '--output', output], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Wrote .*sbom\.json/u); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); From 7bef4fdbfdbff1266a0486eaec76d54e159b4b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:22:19 +0700 Subject: [PATCH 58/89] test(infra): reject invalid disk threshold environment --- tools/repo-cli/test/local-infrastructure.test.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index b7009355..1a7a091c 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -166,6 +166,17 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.notEqual(invalidProject.status, 0); assert.match(`${invalidProject.stdout}\n${invalidProject.stderr}`, /COMPOSE_PROJECT_NAME must start/u); + const invalidEnvironmentDisk = spawnSync(process.execPath, [helpScript, 'preflight'], { + cwd: repositoryRoot, + encoding: 'utf8', + env: { ...process.env, DATABREEZE_MIN_FREE_GIB: 'not-a-number' }, + }); + assert.notEqual(invalidEnvironmentDisk.status, 0); + assert.match( + `${invalidEnvironmentDisk.stdout}\n${invalidEnvironmentDisk.stderr}`, + /--min-free-gib must be a non-negative number/u, + ); + const composeConfig = spawnSync(process.execPath, [helpScript, 'config'], { cwd: repositoryRoot, encoding: 'utf8', From 74737c50cc1ac5675e65be3ad34b8e67cd64b97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:22:41 +0700 Subject: [PATCH 59/89] fix(infra): bound local container log storage --- infrastructure/local/compose.yml | 13 +++++++++++++ tools/repo-cli/test/local-infrastructure.test.mjs | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/infrastructure/local/compose.yml b/infrastructure/local/compose.yml index 05f0cd74..f4c9f170 100644 --- a/infrastructure/local/compose.yml +++ b/infrastructure/local/compose.yml @@ -1,5 +1,11 @@ name: ${COMPOSE_PROJECT_NAME:-databreeze-local} +x-default-logging: &default-logging + driver: json-file + options: + max-size: 10m + max-file: '3' + services: postgres: image: ${POSTGRES_IMAGE:-docker.io/library/postgres:17.5-alpine} @@ -11,6 +17,7 @@ services: ports: - '${POSTGRES_PORT:-5432}:5432' networks: [local] + logging: *default-logging volumes: - postgres-data:/var/lib/postgresql/data - ./postgres/init:/docker-entrypoint-initdb.d:ro @@ -31,6 +38,7 @@ services: ports: - '${REDIS_PORT:-6379}:6379' networks: [local] + logging: *default-logging volumes: - redis-data:/data healthcheck: @@ -51,6 +59,7 @@ services: - '${MINIO_API_PORT:-9000}:9000' - '${MINIO_CONSOLE_PORT:-9001}:9001' networks: [local] + logging: *default-logging volumes: - minio-data:/data healthcheck: @@ -68,6 +77,7 @@ services: minio: condition: service_healthy networks: [local] + logging: *default-logging environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-databreeze} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-databreeze-local-change-me} @@ -88,6 +98,7 @@ services: - '${MAILPIT_SMTP_PORT:-1025}:1025' - '${MAILPIT_UI_PORT:-8025}:8025' networks: [local] + logging: *default-logging volumes: - mailpit-data:/data healthcheck: @@ -106,6 +117,7 @@ services: - '${OTEL_HTTP_PORT:-4318}:4318' - '${OTEL_HEALTH_PORT:-13133}:13133' networks: [local] + logging: *default-logging volumes: - ./otel/collector.yaml:/etc/otelcol-contrib/config.yaml:ro # The collector image is intentionally distroless and has no shell or @@ -126,6 +138,7 @@ services: otel-collector: condition: service_started networks: [local] + logging: *default-logging command: ['sh', '-c', "trap 'exit 0' TERM INT; while :; do sleep 3600; done"] healthcheck: test: [CMD, curl, --fail, --silent, http://otel-collector:13133/] diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 1a7a091c..4d058f7e 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -39,6 +39,10 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, /postgres-data:[\s\S]*name: \$\{COMPOSE_PROJECT_NAME/u); assert.match(compose, /networks: \[local\]/u); assert.match(compose, /name: \$\{COMPOSE_PROJECT_NAME:-databreeze-local\}-network/u); + assert.match(compose, /x-default-logging: &default-logging/u); + assert.match(compose, /max-size: 10m/u); + assert.match(compose, /max-file: '3'/u); + assert.equal((compose.match(/logging: \*default-logging/g) ?? []).length, 7); }); test('local bootstrap is credential-free and creates every owned module schema', () => { From 511e70c4eba149a1dbf2f5a5327d42af8e70e9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:22:55 +0700 Subject: [PATCH 60/89] docs(infra): explain local log retention --- infrastructure/local/README.md | 2 ++ tools/repo-cli/test/local-infrastructure.test.mjs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index 7f27517e..307c1878 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -18,6 +18,8 @@ named volumes prefixed by the Compose project name; no repository directory is mounted for database, object, or mail data. The volumes are disposable and are not removed by the lifecycle commands. Remove the named volumes only when you explicitly want to discard local state. +Container JSON logs are capped at 10 MiB per file with three retained files so +diagnostics cannot silently consume the host disk. ## Lifecycle commands diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 4d058f7e..e5466fec 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -102,6 +102,12 @@ test('local OpenTelemetry collector keeps every signal on the bounded local pipe assert.doesNotMatch(collector, /filelog|otlphttp|s3|https?:\/\//iu); }); +test('local infrastructure documents bounded diagnostic storage', () => { + const readme = read('infrastructure/local/README.md'); + assert.match(readme, /Container JSON logs are capped at 10 MiB per file/u); + assert.match(readme, /three retained files/u); +}); + test('readiness smoke script exposes a non-destructive help command', () => { const script = path.join(repositoryRoot, 'tools', 'repo-cli', 'src', 'local-services-smoke.mjs'); const result = spawnSync(process.execPath, [script, '--help'], { From 039de6b0bc51a351d0d9574d8f9cb08e79036e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:23:15 +0700 Subject: [PATCH 61/89] docs(ci): record supply chain gate evidence --- .../foundation-ci-supply-chain-2026-08-02.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/operations/foundation-ci-supply-chain-2026-08-02.md diff --git a/docs/operations/foundation-ci-supply-chain-2026-08-02.md b/docs/operations/foundation-ci-supply-chain-2026-08-02.md new file mode 100644 index 00000000..d4fe3238 --- /dev/null +++ b/docs/operations/foundation-ci-supply-chain-2026-08-02.md @@ -0,0 +1,45 @@ +# FND-006 CI and supply-chain evidence + +Observed at (UTC): 2026-08-02 +Branch: `feat/fnd003-local-infra-batch` +Task: `FND-006 — Close CI and supply-chain gaps` + +## Implemented boundaries + +- Path-aware scope detection treats infrastructure, tooling, contracts, plans, + and workflows as shared quality-gate inputs. +- Every required workflow uses SHA-pinned actions, least-privilege top-level + permissions, discarded checkout credentials, bounded runner timeouts, and + no `pull_request_target` or long-lived AWS credentials. +- Artifact uploads fail when an expected output is missing. +- SBOM and provenance generators reject malformed output arguments; provenance + fails closed when a declared artifact is missing and records sorted SHA-256 + subjects. +- Container image, secret-pattern, license, SBOM, and provenance checks remain + non-deploying repository operations. + +## Verification + +Passed: + +- `node --test tools/repo-cli/test/ci-policy.test.mjs` +- `node --test tools/repo-cli/test/provenance.test.mjs` +- `node --test tools/repo-cli/test/sbom.test.mjs` +- `node tools/repo-cli/src/check-ci-policy.mjs` +- `node tools/repo-cli/src/check-container-policy.mjs` +- `node tools/repo-cli/src/check-secret-patterns.mjs` +- `node tools/repo-cli/src/check-license-policy.mjs` +- `node tools/repo-cli/src/generate-sbom.mjs --output ` +- `node tools/repo-cli/src/generate-provenance.mjs --output --artifact ` +- `git diff --check` + +Hosted CI remains authoritative for the complete dependency, SAST, container, +OpenTofu, build, and release-environment gates. The generators never write +runtime evidence inside the repository during these checks. + +## Rollback + +Revert the focused CI or generator commit that introduced the behavior, rerun +the scoped tests and `pnpm ci:policy`, then record the resulting gap before +merging. No cloud resource, credential, or customer data is changed by these +checks. From f293d1ae671bd46cad98a4f241ac491a54c9dd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:23:31 +0700 Subject: [PATCH 62/89] docs(orchestration): refresh foundation batch checkpoint --- docs/plans/execution-orchestration.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index efe43425..a3aff955 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -80,15 +80,16 @@ }, "FND-003": { "status": "in-progress", - "commit": "fcabfb3", + "commit": "039de6b", "evidence": [ "infrastructure/local/compose.yml", + "infrastructure/local/README.md", "infrastructure/local/postgres/init/001-create-module-schemas.sql", "tools/repo-cli/src/local-services.mjs", "tools/repo-cli/test/local-infrastructure.test.mjs", "docs/operations/foundation-local-infrastructure-2026-08-02.md" ], - "note": "Static Compose/bootstrap/lifecycle checks, daemon-free config validation, AWS safety checks, telemetry redaction tests, and infrastructure path-aware CI pass. Live Docker health, port-collision, disk-pressure, and restart-persistence evidence remains pending because the Docker daemon is unavailable in this environment." + "note": "Static Compose/bootstrap/lifecycle checks, daemon-free config/preflight, bounded local diagnostics/log retention, AWS safety checks, telemetry redaction tests, and infrastructure path-aware CI pass. Live Docker health, port-collision, disk-pressure, and restart-persistence evidence remains pending because the Docker daemon is unavailable in this environment." } }, "plans": [ From b1bb09202233a760fffb6a4b96162207583be5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:23:39 +0700 Subject: [PATCH 63/89] docs(operations): record foundation batch commit window --- docs/operations/foundation-batch-handoff-2026-08-02.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/operations/foundation-batch-handoff-2026-08-02.md b/docs/operations/foundation-batch-handoff-2026-08-02.md index 356a98b6..6b93b8c8 100644 --- a/docs/operations/foundation-batch-handoff-2026-08-02.md +++ b/docs/operations/foundation-batch-handoff-2026-08-02.md @@ -36,6 +36,12 @@ PR until the branch reaches at least 30 commits, targeting approximately 70 and never reaching 100. The only small-PR exceptions are focused promotion-review fixes required to close an already-open `dev`→`main` gate. +At this checkpoint the branch is 62 commits ahead of `origin/dev`. The current +boundary is still coherent: FND-003 local lifecycle hardening is accompanied by +portable AWS safety, telemetry, CI/supply-chain, and evidence updates. Continue +with scoped foundation work until the final handoff boundary; do not manufacture +empty commits or open a small feature PR merely to reset the count. + ## Safest next command ```powershell From 20ff047fd336716171a8007a4bf96a785a1ac62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:24:23 +0700 Subject: [PATCH 64/89] feat(infra): verify Redis persistence after restart --- ...ndation-local-infrastructure-2026-08-02.md | 3 +++ infrastructure/local/README.md | 1 + tools/repo-cli/src/local-services.mjs | 26 ++++++++++++++++++- .../test/local-infrastructure.test.mjs | 4 ++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index bc106c5f..70d6fdbc 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -20,6 +20,8 @@ Task: `FND-003 — Close local infrastructure gaps` insufficient free disk space before starting containers. - `restart-check` restarts the running stack and waits for every service health check, providing the entry point for persistence evidence. +- `persistence-check` writes a five-minute Redis sentinel, restarts only Redis, + verifies the sentinel, and deletes it; it never flushes a database or volume. ## Verification @@ -30,6 +32,7 @@ Passed: - `node tools/repo-cli/src/local-services-smoke.mjs --help` - `node tools/repo-cli/src/local-services.mjs config` - `node tools/repo-cli/src/local-services.mjs preflight --min-free-gib=0` +- `node tools/repo-cli/src/local-services.mjs persistence-check` *(Docker-gated)* - `git diff --check` Environment-gated: diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index 307c1878..ecb9add1 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -34,6 +34,7 @@ Run these from the repository root: | `pnpm local:services stop` | Stop containers while preserving containers and named volumes. | | `pnpm local:services reset` | Recreate containers/networks while preserving named volumes; it never passes `--volumes`. | | `pnpm local:services restart-check` | Restart the running stack and verify health after restart. | +| `pnpm local:services persistence-check` | Restart Redis and verify a disposable sentinel survives. | | `pnpm local:services status` | Print container/health state without changing it. | | `pnpm local:services logs --tail=100` | Print bounded, read-only logs for known local services. | diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 7a3dcd6e..e5613bef 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -42,6 +42,7 @@ Commands (all preserve named volumes): stop stop containers without removing containers or volumes reset recreate containers and networks, preserving volumes restart-check restart running services and verify health/persistence + persistence-check restart Redis and verify a disposable sentinel survives status print current container and health state logs print bounded local container logs (read-only) smoke legacy readiness command (use --start to start first) @@ -267,7 +268,7 @@ function parseArguments(argv) { if (!Number.isFinite(options.minFreeGib) || options.minFreeGib < 0) { fail('--min-free-gib must be a non-negative number'); } - if (!['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'logs', 'smoke'].includes(command)) { + if (!['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'persistence-check', 'status', 'logs', 'smoke'].includes(command)) { fail(`unknown command: ${command}`); } return { command, options }; @@ -334,6 +335,29 @@ export async function main(argv = process.argv.slice(2)) { console.log('Local service restart and health persistence check passed.'); return; } + if (command === 'persistence-check') { + const key = `databreeze:local:persistence-check:${process.pid}`; + const value = `${Date.now()}`; + runDocker([ + ...composeArgs(values), + 'exec', + '-T', + 'redis', + 'redis-cli', + 'SET', + key, + value, + 'EX', + '300', + ]); + runDocker([...composeArgs(values), 'restart', 'redis']); + await waitForReady(values, options.waitSeconds); + const result = runDocker([...composeArgs(values), 'exec', '-T', 'redis', 'redis-cli', 'GET', key]); + if (result.stdout.trim() !== value) fail('Redis persistence sentinel was not recovered after restart'); + runDocker([...composeArgs(values), 'exec', '-T', 'redis', 'redis-cli', 'DEL', key]); + console.log('Local Redis persistence check passed; sentinel was removed.'); + return; + } if (shouldStart) runDocker([...composeArgs(values), 'up', '-d']); await waitForReady(values, options.waitSeconds); } diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index e5466fec..ba941635 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -127,7 +127,7 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); - for (const command of ['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'status', 'logs', 'smoke']) { + for (const command of ['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'persistence-check', 'status', 'logs', 'smoke']) { assert.match(result.stdout, new RegExp(`^ ${command}\\s`, 'm')); } assert.match(script, /statfsSync/u); @@ -196,4 +196,6 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum assert.match(script, /--service must name one of/u); assert.match(script, /--tail must be an integer/u); assert.match(script, /COMPOSE_PROJECT_NAME must start/u); + assert.match(script, /Redis persistence sentinel was not recovered/u); + assert.doesNotMatch(script, /redis-cli\s+FLUSH(?:ALL|DB)/iu); }); From a19917e3146c7718c72a904752ebe4aa959f51d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:24:36 +0700 Subject: [PATCH 65/89] fix(infra): distinguish restart and persistence checks --- tools/repo-cli/src/local-services.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index e5613bef..9c6d53cc 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -332,7 +332,7 @@ export async function main(argv = process.argv.slice(2)) { if (command === 'restart-check') { runDocker([...composeArgs(values), 'restart']); await waitForReady(values, options.waitSeconds); - console.log('Local service restart and health persistence check passed.'); + console.log('Local service restart and health checks passed. Use persistence-check for a Redis sentinel probe.'); return; } if (command === 'persistence-check') { From 46145eed1d54d6e270ded97bb0722cb249e4beb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:25:29 +0700 Subject: [PATCH 66/89] fix(telemetry): fail closed on hostile trace headers --- packages/telemetry/src/v1.ts | 12 +++++++-- packages/telemetry/test/telemetry-v1.test.mjs | 14 +++++++++++ .../engine/src/databreeze_engine/telemetry.py | 25 +++++++++++++------ services/engine/tests/test_telemetry.py | 15 +++++++++++ 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/telemetry/src/v1.ts b/packages/telemetry/src/v1.ts index 5977b628..6af5f785 100644 --- a/packages/telemetry/src/v1.ts +++ b/packages/telemetry/src/v1.ts @@ -259,9 +259,17 @@ function readSingleHeader( name: string, ): string | undefined { const values: string[] = []; - for (const [key, value] of Object.entries(headers)) { + for (const key of Object.keys(headers)) { + const descriptor = Object.getOwnPropertyDescriptor(headers, key); + if (!descriptor || !('value' in descriptor)) throw new Error(`Unreadable telemetry ${name} header`); + const value = descriptor.value as string | string[] | undefined; if (key.toLowerCase() !== name) continue; - if (Array.isArray(value)) values.push(...value); + if (Array.isArray(value)) { + if (!value.every((item) => typeof item === 'string')) { + throw new Error(`Unreadable telemetry ${name} header`); + } + values.push(...value); + } else if (value !== undefined) values.push(value); } if (values.length > 1) throw new Error(`Ambiguous telemetry ${name} header`); diff --git a/packages/telemetry/test/telemetry-v1.test.mjs b/packages/telemetry/test/telemetry-v1.test.mjs index 3c7daf31..a00b9234 100644 --- a/packages/telemetry/test/telemetry-v1.test.mjs +++ b/packages/telemetry/test/telemetry-v1.test.mjs @@ -71,6 +71,20 @@ test('telemetry never executes accessor-backed attributes', () => { assert.equal(accessed, false); }); +test('telemetry never executes accessor-backed correlation headers', () => { + let accessed = false; + const hostile = {}; + Object.defineProperty(hostile, 'x-correlation-id', { + enumerable: true, + get() { + accessed = true; + throw new Error('hostile header getter'); + }, + }); + assert.throws(() => correlationFromHeadersV1(hostile), /Unreadable telemetry/u); + assert.equal(accessed, false); +}); + test('correlation headers round-trip without accepting malformed identifiers', () => { const context = createCorrelationContextV1({ correlationId, diff --git a/services/engine/src/databreeze_engine/telemetry.py b/services/engine/src/databreeze_engine/telemetry.py index bc2b817a..ab659f9f 100644 --- a/services/engine/src/databreeze_engine/telemetry.py +++ b/services/engine/src/databreeze_engine/telemetry.py @@ -256,13 +256,24 @@ def correlation_headers(context: CorrelationContext) -> dict[str, str]: def _single_header(headers: Mapping[str, str | Sequence[str] | None], name: str) -> str | None: values: list[str] = [] - for key, value in headers.items(): - if key.lower() != name: - continue - if isinstance(value, str): - values.append(value) - elif value is not None: - values.extend(value) + try: + for key, value in headers.items(): + if not isinstance(key, str): + raise ValueError("telemetry header name is not a string") + if key.lower() != name: + continue + if isinstance(value, str): + values.append(value) + elif value is not None: + if not isinstance(value, Sequence) or isinstance(value, (bytes, bytearray)): + raise ValueError("telemetry header value is not readable") + if not all(isinstance(item, str) for item in value): + raise ValueError("telemetry header value is not readable") + values.extend(value) + except ValueError: + raise + except Exception: + raise ValueError("telemetry headers are not readable") from None if len(values) > 1: raise ValueError(f"ambiguous telemetry {name} header") if not values: diff --git a/services/engine/tests/test_telemetry.py b/services/engine/tests/test_telemetry.py index 050dfc4a..c5d4e35c 100644 --- a/services/engine/tests/test_telemetry.py +++ b/services/engine/tests/test_telemetry.py @@ -78,6 +78,21 @@ def items(self): # type: ignore[override] assert_safe_attributes(HostileMapping()) +def test_engine_telemetry_rejects_hostile_or_non_string_header_values() -> None: + class HostileHeaders(dict[str, object]): + def items(self): # type: ignore[override] + raise RuntimeError("provider header cause must not escape") + + with pytest.raises(ValueError, match="not readable"): + correlation_from_headers(HostileHeaders()) + with pytest.raises(ValueError, match="not readable"): + correlation_from_headers( + { + "x-correlation-id": [1, 2], + } + ) + + def test_engine_accepts_mixed_case_header_names() -> None: context = CorrelationContext( "00000000-0000-4000-8000-000000000001", From 30421f5da17a8dc65e06836f256cdff4ef61d071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:25:39 +0700 Subject: [PATCH 67/89] docs(telemetry): document hostile header handling --- docs/operations/telemetry-safety.md | 5 +++-- packages/telemetry/README.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/operations/telemetry-safety.md b/docs/operations/telemetry-safety.md index b7e860a0..0d42b961 100644 --- a/docs/operations/telemetry-safety.md +++ b/docs/operations/telemetry-safety.md @@ -23,8 +23,9 @@ so accessor-backed diagnostics cannot execute arbitrary getters during logging. ## Failure behavior Malformed correlation or trace headers fail closed. Ambiguous duplicate headers -are rejected. Invalid or oversized values are omitted by the permissive -sanitizer and rejected by strict mode. Providers and exporters remain +are rejected, including accessor-backed or non-string header values; hostile +header mappings fail with a generic unreadable-header reason. Invalid or +oversized values are omitted by the permissive sanitizer and rejected by strict mode. Providers and exporters remain replaceable; a collector outage cannot become domain authority or block durable jobs and audit writes. diff --git a/packages/telemetry/README.md b/packages/telemetry/README.md index 50a7462c..3f2157c1 100644 --- a/packages/telemetry/README.md +++ b/packages/telemetry/README.md @@ -15,8 +15,9 @@ filenames, source values, prompts, evidence, contact data, and content) are rejected by the strict assertion helper and never serialized. Sanitization reads only own data properties. Accessor-backed or hostile -diagnostic fields are ignored (or rejected by the strict helper) without -executing their getters. +diagnostic fields and correlation headers are ignored (or rejected by the +strict helper) without executing their getters. Header arrays must contain +strings; provider mapping failures become a generic unreadable-header error. Use `createStructuredLoggerV1` at runtime boundaries and pass only the correlation headers produced by `correlationHeadersV1`. A logger sink receives From 0072076473791814c22c7ac45e870b4daa3acd89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:25:53 +0700 Subject: [PATCH 68/89] ci: require explicit read-only repository permissions --- tools/repo-cli/src/check-ci-policy.mjs | 3 +++ tools/repo-cli/test/ci-policy.test.mjs | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index b095d70b..9202479c 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -30,6 +30,9 @@ function assertLeastPrivilege(text, filename) { if (!/^permissions:\s*$/im.test(text)) { throw new Error(`${filename} must declare a top-level permissions block`); } + if (!/^\s+contents:\s*read\s*$/im.test(text)) { + throw new Error(`${filename} must grant contents: read explicitly`); + } if (/pull_request_target:/iu.test(text)) { throw new Error(`${filename} must not execute untrusted code from pull_request_target`); } diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index 0d0df2b1..4675c837 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -11,8 +11,11 @@ test('repository workflows are present, pinned, and least privilege', () => { }); test('CI policy requires checkout credentials to be discarded', () => { - const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows/quality.yml'), 'utf8'); - assert.match(workflow, /persist-credentials:\s*false/u); + for (const name of ['quality.yml', 'security.yml', 'release.yml']) { + const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows', name), 'utf8'); + assert.match(workflow, /persist-credentials:\s*false/u); + assert.match(workflow, /^\s+contents:\s*read\s*$/mu); + } }); test('CI policy fails closed when an artifact output is missing', () => { From 6e5b93013a47f279847ea325b41a8514157a7efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:26:03 +0700 Subject: [PATCH 69/89] ci: require the protected release environment --- tools/repo-cli/src/check-ci-policy.mjs | 3 +++ tools/repo-cli/test/ci-policy.test.mjs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index 9202479c..4d1be5d7 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -88,6 +88,9 @@ export function checkCiPolicy(root = process.cwd()) { if (!release.includes('generate-provenance.mjs')) { throw new Error('release.yml must generate a provenance record'); } + if (!/^\s+environment:\s*release\s*$/im.test(release)) { + throw new Error('release.yml must use the protected release environment'); + } return { workflowCount: REQUIRED_WORKFLOWS.length }; } diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index 4675c837..b63e051a 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -23,6 +23,11 @@ test('CI policy fails closed when an artifact output is missing', () => { assert.match(workflow, /actions\/upload-artifact@[0-9a-f]{40}[\s\S]*if-no-files-found:\s*error/iu); }); +test('release workflow uses the protected release environment', () => { + const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows/release.yml'), 'utf8'); + assert.match(workflow, /^\s+environment:\s*release\s*$/mu); +}); + test('CI policy rejects floating actions and pull request target execution', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'databreeze-ci-policy-')); fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true }); From f0251d4182caa815ea774e2651ed5f3c0b8c0f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:26:14 +0700 Subject: [PATCH 70/89] fix(infra): protect hosted web bucket deletion --- infrastructure/aws/modules/web/main.tf | 5 +++-- tools/repo-cli/src/check-aws-infrastructure.mjs | 1 + tools/repo-cli/test/aws-infrastructure.test.mjs | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/infrastructure/aws/modules/web/main.tf b/infrastructure/aws/modules/web/main.tf index 8a8e80f9..6937f755 100644 --- a/infrastructure/aws/modules/web/main.tf +++ b/infrastructure/aws/modules/web/main.tf @@ -3,8 +3,9 @@ locals { } resource "aws_s3_bucket" "web" { - bucket = "databreeze-${var.name}-web" - tags = merge(local.common_tags, { Name = "databreeze-${var.name}-web" }) + bucket = "databreeze-${var.name}-web" + force_destroy = false + tags = merge(local.common_tags, { Name = "databreeze-${var.name}-web" }) } resource "aws_s3_bucket_ownership_controls" "web" { diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 33d6bef9..97ee4821 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -53,6 +53,7 @@ for (const requiredBoundary of [ 'backup_retention_period', 'token.actions.githubusercontent.com:sub', 'recovery_window_in_days = 30', + 'force_destroy = false', ]) { if (!allTerraform.includes(requiredBoundary)) fail(`missing required safety boundary ${requiredBoundary}`); } diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 8f42d243..404300cb 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -73,6 +73,7 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s assert.match(sources, /aws_s3_bucket_lifecycle_configuration/u); assert.match(sources, /noncurrent_version_expiration/u); assert.match(sources, /abort_incomplete_multipart_upload/u); + assert.match(sources, /force_destroy\s*=\s*false/u); assert.doesNotMatch(sources, /aws_iam_role_policy" task/u); assert.doesNotMatch(sources, /ecs-task-minimal/u); }); From 33a6398c75500c984c7bea5330846644f80c3e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:26:29 +0700 Subject: [PATCH 71/89] docs(orchestration): finalize foundation batch checkpoint --- docs/operations/foundation-batch-handoff-2026-08-02.md | 2 +- docs/plans/execution-orchestration.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/operations/foundation-batch-handoff-2026-08-02.md b/docs/operations/foundation-batch-handoff-2026-08-02.md index 6b93b8c8..6dcc524a 100644 --- a/docs/operations/foundation-batch-handoff-2026-08-02.md +++ b/docs/operations/foundation-batch-handoff-2026-08-02.md @@ -36,7 +36,7 @@ PR until the branch reaches at least 30 commits, targeting approximately 70 and never reaching 100. The only small-PR exceptions are focused promotion-review fixes required to close an already-open `dev`→`main` gate. -At this checkpoint the branch is 62 commits ahead of `origin/dev`. The current +At this checkpoint the branch is 70 commits ahead of `origin/dev`. The current boundary is still coherent: FND-003 local lifecycle hardening is accompanied by portable AWS safety, telemetry, CI/supply-chain, and evidence updates. Continue with scoped foundation work until the final handoff boundary; do not manufacture diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index a3aff955..e771e810 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -80,7 +80,7 @@ }, "FND-003": { "status": "in-progress", - "commit": "039de6b", + "commit": "f0251d4", "evidence": [ "infrastructure/local/compose.yml", "infrastructure/local/README.md", From d9598a2e2ed6055ab81261eb94fd539ba5cfd934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:30:32 +0700 Subject: [PATCH 72/89] fix(repo): satisfy formatting and lint gates --- packages/telemetry/src/v1.ts | 12 +- .../repo-cli/src/check-aws-infrastructure.mjs | 3 +- tools/repo-cli/src/check-ci-policy.mjs | 5 +- tools/repo-cli/src/generate-provenance.mjs | 17 ++- tools/repo-cli/src/local-services.mjs | 104 ++++++++++++++---- tools/repo-cli/test/ci-policy.test.mjs | 15 ++- .../test/local-infrastructure.test.mjs | 41 +++++-- tools/repo-cli/test/provenance.test.mjs | 8 +- 8 files changed, 156 insertions(+), 49 deletions(-) diff --git a/packages/telemetry/src/v1.ts b/packages/telemetry/src/v1.ts index 6af5f785..df8d1b7e 100644 --- a/packages/telemetry/src/v1.ts +++ b/packages/telemetry/src/v1.ts @@ -198,7 +198,11 @@ export function assertSafeTelemetryAttributesV1( throw new UnsafeTelemetryAttributeErrorV1(key); } const descriptor = Object.getOwnPropertyDescriptor(input, key); - if (!descriptor || !('value' in descriptor) || safeScalar(key, descriptor.value) === undefined) { + if ( + !descriptor || + !('value' in descriptor) || + safeScalar(key, descriptor.value) === undefined + ) { throw new UnsafeTelemetryAttributeErrorV1(key); } } @@ -261,7 +265,8 @@ function readSingleHeader( const values: string[] = []; for (const key of Object.keys(headers)) { const descriptor = Object.getOwnPropertyDescriptor(headers, key); - if (!descriptor || !('value' in descriptor)) throw new Error(`Unreadable telemetry ${name} header`); + if (!descriptor || !('value' in descriptor)) + throw new Error(`Unreadable telemetry ${name} header`); const value = descriptor.value as string | string[] | undefined; if (key.toLowerCase() !== name) continue; if (Array.isArray(value)) { @@ -269,8 +274,7 @@ function readSingleHeader( throw new Error(`Unreadable telemetry ${name} header`); } values.push(...value); - } - else if (value !== undefined) values.push(value); + } else if (value !== undefined) values.push(value); } if (values.length > 1) throw new Error(`Ambiguous telemetry ${name} header`); if (values.length === 0) return undefined; diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 97ee4821..82fe4f30 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -55,7 +55,8 @@ for (const requiredBoundary of [ 'recovery_window_in_days = 30', 'force_destroy = false', ]) { - if (!allTerraform.includes(requiredBoundary)) fail(`missing required safety boundary ${requiredBoundary}`); + if (!allTerraform.includes(requiredBoundary)) + fail(`missing required safety boundary ${requiredBoundary}`); } if (/ingress[\s\S]{0,400}cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u.test(allTerraform)) { fail('a private service security group permits unrestricted ingress'); diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index 4d1be5d7..46e88660 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -53,7 +53,10 @@ function assertBoundedJobs(text, filename) { } function assertArtifactOutputs(text, filename) { - const artifactSteps = text.match(/-\s+uses:\s+actions\/upload-artifact@[^\n]+[\s\S]*?(?=\n\s+-\s+name:|\n\s+\w+:\s*$|$)/gim) ?? []; + const artifactSteps = + text.match( + /-\s+uses:\s+actions\/upload-artifact@[^\n]+[\s\S]*?(?=\n\s+-\s+name:|\n\s+\w+:\s*$|$)/gim, + ) ?? []; for (const step of artifactSteps) { if (!/if-no-files-found:\s*error/iu.test(step)) { throw new Error(`${filename} artifact uploads must fail when an output is missing`); diff --git a/tools/repo-cli/src/generate-provenance.mjs b/tools/repo-cli/src/generate-provenance.mjs index 46aa86b8..ec8eadbf 100644 --- a/tools/repo-cli/src/generate-provenance.mjs +++ b/tools/repo-cli/src/generate-provenance.mjs @@ -20,15 +20,14 @@ const missingArtifacts = artifactArguments.filter((file) => !fs.existsSync(file) if (missingArtifacts.length > 0) { throw new Error(`Provenance artifact(s) do not exist: ${missingArtifacts.join(', ')}`); } -const artifacts = artifactArguments - .map((file) => { - const bytes = fs.readFileSync(file); - return { - path: path.relative(process.cwd(), file).replaceAll('\\', '/'), - sha256: crypto.createHash('sha256').update(bytes).digest('hex'), - size: bytes.length, - }; - }); +const artifacts = artifactArguments.map((file) => { + const bytes = fs.readFileSync(file); + return { + path: path.relative(process.cwd(), file).replaceAll('\\', '/'), + sha256: crypto.createHash('sha256').update(bytes).digest('hex'), + size: bytes.length, + }; +}); artifacts.sort((a, b) => a.path.localeCompare(b.path)); const epoch = Number(process.env.SOURCE_DATE_EPOCH || 0); const provenance = { diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 9c6d53cc..6da592b8 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -81,15 +81,19 @@ function environment() { fileValues.set('COMPOSE_PROJECT_NAME', process.env.COMPOSE_PROJECT_NAME); } for (const definition of hostPorts) { - if (process.env[definition.key] !== undefined) fileValues.set(definition.key, process.env[definition.key]); + if (process.env[definition.key] !== undefined) + fileValues.set(definition.key, process.env[definition.key]); } return fileValues; } function projectName(values) { - const value = values.get('COMPOSE_PROJECT_NAME') || process.env.COMPOSE_PROJECT_NAME || 'databreeze-local'; + const value = + values.get('COMPOSE_PROJECT_NAME') || process.env.COMPOSE_PROJECT_NAME || 'databreeze-local'; if (!/^[a-z0-9][a-z0-9_-]{0,62}$/u.test(value)) { - fail('COMPOSE_PROJECT_NAME must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, or underscores'); + fail( + 'COMPOSE_PROJECT_NAME must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, or underscores', + ); } return value; } @@ -112,7 +116,9 @@ function runDocker(args, { allowFailure = false } = {}) { const result = spawnSync('docker', args, { cwd: repositoryRoot, encoding: 'utf8' }); if (!allowFailure && (result.error || result.status !== 0)) { if (result.error?.code === 'ENOENT') { - fail('Docker CLI is not installed or not on PATH; start Docker Desktop before using this command'); + fail( + 'Docker CLI is not installed or not on PATH; start Docker Desktop before using this command', + ); } const detail = (result.stderr || result.stdout || result.error?.message || '').trim(); fail(`docker ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); @@ -126,10 +132,14 @@ function requireDocker() { encoding: 'utf8', }); if (result.error?.code === 'ENOENT') { - fail('Docker CLI is not installed or not on PATH; start Docker Desktop before using this command'); + fail( + 'Docker CLI is not installed or not on PATH; start Docker Desktop before using this command', + ); } if (result.status !== 0) { - fail('Docker daemon is unavailable; start Docker Desktop or another Docker Engine before using this command'); + fail( + 'Docker daemon is unavailable; start Docker Desktop or another Docker Engine before using this command', + ); } } @@ -146,14 +156,20 @@ function ensureDiskSpace(minFreeGib) { const freeBytes = Number(stats.bavail) * Number(stats.bsize); const requiredBytes = minFreeGib * 1024 ** 3; if (freeBytes < requiredBytes) { - fail(`host free space is ${(freeBytes / 1024 ** 3).toFixed(2)} GiB; at least ${minFreeGib} GiB is required`); + fail( + `host free space is ${(freeBytes / 1024 ** 3).toFixed(2)} GiB; at least ${minFreeGib} GiB is required`, + ); } } function containerRunning(service, values) { - const id = runDocker([...composeArgs(values), 'ps', '-q', service], { allowFailure: true }).stdout.trim(); + const id = runDocker([...composeArgs(values), 'ps', '-q', service], { + allowFailure: true, + }).stdout.trim(); if (!id) return false; - const state = runDocker(['inspect', '--format', '{{.State.Running}}', id], { allowFailure: true }); + const state = runDocker(['inspect', '--format', '{{.State.Running}}', id], { + allowFailure: true, + }); return state.stdout.trim() === 'true'; } @@ -182,10 +198,13 @@ async function ensurePorts(values) { } configured.set(port, definition); if (containerRunning(definition.service, values)) continue; - if (!(await portAvailable(port))) collisions.push(`${definition.key}=${port} (${definition.service})`); + if (!(await portAvailable(port))) + collisions.push(`${definition.key}=${port} (${definition.service})`); } if (collisions.length > 0) { - fail(`host ports are already in use: ${collisions.join(', ')}; set alternate ports in infrastructure/local/.env`); + fail( + `host ports are already in use: ${collisions.join(', ')}; set alternate ports in infrastructure/local/.env`, + ); } } @@ -208,7 +227,9 @@ async function waitForReady(values, waitSeconds) { let last = new Map(); while (Date.now() <= deadline) { last = new Map(services.map((service) => [service, inspectHealth(service, values)])); - if ([...last.values()].every(({ state, health }) => state === 'running' && health === 'healthy')) { + if ( + [...last.values()].every(({ state, health }) => state === 'running' && health === 'healthy') + ) { console.log(`Local services ready (${services.join(', ')}).`); return; } @@ -217,14 +238,16 @@ async function waitForReady(values, waitSeconds) { await delay(1000); } console.error('\nLocal services did not become ready:'); - for (const service of services) console.error(`- ${service}: ${last.get(service)?.detail ?? 'unknown'}`); + for (const service of services) + console.error(`- ${service}: ${last.get(service)?.detail ?? 'unknown'}`); fail(`readiness timeout after ${waitSeconds}s`); } function parseArguments(argv) { let command = 'smoke'; const argumentsToParse = [...argv]; - if (argumentsToParse[0] && !argumentsToParse[0].startsWith('-')) command = argumentsToParse.shift(); + if (argumentsToParse[0] && !argumentsToParse[0].startsWith('-')) + command = argumentsToParse.shift(); const options = { start: false, waitSeconds: 60, @@ -256,7 +279,11 @@ function parseArguments(argv) { } fail(`unknown argument: ${argument}`); } - if (!Number.isInteger(options.waitSeconds) || options.waitSeconds < 1 || options.waitSeconds > 3600) { + if ( + !Number.isInteger(options.waitSeconds) || + options.waitSeconds < 1 || + options.waitSeconds > 3600 + ) { fail('--wait-seconds must be an integer from 1 to 3600'); } if (!Number.isInteger(options.tail) || options.tail < 1 || options.tail > 1000) { @@ -268,7 +295,21 @@ function parseArguments(argv) { if (!Number.isFinite(options.minFreeGib) || options.minFreeGib < 0) { fail('--min-free-gib must be a non-negative number'); } - if (!['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'persistence-check', 'status', 'logs', 'smoke'].includes(command)) { + if ( + ![ + 'config', + 'preflight', + 'check', + 'start', + 'stop', + 'reset', + 'restart-check', + 'persistence-check', + 'status', + 'logs', + 'smoke', + ].includes(command) + ) { fail(`unknown command: ${command}`); } return { command, options }; @@ -297,12 +338,19 @@ export async function main(argv = process.argv.slice(2)) { validateCompose(values); if (command === 'status') { - for (const service of services) console.log(`${service}: ${inspectHealth(service, values).detail}`); + for (const service of services) + console.log(`${service}: ${inspectHealth(service, values).detail}`); return; } if (command === 'logs') { const selected = options.service ? [options.service] : logServices; - runDocker([...composeArgs(values), 'logs', '--no-color', `--tail=${options.tail}`, ...selected]); + runDocker([ + ...composeArgs(values), + 'logs', + '--no-color', + `--tail=${options.tail}`, + ...selected, + ]); return; } if (command === 'stop') { @@ -311,7 +359,8 @@ export async function main(argv = process.argv.slice(2)) { return; } - const shouldStart = command === 'start' || command === 'reset' || (command === 'smoke' && options.start); + const shouldStart = + command === 'start' || command === 'reset' || (command === 'smoke' && options.start); if (shouldStart) { ensureDiskSpace(options.minFreeGib); await ensurePorts(values); @@ -332,7 +381,9 @@ export async function main(argv = process.argv.slice(2)) { if (command === 'restart-check') { runDocker([...composeArgs(values), 'restart']); await waitForReady(values, options.waitSeconds); - console.log('Local service restart and health checks passed. Use persistence-check for a Redis sentinel probe.'); + console.log( + 'Local service restart and health checks passed. Use persistence-check for a Redis sentinel probe.', + ); return; } if (command === 'persistence-check') { @@ -352,8 +403,17 @@ export async function main(argv = process.argv.slice(2)) { ]); runDocker([...composeArgs(values), 'restart', 'redis']); await waitForReady(values, options.waitSeconds); - const result = runDocker([...composeArgs(values), 'exec', '-T', 'redis', 'redis-cli', 'GET', key]); - if (result.stdout.trim() !== value) fail('Redis persistence sentinel was not recovered after restart'); + const result = runDocker([ + ...composeArgs(values), + 'exec', + '-T', + 'redis', + 'redis-cli', + 'GET', + key, + ]); + if (result.stdout.trim() !== value) + fail('Redis persistence sentinel was not recovered after restart'); runDocker([...composeArgs(values), 'exec', '-T', 'redis', 'redis-cli', 'DEL', key]); console.log('Local Redis persistence check passed; sentinel was removed.'); return; diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index b63e051a..526347c1 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -19,12 +19,21 @@ test('CI policy requires checkout credentials to be discarded', () => { }); test('CI policy fails closed when an artifact output is missing', () => { - const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows/release.yml'), 'utf8'); - assert.match(workflow, /actions\/upload-artifact@[0-9a-f]{40}[\s\S]*if-no-files-found:\s*error/iu); + const workflow = fs.readFileSync( + path.join(process.cwd(), '.github/workflows/release.yml'), + 'utf8', + ); + assert.match( + workflow, + /actions\/upload-artifact@[0-9a-f]{40}[\s\S]*if-no-files-found:\s*error/iu, + ); }); test('release workflow uses the protected release environment', () => { - const workflow = fs.readFileSync(path.join(process.cwd(), '.github/workflows/release.yml'), 'utf8'); + const workflow = fs.readFileSync( + path.join(process.cwd(), '.github/workflows/release.yml'), + 'utf8', + ); assert.match(workflow, /^\s+environment:\s*release\s*$/mu); }); diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index ba941635..0291e8ba 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -33,7 +33,7 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, new RegExp(`^ ${volume}:`, 'm')); } assert.equal((compose.match(/healthcheck:/g) ?? []).length, 6); - assert.equal((compose.match(/^ init: true$/gmu) ?? []).length, 7); + assert.equal((compose.match(/^\s{4}init: true$/gmu) ?? []).length, 7); assert.match(compose, /minio-init:[\s\S]*depends_on:[\s\S]*condition: service_healthy/u); assert.match(compose, /minio-init:[\s\S]*restart: 'no'/u); assert.match(compose, /postgres-data:[\s\S]*name: \$\{COMPOSE_PROJECT_NAME/u); @@ -95,7 +95,10 @@ test('local OpenTelemetry collector keeps every signal on the bounded local pipe for (const signal of ['traces:', 'metrics:', 'logs:']) { assert.match(collector, new RegExp(`^ ${signal}`, 'm')); assert.match(collector, new RegExp(`${signal}[\\s\\S]*receivers: \\[otlp\\]`, 'u')); - assert.match(collector, new RegExp(`${signal}[\\s\\S]*processors: \\[memory_limiter, batch\\]`, 'u')); + assert.match( + collector, + new RegExp(`${signal}[\\s\\S]*processors: \\[memory_limiter, batch\\]`, 'u'), + ); assert.match(collector, new RegExp(`${signal}[\\s\\S]*exporters: \\[debug\\]`, 'u')); } assert.match(collector, /health_check:[\s\S]*endpoint: 0\.0\.0\.0:13133/u); @@ -127,7 +130,19 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); - for (const command of ['config', 'preflight', 'check', 'start', 'stop', 'reset', 'restart-check', 'persistence-check', 'status', 'logs', 'smoke']) { + for (const command of [ + 'config', + 'preflight', + 'check', + 'start', + 'stop', + 'reset', + 'restart-check', + 'persistence-check', + 'status', + 'logs', + 'smoke', + ]) { assert.match(result.stdout, new RegExp(`^ ${command}\\s`, 'm')); } assert.match(script, /statfsSync/u); @@ -145,14 +160,20 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum encoding: 'utf8', }); assert.notEqual(invalidTimeout.status, 0); - assert.match(`${invalidTimeout.stdout}\n${invalidTimeout.stderr}`, /--wait-seconds must be an integer/u); + assert.match( + `${invalidTimeout.stdout}\n${invalidTimeout.stderr}`, + /--wait-seconds must be an integer/u, + ); const invalidDisk = spawnSync(process.execPath, [helpScript, 'check', '--min-free-gib=-1'], { cwd: repositoryRoot, encoding: 'utf8', }); assert.notEqual(invalidDisk.status, 0); - assert.match(`${invalidDisk.stdout}\n${invalidDisk.stderr}`, /--min-free-gib must be a non-negative number/u); + assert.match( + `${invalidDisk.stdout}\n${invalidDisk.stderr}`, + /--min-free-gib must be a non-negative number/u, + ); const invalidTail = spawnSync(process.execPath, [helpScript, 'logs', '--tail=0'], { cwd: repositoryRoot, @@ -174,7 +195,10 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum env: { ...process.env, COMPOSE_PROJECT_NAME: '../unsafe-project' }, }); assert.notEqual(invalidProject.status, 0); - assert.match(`${invalidProject.stdout}\n${invalidProject.stderr}`, /COMPOSE_PROJECT_NAME must start/u); + assert.match( + `${invalidProject.stdout}\n${invalidProject.stderr}`, + /COMPOSE_PROJECT_NAME must start/u, + ); const invalidEnvironmentDisk = spawnSync(process.execPath, [helpScript, 'preflight'], { cwd: repositoryRoot, @@ -191,8 +215,9 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum cwd: repositoryRoot, encoding: 'utf8', }); - if (composeConfig.status === 0) assert.match(composeConfig.stdout, /Compose configuration is valid/u); - assert.match(script, /logs', '--no-color/u); + if (composeConfig.status === 0) + assert.match(composeConfig.stdout, /Compose configuration is valid/u); + assert.match(script, /logs[\s\S]*--no-color/u); assert.match(script, /--service must name one of/u); assert.match(script, /--tail must be an integer/u); assert.match(script, /COMPOSE_PROJECT_NAME must start/u); diff --git a/tools/repo-cli/test/provenance.test.mjs b/tools/repo-cli/test/provenance.test.mjs index 40a50201..0f4fbe89 100644 --- a/tools/repo-cli/test/provenance.test.mjs +++ b/tools/repo-cli/test/provenance.test.mjs @@ -37,7 +37,13 @@ test('provenance generation fails closed when an artifact is missing', () => { try { const result = spawnSync( process.execPath, - [script, '--output', path.join(directory, 'provenance.json'), '--artifact', path.join(directory, 'missing.json')], + [ + script, + '--output', + path.join(directory, 'provenance.json'), + '--artifact', + path.join(directory, 'missing.json'), + ], { cwd: repositoryRoot, encoding: 'utf8' }, ); assert.notEqual(result.status, 0); From 214e45b7e14b0eb10fef851c82a61871fe38bbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:35:03 +0700 Subject: [PATCH 73/89] docs(operations): record foundation verification boundary --- .../foundation-batch-handoff-2026-08-02.md | 24 ++++++++++++++++++- docs/plans/execution-orchestration.json | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/operations/foundation-batch-handoff-2026-08-02.md b/docs/operations/foundation-batch-handoff-2026-08-02.md index 6dcc524a..13946ceb 100644 --- a/docs/operations/foundation-batch-handoff-2026-08-02.md +++ b/docs/operations/foundation-batch-handoff-2026-08-02.md @@ -36,12 +36,34 @@ PR until the branch reaches at least 30 commits, targeting approximately 70 and never reaching 100. The only small-PR exceptions are focused promotion-review fixes required to close an already-open `dev`→`main` gate. -At this checkpoint the branch is 70 commits ahead of `origin/dev`. The current +At this checkpoint the branch is 72 commits ahead of `origin/dev`. The current boundary is still coherent: FND-003 local lifecycle hardening is accompanied by portable AWS safety, telemetry, CI/supply-chain, and evidence updates. Continue with scoped foundation work until the final handoff boundary; do not manufacture empty commits or open a small feature PR merely to reset the count. +## Final scoped verification before the feature PR + +Passed from this worktree: + +- `corepack pnpm format:check` +- `corepack pnpm lint` +- `corepack pnpm typecheck` +- `corepack pnpm contracts:check` +- `corepack pnpm orchestration:check` +- `corepack pnpm requirements:check` +- `corepack pnpm test` (67 repository CLI tests plus all workspace suites) +- `corepack pnpm repo:build` (API, Web, Desktop, shared packages, and engine) +- `uv run --locked pytest`, Ruff, format, and mypy (91 engine tests) +- `apps/android/gradlew.bat :app:testDebugUnitTest --offline --no-daemon` + +Environment-gated and intentionally not claimed as verified: + +- OpenTofu format/init/validate because OpenTofu is not installed locally. +- Live Docker startup, health, port-collision, disk-pressure, Redis + persistence, and restart checks because the Docker daemon is unavailable. +- Android instrumentation/emulator and signed release packaging. + ## Safest next command ```powershell diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index e771e810..84f853b2 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -80,7 +80,7 @@ }, "FND-003": { "status": "in-progress", - "commit": "f0251d4", + "commit": "d9598a2", "evidence": [ "infrastructure/local/compose.yml", "infrastructure/local/README.md", From 2b79e49694ed03f6ac0caf90492b8c2455469e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:50:13 +0700 Subject: [PATCH 74/89] test(cli): verify generated artifact contents --- tools/repo-cli/test/provenance.test.mjs | 7 +++++++ tools/repo-cli/test/sbom.test.mjs | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/repo-cli/test/provenance.test.mjs b/tools/repo-cli/test/provenance.test.mjs index 0f4fbe89..70ebec9c 100644 --- a/tools/repo-cli/test/provenance.test.mjs +++ b/tools/repo-cli/test/provenance.test.mjs @@ -27,6 +27,13 @@ test('provenance generation records sorted artifact digests', () => { provenance.subject.map((subject) => path.posix.basename(subject.path)), ['a-output.json', 'z-output.json'], ); + assert.deepEqual( + provenance.subject.map((subject) => subject.sha256), + [ + 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', + '594e519ae499312b29433b7dd8a97ff068defcba9755b6d5d00e84c524d67b06', + ], + ); } finally { rmSync(directory, { recursive: true, force: true }); } diff --git a/tools/repo-cli/test/sbom.test.mjs b/tools/repo-cli/test/sbom.test.mjs index 4d0159a4..72ec7d73 100644 --- a/tools/repo-cli/test/sbom.test.mjs +++ b/tools/repo-cli/test/sbom.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; @@ -27,6 +27,12 @@ test('SBOM generation writes to an explicit output path', () => { }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Wrote .*sbom\.json/u); + assert.equal(existsSync(output), true); + const sbom = JSON.parse(readFileSync(output, 'utf8')); + assert.equal(sbom.bomFormat, 'CycloneDX'); + assert.equal(sbom.specVersion, '1.5'); + assert.ok(Array.isArray(sbom.components)); + assert.ok(sbom.components.some((component) => component.name === '@databreeze/platform')); } finally { rmSync(directory, { recursive: true, force: true }); } From f263be7867f8cb50f8e8b1bf47d287520cc7883b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:50:36 +0700 Subject: [PATCH 75/89] fix(local): bind published services to loopback --- infrastructure/local/README.md | 2 ++ infrastructure/local/compose.yml | 18 +++++++++--------- .../test/local-infrastructure.test.mjs | 2 ++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md index ecb9add1..a94eee8e 100644 --- a/infrastructure/local/README.md +++ b/infrastructure/local/README.md @@ -18,6 +18,8 @@ named volumes prefixed by the Compose project name; no repository directory is mounted for database, object, or mail data. The volumes are disposable and are not removed by the lifecycle commands. Remove the named volumes only when you explicitly want to discard local state. +Every published port is bound to `127.0.0.1`, so the development credentials and +data endpoints are not reachable from other hosts on the local network. Container JSON logs are capped at 10 MiB per file with three retained files so diagnostics cannot silently consume the host disk. diff --git a/infrastructure/local/compose.yml b/infrastructure/local/compose.yml index f4c9f170..85fd97de 100644 --- a/infrastructure/local/compose.yml +++ b/infrastructure/local/compose.yml @@ -15,7 +15,7 @@ services: POSTGRES_USER: ${POSTGRES_USER:-databreeze} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-databreeze-local-change-me} ports: - - '${POSTGRES_PORT:-5432}:5432' + - '127.0.0.1:${POSTGRES_PORT:-5432}:5432' networks: [local] logging: *default-logging volumes: @@ -36,7 +36,7 @@ services: init: true command: ['redis-server', '--appendonly', 'yes'] ports: - - '${REDIS_PORT:-6379}:6379' + - '127.0.0.1:${REDIS_PORT:-6379}:6379' networks: [local] logging: *default-logging volumes: @@ -56,8 +56,8 @@ services: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-databreeze} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-databreeze-local-change-me} ports: - - '${MINIO_API_PORT:-9000}:9000' - - '${MINIO_CONSOLE_PORT:-9001}:9001' + - '127.0.0.1:${MINIO_API_PORT:-9000}:9000' + - '127.0.0.1:${MINIO_CONSOLE_PORT:-9001}:9001' networks: [local] logging: *default-logging volumes: @@ -95,8 +95,8 @@ services: MP_MAX_MESSAGES: ${MAILPIT_MAX_MESSAGES:-5000} MP_DATABASE: /data/mailpit.db ports: - - '${MAILPIT_SMTP_PORT:-1025}:1025' - - '${MAILPIT_UI_PORT:-8025}:8025' + - '127.0.0.1:${MAILPIT_SMTP_PORT:-1025}:1025' + - '127.0.0.1:${MAILPIT_UI_PORT:-8025}:8025' networks: [local] logging: *default-logging volumes: @@ -113,9 +113,9 @@ services: init: true command: ['--config=/etc/otelcol-contrib/config.yaml'] ports: - - '${OTEL_GRPC_PORT:-4317}:4317' - - '${OTEL_HTTP_PORT:-4318}:4318' - - '${OTEL_HEALTH_PORT:-13133}:13133' + - '127.0.0.1:${OTEL_GRPC_PORT:-4317}:4317' + - '127.0.0.1:${OTEL_HTTP_PORT:-4318}:4318' + - '127.0.0.1:${OTEL_HEALTH_PORT:-13133}:13133' networks: [local] logging: *default-logging volumes: diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 0291e8ba..2881d85e 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -43,6 +43,8 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, /max-size: 10m/u); assert.match(compose, /max-file: '3'/u); assert.equal((compose.match(/logging: \*default-logging/g) ?? []).length, 7); + assert.equal((compose.match(/127\.0\.0\.1:\$\{/g) ?? []).length, 9); + assert.match(read('infrastructure/local/README.md'), /Every published port is bound to `127\.0\.0\.1`/u); }); test('local bootstrap is credential-free and creates every owned module schema', () => { From 901d76f12b9f4b963c4f16d4e55fd3760cc610a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:52:11 +0700 Subject: [PATCH 76/89] fix(local): bound Docker probes and cleanup sentinels --- tools/repo-cli/src/local-services.mjs | 102 ++++++++++++++++---------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 6da592b8..5323e021 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -68,8 +68,8 @@ function parseEnvFile(file) { if (!existsSync(file)) return new Map(); const values = new Map(); for (const line of readFileSync(file, 'utf8').split(/\r?\n/u)) { - const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)\s*$/u.exec(line); - if (!match || match[1].startsWith('#')) continue; + const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/u.exec(line); + if (!match) continue; values.set(match[1], match[2].replace(/^(['"])(.*)\1$/u, '$2')); } return values; @@ -84,6 +84,9 @@ function environment() { if (process.env[definition.key] !== undefined) fileValues.set(definition.key, process.env[definition.key]); } + if (process.env.DATABREEZE_MIN_FREE_GIB !== undefined) { + fileValues.set('DATABREEZE_MIN_FREE_GIB', process.env.DATABREEZE_MIN_FREE_GIB); + } return fileValues; } @@ -112,14 +115,23 @@ function composeArgs(values = environment()) { return ['compose', '--project-name', project, '--env-file', envFile, '-f', composeFile]; } -function runDocker(args, { allowFailure = false } = {}) { - const result = spawnSync('docker', args, { cwd: repositoryRoot, encoding: 'utf8' }); +function runDocker(args, { allowFailure = false, capture = true, timeoutMs = 30_000 } = {}) { + const result = spawnSync( + 'docker', + args, + capture + ? { cwd: repositoryRoot, encoding: 'utf8', timeout: timeoutMs } + : { cwd: repositoryRoot, stdio: 'inherit', timeout: timeoutMs }, + ); if (!allowFailure && (result.error || result.status !== 0)) { if (result.error?.code === 'ENOENT') { fail( 'Docker CLI is not installed or not on PATH; start Docker Desktop before using this command', ); } + if (result.error?.code === 'ETIMEDOUT') { + fail(`docker ${args.join(' ')} timed out after ${timeoutMs}ms`); + } const detail = (result.stderr || result.stdout || result.error?.message || '').trim(); fail(`docker ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); } @@ -130,12 +142,16 @@ function requireDocker() { const result = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { cwd: repositoryRoot, encoding: 'utf8', + timeout: 15_000, }); if (result.error?.code === 'ENOENT') { fail( 'Docker CLI is not installed or not on PATH; start Docker Desktop before using this command', ); } + if (result.error?.code === 'ETIMEDOUT') { + fail('Docker CLI check timed out after 15000ms; verify Docker Desktop is responsive'); + } if (result.status !== 0) { fail( 'Docker daemon is unavailable; start Docker Desktop or another Docker Engine before using this command', @@ -217,8 +233,12 @@ function inspectHealth(service, values) { '--format', '{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health{{end}}', id, - ]); - const [state, health] = inspect.stdout.trim().split('|'); + ], { allowFailure: true }); + const inspection = inspect.stdout?.trim(); + if (inspect.error || inspect.status !== 0 || !inspection) { + return { state: 'unknown', health: 'unknown', detail: 'unknown/unknown (inspect unavailable)' }; + } + const [state, health] = inspection.split('|'); return { state, health, detail: `${state}/${health}` }; } @@ -243,7 +263,7 @@ async function waitForReady(values, waitSeconds) { fail(`readiness timeout after ${waitSeconds}s`); } -function parseArguments(argv) { +function parseArguments(argv, values = environment()) { let command = 'smoke'; const argumentsToParse = [...argv]; if (argumentsToParse[0] && !argumentsToParse[0].startsWith('-')) @@ -253,7 +273,7 @@ function parseArguments(argv) { waitSeconds: 60, tail: 100, service: undefined, - minFreeGib: Number(process.env.DATABREEZE_MIN_FREE_GIB || 5), + minFreeGib: Number(values.get('DATABREEZE_MIN_FREE_GIB') ?? 5), }; for (const argument of argumentsToParse) { if (argument === '--help' || argument === '-h') return { command: 'help', options }; @@ -316,12 +336,12 @@ function parseArguments(argv) { } export async function main(argv = process.argv.slice(2)) { - const { command, options } = parseArguments(argv); + const values = environment(); + const { command, options } = parseArguments(argv, values); if (command === 'help') { usage(); return; } - const values = environment(); if (command === 'config') { validateCompose(values); console.log('Local Compose configuration is valid.'); @@ -350,7 +370,7 @@ export async function main(argv = process.argv.slice(2)) { '--no-color', `--tail=${options.tail}`, ...selected, - ]); + ], { capture: false, timeoutMs: 120_000 }); return; } if (command === 'stop') { @@ -389,32 +409,40 @@ export async function main(argv = process.argv.slice(2)) { if (command === 'persistence-check') { const key = `databreeze:local:persistence-check:${process.pid}`; const value = `${Date.now()}`; - runDocker([ - ...composeArgs(values), - 'exec', - '-T', - 'redis', - 'redis-cli', - 'SET', - key, - value, - 'EX', - '300', - ]); - runDocker([...composeArgs(values), 'restart', 'redis']); - await waitForReady(values, options.waitSeconds); - const result = runDocker([ - ...composeArgs(values), - 'exec', - '-T', - 'redis', - 'redis-cli', - 'GET', - key, - ]); - if (result.stdout.trim() !== value) - fail('Redis persistence sentinel was not recovered after restart'); - runDocker([...composeArgs(values), 'exec', '-T', 'redis', 'redis-cli', 'DEL', key]); + let recovered = false; + try { + runDocker([ + ...composeArgs(values), + 'exec', + '-T', + 'redis', + 'redis-cli', + 'SET', + key, + value, + 'EX', + '300', + ]); + runDocker([...composeArgs(values), 'restart', 'redis']); + await waitForReady(values, options.waitSeconds); + const result = runDocker([ + ...composeArgs(values), + 'exec', + '-T', + 'redis', + 'redis-cli', + 'GET', + key, + ]); + if (result.stdout.trim() !== value) + fail('Redis persistence sentinel was not recovered after restart'); + recovered = true; + } finally { + runDocker([...composeArgs(values), 'exec', '-T', 'redis', 'redis-cli', 'DEL', key], { + allowFailure: true, + }); + } + if (!recovered) return; console.log('Local Redis persistence check passed; sentinel was removed.'); return; } From 0d5635b5e1216d9f294d4efafe9bb43934f1199b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:53:47 +0700 Subject: [PATCH 77/89] fix(telemetry): normalize hostile reflection failures --- packages/telemetry/src/v1.ts | 75 ++++++++++++++----- packages/telemetry/test/telemetry-v1.test.mjs | 30 ++++++++ .../engine/src/databreeze_engine/telemetry.py | 22 +++--- services/engine/tests/test_telemetry.py | 19 +++++ 4 files changed, 119 insertions(+), 27 deletions(-) diff --git a/packages/telemetry/src/v1.ts b/packages/telemetry/src/v1.ts index df8d1b7e..e74ea2e2 100644 --- a/packages/telemetry/src/v1.ts +++ b/packages/telemetry/src/v1.ts @@ -166,13 +166,25 @@ function safeScalar(key: string, value: unknown): TelemetryScalarV1 | undefined return safeString(key, value); } -function ownDataEntries(input: Record): Array<[string, unknown]> { +function readOwnDataEntries(input: Record): { + entries: Array<[string, unknown]>; + readable: boolean; +} { const entries: Array<[string, unknown]> = []; - for (const key of Object.keys(input)) { - const descriptor = Object.getOwnPropertyDescriptor(input, key); - if (descriptor && 'value' in descriptor) entries.push([key, descriptor.value]); + try { + for (const key of Object.keys(input)) { + const descriptor = Object.getOwnPropertyDescriptor(input, key); + if (!descriptor || !('value' in descriptor)) return { entries: [], readable: false }; + entries.push([key, descriptor.value]); + } + } catch { + return { entries: [], readable: false }; } - return entries; + return { entries, readable: true }; +} + +function ownDataEntries(input: Record): Array<[string, unknown]> { + return readOwnDataEntries(input).entries; } export function sanitizeTelemetryAttributesV1( @@ -192,17 +204,14 @@ export function sanitizeTelemetryAttributesV1( export function assertSafeTelemetryAttributesV1( input: Record, ): asserts input is SafeTelemetryAttributesV1 { - for (const key of Object.keys(input ?? {})) { + const readable = readOwnDataEntries(input ?? {}); + if (!readable.readable) throw new UnsafeTelemetryAttributeErrorV1('unreadable'); + for (const [key, value] of readable.entries) { assertBoundedKey(key); if (forbiddenKeyPattern.test(key) || !safeAttributeSet.has(key)) { throw new UnsafeTelemetryAttributeErrorV1(key); } - const descriptor = Object.getOwnPropertyDescriptor(input, key); - if ( - !descriptor || - !('value' in descriptor) || - safeScalar(key, descriptor.value) === undefined - ) { + if (safeScalar(key, value) === undefined) { throw new UnsafeTelemetryAttributeErrorV1(key); } } @@ -263,18 +272,48 @@ function readSingleHeader( name: string, ): string | undefined { const values: string[] = []; - for (const key of Object.keys(headers)) { - const descriptor = Object.getOwnPropertyDescriptor(headers, key); + let keys: string[]; + try { + keys = Object.keys(headers); + } catch { + throw new Error(`Unreadable telemetry ${name} header`); + } + for (const key of keys) { + let descriptor: PropertyDescriptor | undefined; + try { + descriptor = Object.getOwnPropertyDescriptor(headers, key); + } catch { + throw new Error(`Unreadable telemetry ${name} header`); + } if (!descriptor || !('value' in descriptor)) throw new Error(`Unreadable telemetry ${name} header`); const value = descriptor.value as string | string[] | undefined; if (key.toLowerCase() !== name) continue; - if (Array.isArray(value)) { - if (!value.every((item) => typeof item === 'string')) { + let arrayValue = false; + try { + arrayValue = Array.isArray(value); + } catch { + throw new Error(`Unreadable telemetry ${name} header`); + } + if (arrayValue) { + let valid = false; + try { + valid = Array.prototype.every.call(value, (item: unknown) => typeof item === 'string'); + } catch { + throw new Error(`Unreadable telemetry ${name} header`); + } + if (!valid) { throw new Error(`Unreadable telemetry ${name} header`); } - values.push(...value); - } else if (value !== undefined) values.push(value); + try { + values.push(...(value as string[])); + } catch { + throw new Error(`Unreadable telemetry ${name} header`); + } + } else if (value !== undefined) { + if (typeof value !== 'string') throw new Error(`Unreadable telemetry ${name} header`); + values.push(value); + } } if (values.length > 1) throw new Error(`Ambiguous telemetry ${name} header`); if (values.length === 0) return undefined; diff --git a/packages/telemetry/test/telemetry-v1.test.mjs b/packages/telemetry/test/telemetry-v1.test.mjs index a00b9234..e8aaa7fc 100644 --- a/packages/telemetry/test/telemetry-v1.test.mjs +++ b/packages/telemetry/test/telemetry-v1.test.mjs @@ -85,6 +85,32 @@ test('telemetry never executes accessor-backed correlation headers', () => { assert.equal(accessed, false); }); +test('telemetry rejects proxies that fail during reflection without exposing trap errors', () => { + const hostileAttributes = new Proxy( + {}, + { + ownKeys() { + throw new Error('attribute trap cause'); + }, + }, + ); + assert.deepEqual(sanitizeTelemetryAttributesV1(hostileAttributes), {}); + assert.throws( + () => assertSafeTelemetryAttributesV1(hostileAttributes), + UnsafeTelemetryAttributeErrorV1, + ); + + const hostileHeaders = new Proxy( + {}, + { + ownKeys() { + throw new Error('header trap cause'); + }, + }, + ); + assert.throws(() => correlationFromHeadersV1(hostileHeaders), /Unreadable telemetry/u); +}); + test('correlation headers round-trip without accepting malformed identifiers', () => { const context = createCorrelationContextV1({ correlationId, @@ -118,6 +144,10 @@ test('correlation headers round-trip without accepting malformed identifiers', ( }), ); assert.throws(() => correlationFromHeadersV1({})); + assert.throws( + () => correlationFromHeadersV1({ 'x-correlation-id': 1 }), + /Unreadable telemetry x-correlation-id header/u, + ); assert.throws(() => correlationFromHeadersV1({ 'x-correlation-id': [correlationId, correlationId] }), ); diff --git a/services/engine/src/databreeze_engine/telemetry.py b/services/engine/src/databreeze_engine/telemetry.py index ab659f9f..b8418ab2 100644 --- a/services/engine/src/databreeze_engine/telemetry.py +++ b/services/engine/src/databreeze_engine/telemetry.py @@ -107,6 +107,10 @@ _LEVELS = frozenset({"debug", "info", "warn", "error"}) +class _LocalTelemetryValidationError(ValueError): + """A validation error raised by this module rather than a provider object.""" + + @dataclass(frozen=True) class CorrelationContext: correlation_id: str @@ -152,7 +156,7 @@ def _validate_key(key: object) -> str: or len(key) > 64 or not _KEY_PATTERN.fullmatch(key) ): - raise ValueError(f"invalid telemetry key: {key!r}") + raise _LocalTelemetryValidationError(f"invalid telemetry key: {key!r}") return key @@ -184,9 +188,9 @@ def assert_safe_attributes(attributes: Mapping[str, Any]) -> None: for raw_key, value in attributes.items(): key = _validate_key(raw_key) if key not in SAFE_ATTRIBUTE_KEYS or _safe_scalar(key, value) is None: - raise ValueError(f"telemetry attribute is not allowed: {key}") - except ValueError: - raise + raise _LocalTelemetryValidationError(f"telemetry attribute is not allowed: {key}") + except _LocalTelemetryValidationError as error: + raise ValueError(str(error)) from None except Exception: raise ValueError("telemetry attributes are not readable") from None @@ -259,19 +263,19 @@ def _single_header(headers: Mapping[str, str | Sequence[str] | None], name: str) try: for key, value in headers.items(): if not isinstance(key, str): - raise ValueError("telemetry header name is not a string") + raise _LocalTelemetryValidationError("telemetry header name is not a string") if key.lower() != name: continue if isinstance(value, str): values.append(value) elif value is not None: if not isinstance(value, Sequence) or isinstance(value, (bytes, bytearray)): - raise ValueError("telemetry header value is not readable") + raise _LocalTelemetryValidationError("telemetry header value is not readable") if not all(isinstance(item, str) for item in value): - raise ValueError("telemetry header value is not readable") + raise _LocalTelemetryValidationError("telemetry header value is not readable") values.extend(value) - except ValueError: - raise + except _LocalTelemetryValidationError as error: + raise ValueError(str(error)) from None except Exception: raise ValueError("telemetry headers are not readable") from None if len(values) > 1: diff --git a/services/engine/tests/test_telemetry.py b/services/engine/tests/test_telemetry.py index c5d4e35c..2860e7a9 100644 --- a/services/engine/tests/test_telemetry.py +++ b/services/engine/tests/test_telemetry.py @@ -78,6 +78,17 @@ def items(self): # type: ignore[override] assert_safe_attributes(HostileMapping()) +def test_engine_telemetry_normalizes_provider_value_errors() -> None: + class ValueErrorMapping(dict[str, object]): + def items(self): # type: ignore[override] + raise ValueError("provider value error must not escape") + + assert sanitize_attributes(ValueErrorMapping()) == {} + with pytest.raises(ValueError, match="not readable") as error: + assert_safe_attributes(ValueErrorMapping()) + assert "provider value error" not in str(error.value) + + def test_engine_telemetry_rejects_hostile_or_non_string_header_values() -> None: class HostileHeaders(dict[str, object]): def items(self): # type: ignore[override] @@ -92,6 +103,14 @@ def items(self): # type: ignore[override] } ) + class ValueErrorHeaders(dict[str, object]): + def items(self): # type: ignore[override] + raise ValueError("provider header value error must not escape") + + with pytest.raises(ValueError, match="not readable") as error: + correlation_from_headers(ValueErrorHeaders()) + assert "provider header value error" not in str(error.value) + def test_engine_accepts_mixed_case_header_names() -> None: context = CorrelationContext( From 2ebadd5a13e05c7b93330160e8228eaca063b821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:54:28 +0700 Subject: [PATCH 78/89] fix(infra): enforce AWS policy and Fargate constraints --- infrastructure/aws/modules/compute/main.tf | 14 ++++++ .../repo-cli/src/check-aws-infrastructure.mjs | 47 ++++++++++++++++++- .../repo-cli/test/aws-infrastructure.test.mjs | 5 +- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/infrastructure/aws/modules/compute/main.tf b/infrastructure/aws/modules/compute/main.tf index 0bfcb70b..7e9c14cb 100644 --- a/infrastructure/aws/modules/compute/main.tf +++ b/infrastructure/aws/modules/compute/main.tf @@ -1,5 +1,12 @@ locals { common_tags = merge(var.tags, { Component = "compute" }) + allowed_worker_memory_by_cpu = { + "256" = [512, 1024, 2048] + "512" = [1024, 2048, 3072, 4096] + "1024" = [2048, 3072, 4096, 5120, 6144, 7168, 8192] + "2048" = [4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384] + "4096" = [8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720] + } } resource "aws_ecs_cluster" "this" { @@ -151,6 +158,13 @@ resource "aws_ecs_task_definition" "worker" { condition = var.environment != "production" || can(regex("@sha256:[0-9a-f]{64}$", var.worker_image)) error_message = "Production worker deployments must use an immutable image digest." } + precondition { + condition = contains( + lookup(local.allowed_worker_memory_by_cpu, tostring(var.worker_cpu), []), + var.worker_memory, + ) + error_message = "worker_memory must be an AWS-supported Fargate size for worker_cpu." + } } } diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 82fe4f30..3fd0fd52 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -31,6 +31,40 @@ const allTerraform = requiredFiles .filter((relativePath) => relativePath.endsWith('.tf')) .map((relativePath) => readFileSync(path.join(infrastructureRoot, relativePath), 'utf8')) .join('\n'); + +function balancedBlocks(text, keyword) { + const blocks = []; + const startPattern = new RegExp(`\\b${keyword}\\s*\\{`, 'g'); + for (const match of text.matchAll(startPattern)) { + const openingBrace = text.indexOf('{', match.index); + let depth = 0; + let quoted = false; + let escaped = false; + for (let index = openingBrace; index < text.length; index += 1) { + const character = text[index]; + if (quoted) { + if (escaped) escaped = false; + else if (character === '\\') escaped = true; + else if (character === '"') quoted = false; + continue; + } + if (character === '"') { + quoted = true; + continue; + } + if (character === '{') depth += 1; + if (character === '}') { + depth -= 1; + if (depth === 0) { + blocks.push(text.slice(openingBrace, index + 1)); + break; + } + } + } + } + return blocks; +} + for (const requiredText of [ 'ap-southeast-1', 'hashicorp/aws', @@ -58,10 +92,19 @@ for (const requiredBoundary of [ if (!allTerraform.includes(requiredBoundary)) fail(`missing required safety boundary ${requiredBoundary}`); } -if (/ingress[\s\S]{0,400}cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u.test(allTerraform)) { +if ( + balancedBlocks(allTerraform, 'ingress').some((block) => + /\bcidr_blocks\s*=\s*\[[^\]]*"0\.0\.0\.0\/0"/u.test(block), + ) +) { fail('a private service security group permits unrestricted ingress'); } -if (/resource\s+"aws_s3_bucket_policy"[\s\S]*?Principal\s*=\s*"\*"/u.test(allTerraform)) { +if ( + /resource\s+"aws_s3_bucket_policy"[\s\S]*?Principal\s*=\s*"\*"/u.test(allTerraform) || + balancedBlocks(allTerraform, 'principals').some((block) => + /identifiers\s*=\s*\[[^\]]*"\*"/u.test(block), + ) +) { fail('the Web bucket policy grants a wildcard principal'); } if ( diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 404300cb..3c3e5af8 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -65,7 +65,8 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s ]) assert.match(sources, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); assert.doesNotMatch(sources, /AKIA[0-9A-Z]{16}|BEGIN (RSA|OPENSSH) PRIVATE KEY/); - assert.doesNotMatch(sources, /ingress[\s\S]{0,400}cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u); + assert.doesNotMatch(sources, /ingress[\s\S]*?cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u); + assert.doesNotMatch(sources, /principals[\s\S]*?identifiers\s*=\s*\[[^\]]*"\*"/u); assert.match(sources, /assign_public_ip\s*=\s*false/u); assert.match(sources, /token\.actions\.githubusercontent\.com:sub/u); assert.match(sources, /repo:\$\{var\.github_repository\}:ref:refs\/heads\/dev/u); @@ -122,6 +123,8 @@ test('AWS production profile enables recovery and prevents public data paths', ( assert.match(computeVariables, /variable "worker_memory"/u); assert.match(computeVariables, /supported Fargate CPU size/u); assert.match(computeVariables, /between 512 and 30720 MiB/u); + assert.match(compute, /allowed_worker_memory_by_cpu/u); + assert.match(compute, /AWS-supported Fargate size for worker_cpu/u); }); test('AWS foundation keeps state and apply outside the repository', () => { From 2286770ffc88431fea2645e66e2b5e44a526f027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:54:46 +0700 Subject: [PATCH 79/89] docs(infra): mark persistence evidence environment-gated --- .../foundation-local-infrastructure-2026-08-02.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index 70d6fdbc..5bed1597 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -32,7 +32,6 @@ Passed: - `node tools/repo-cli/src/local-services-smoke.mjs --help` - `node tools/repo-cli/src/local-services.mjs config` - `node tools/repo-cli/src/local-services.mjs preflight --min-free-gib=0` -- `node tools/repo-cli/src/local-services.mjs persistence-check` *(Docker-gated)* - `git diff --check` Environment-gated: @@ -43,8 +42,9 @@ Environment-gated: without starting containers; the evidence run used a zero-GiB threshold so it remains independent of the workstation's available disk headroom. - Live `compose up`, health polling, port-collision simulation, disk-pressure - threshold validation, and restart-persistence checks must run on a machine - with Docker Desktop/Compose v2 before FND-003 can become `verified`. + threshold validation, and restart-persistence checks (including + `persistence-check`) must run on a machine with Docker Desktop/Compose v2 + before FND-003 can become `verified`. ## Rollback From 2a1df9e97b91907c47902ce4813f2e7cf76c1213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:55:13 +0700 Subject: [PATCH 80/89] test(local): exercise preflight and service topology --- .../test/local-infrastructure.test.mjs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 2881d85e..b274f7da 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -37,7 +37,7 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, /minio-init:[\s\S]*depends_on:[\s\S]*condition: service_healthy/u); assert.match(compose, /minio-init:[\s\S]*restart: 'no'/u); assert.match(compose, /postgres-data:[\s\S]*name: \$\{COMPOSE_PROJECT_NAME/u); - assert.match(compose, /networks: \[local\]/u); + assert.equal((compose.match(/networks: \[local\]/g) ?? []).length, 7); assert.match(compose, /name: \$\{COMPOSE_PROJECT_NAME:-databreeze-local\}-network/u); assert.match(compose, /x-default-logging: &default-logging/u); assert.match(compose, /max-size: 10m/u); @@ -217,12 +217,25 @@ test('local lifecycle commands fail safely around Docker, ports, disk, and volum cwd: repositoryRoot, encoding: 'utf8', }); - if (composeConfig.status === 0) + if (composeConfig.status === 0) { assert.match(composeConfig.stdout, /Compose configuration is valid/u); - assert.match(script, /logs[\s\S]*--no-color/u); - assert.match(script, /--service must name one of/u); - assert.match(script, /--tail must be an integer/u); - assert.match(script, /COMPOSE_PROJECT_NAME must start/u); - assert.match(script, /Redis persistence sentinel was not recovered/u); + } else { + assert.match( + `${composeConfig.stdout}\n${composeConfig.stderr}`, + /Docker CLI is not installed or not on PATH/u, + ); + } + const preflight = spawnSync(process.execPath, [helpScript, 'preflight', '--min-free-gib=0'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + if (preflight.status === 0) { + assert.match(preflight.stdout, /preflight passed without starting services/u); + } else { + assert.match( + `${preflight.stdout}\n${preflight.stderr}`, + /Docker CLI is not installed or not on PATH/u, + ); + } assert.doesNotMatch(script, /redis-cli\s+FLUSH(?:ALL|DB)/iu); }); From 806a81f140131fd1d0cc90b76909b8f7de965d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:57:00 +0700 Subject: [PATCH 81/89] fix(ci): parse workflow policy as YAML --- package.json | 3 +- pnpm-lock.yaml | 45 +++++---- tools/repo-cli/src/check-ci-policy.mjs | 132 +++++++++++++++++-------- 3 files changed, 121 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index b62a858b..fb21c49a 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "prettier": "3.6.2", "turbo": "2.5.6", "typescript": "5.9.2", - "typescript-eslint": "8.43.0" + "typescript-eslint": "8.43.0", + "yaml": "2.8.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b99d5e1e..43cd5809 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: typescript-eslint: specifier: 8.43.0 version: 8.43.0(eslint@9.36.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.2) + yaml: + specifier: 2.8.1 + version: 2.8.1 apps/desktop: dependencies: @@ -67,7 +70,7 @@ importers: version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': specifier: 6.0.5 - version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)) electron: specifier: 43.2.0 version: 43.2.0(supports-color@7.2.0) @@ -79,10 +82,10 @@ importers: version: 5.9.2 vite: specifier: 8.2.0 - version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + version: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)) apps/web: dependencies: @@ -119,7 +122,7 @@ importers: version: 1.62.1 '@tailwindcss/vite': specifier: 4.3.3 - version: 4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + version: 4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)) '@testing-library/react': specifier: 16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -137,7 +140,7 @@ importers: version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': specifier: 6.0.5 - version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)) jsdom: specifier: 30.0.1 version: 30.0.1 @@ -149,10 +152,10 @@ importers: version: 5.9.2 vite: specifier: 8.2.0 - version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + version: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)) packages/config: dependencies: @@ -3061,6 +3064,11 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3835,12 +3843,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': + '@tailwindcss/vite@4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1) '@tanstack/query-core@5.101.4': {} @@ -4119,10 +4127,10 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': + '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1) '@vitest/expect@4.1.10': dependencies: @@ -4133,13 +4141,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1) '@vitest/pretty-format@4.1.10': dependencies: @@ -5396,7 +5404,7 @@ snapshots: validator@13.15.35: {} - vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1): + vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -5409,11 +5417,12 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 tsx: 4.23.1 + yaml: 2.8.1 - vitest@4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)): + vitest@4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -5430,7 +5439,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 @@ -5477,6 +5486,8 @@ snapshots: xmlchars@2.2.0: {} + yaml@2.8.1: {} + yocto-queue@0.1.0: {} zeptomatch@2.1.0: diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index 46e88660..6c97dd12 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { parse } from 'yaml'; const WORKFLOW_DIRECTORY = '.github/workflows'; const REQUIRED_WORKFLOWS = ['quality.yml', 'security.yml', 'release.yml']; @@ -12,67 +13,112 @@ function readWorkflow(root, name) { return fs.readFileSync(filename, 'utf8'); } -function assertPinnedActions(text, filename) { - for (const match of text.matchAll(/(^|\s)uses:\s*([^\s#]+)/gim)) { - const reference = match[2]; - if (reference.startsWith('./') || reference.startsWith('docker://')) continue; +function isRecord(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function parseWorkflow(text, filename) { + try { + const workflow = parse(text, { strict: true, uniqueKeys: true }); + if (!isRecord(workflow)) throw new Error('top-level document must be a mapping'); + return workflow; + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + throw new Error(`${filename} is not valid workflow YAML: ${detail}`); + } +} + +function walk(value, visit) { + if (Array.isArray(value)) { + for (const item of value) walk(item, visit); + return; + } + if (!isRecord(value)) return; + visit(value); + for (const child of Object.values(value)) walk(child, visit); +} + +function containsText(value, expected) { + let found = false; + walk(value, (node) => { + if ( + Object.values(node).some((child) => typeof child === 'string' && child.includes(expected)) + ) { + found = true; + } + }); + return found; +} + +function assertPinnedActions(workflow, filename) { + walk(workflow, (node) => { + if (typeof node.uses !== 'string') return; + const reference = node.uses; + if (reference.startsWith('./') || reference.startsWith('docker://')) return; const at = reference.lastIndexOf('@'); if (at < 1 || !SHA_REFERENCE.test(reference.slice(at + 1))) { throw new Error(`${filename} uses an unpinned action: ${reference}`); } - } + }); } -function assertLeastPrivilege(text, filename) { - if (/permissions:\s*write-all/iu.test(text)) { +function assertLeastPrivilege(workflow, filename) { + const permissions = workflow.permissions; + if (permissions === 'write-all' || (isRecord(permissions) && permissions['write-all'])) { throw new Error(`${filename} grants write-all permissions`); } - if (!/^permissions:\s*$/im.test(text)) { + if (!isRecord(permissions)) { throw new Error(`${filename} must declare a top-level permissions block`); } - if (!/^\s+contents:\s*read\s*$/im.test(text)) { + if (permissions.contents !== 'read') { throw new Error(`${filename} must grant contents: read explicitly`); } - if (/pull_request_target:/iu.test(text)) { - throw new Error(`${filename} must not execute untrusted code from pull_request_target`); - } - if (/AWS_(?:ACCESS_KEY_ID|SECRET_ACCESS_KEY)\s*:/iu.test(text)) { - throw new Error(`${filename} must not define long-lived AWS key environment variables`); - } - if (/uses:\s*actions\/checkout@/iu.test(text) && !/persist-credentials:\s*false/iu.test(text)) { - throw new Error(`${filename} must disable checkout credential persistence`); - } + walk(workflow, (node) => { + if (Object.hasOwn(node, 'pull_request_target')) { + throw new Error(`${filename} must not execute untrusted code from pull_request_target`); + } + if (Object.keys(node).some((key) => /^AWS_(?:ACCESS_KEY_ID|SECRET_ACCESS_KEY)$/u.test(key))) { + throw new Error(`${filename} must not define long-lived AWS key environment variables`); + } + if (typeof node.uses === 'string' && node.uses.startsWith('actions/checkout@')) { + if (!isRecord(node.with) || node.with['persist-credentials'] !== false) { + throw new Error(`${filename} must disable checkout credential persistence`); + } + } + }); } -function assertBoundedJobs(text, filename) { - const jobCount = (text.match(/^\s+runs-on:\s*\S+/gim) ?? []).length; - const timeoutCount = (text.match(/^\s+timeout-minutes:\s*\d+/gim) ?? []).length; - if (jobCount > timeoutCount) { - throw new Error(`${filename} must bound every runner job with timeout-minutes`); +function assertBoundedJobs(workflow, filename) { + if (!isRecord(workflow.jobs)) return; + for (const job of Object.values(workflow.jobs)) { + if (!isRecord(job) || !Object.hasOwn(job, 'runs-on')) continue; + if (!Number.isInteger(job['timeout-minutes']) || job['timeout-minutes'] < 1) { + throw new Error(`${filename} must bound every runner job with timeout-minutes`); + } } } -function assertArtifactOutputs(text, filename) { - const artifactSteps = - text.match( - /-\s+uses:\s+actions\/upload-artifact@[^\n]+[\s\S]*?(?=\n\s+-\s+name:|\n\s+\w+:\s*$|$)/gim, - ) ?? []; - for (const step of artifactSteps) { - if (!/if-no-files-found:\s*error/iu.test(step)) { +function assertArtifactOutputs(workflow, filename) { + walk(workflow, (node) => { + if (typeof node.uses !== 'string' || !node.uses.startsWith('actions/upload-artifact@')) return; + if (!isRecord(node.with) || node.with['if-no-files-found'] !== 'error') { throw new Error(`${filename} artifact uploads must fail when an output is missing`); } - } + }); } export function checkCiPolicy(root = process.cwd()) { const workflows = Object.fromEntries( - REQUIRED_WORKFLOWS.map((name) => [name, readWorkflow(root, name)]), + REQUIRED_WORKFLOWS.map((name) => { + const text = readWorkflow(root, name); + return [name, parseWorkflow(text, name)]; + }), ); - for (const [name, text] of Object.entries(workflows)) { - assertPinnedActions(text, name); - assertLeastPrivilege(text, name); - assertBoundedJobs(text, name); - assertArtifactOutputs(text, name); + for (const [name, workflow] of Object.entries(workflows)) { + assertPinnedActions(workflow, name); + assertLeastPrivilege(workflow, name); + assertBoundedJobs(workflow, name); + assertArtifactOutputs(workflow, name); } const security = workflows['security.yml']; for (const required of [ @@ -82,16 +128,20 @@ export function checkCiPolicy(root = process.cwd()) { 'check-container-policy.mjs', 'generate-sbom.mjs', ]) { - if (!security.includes(required)) throw new Error(`security.yml is missing ${required}`); + if (!containsText(security, required)) throw new Error(`security.yml is missing ${required}`); } const release = workflows['release.yml']; - if (!/id-token:\s*write/iu.test(release)) { + if (!isRecord(release.permissions) || release.permissions['id-token'] !== 'write') { throw new Error('release.yml must request OIDC id-token permission explicitly'); } - if (!release.includes('generate-provenance.mjs')) { + if (!containsText(release, 'generate-provenance.mjs')) { throw new Error('release.yml must generate a provenance record'); } - if (!/^\s+environment:\s*release\s*$/im.test(release)) { + let hasReleaseEnvironment = false; + walk(release, (node) => { + if (node.environment === 'release') hasReleaseEnvironment = true; + }); + if (!hasReleaseEnvironment) { throw new Error('release.yml must use the protected release environment'); } return { workflowCount: REQUIRED_WORKFLOWS.length }; From 24d117dd98498ffacf30235bb85a3b6d5f349378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:57:02 +0700 Subject: [PATCH 82/89] style(cli): format infrastructure assertions --- tools/repo-cli/test/local-infrastructure.test.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index b274f7da..084cad42 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -44,7 +44,10 @@ test('local compose defines pinned, healthy disposable dependencies', () => { assert.match(compose, /max-file: '3'/u); assert.equal((compose.match(/logging: \*default-logging/g) ?? []).length, 7); assert.equal((compose.match(/127\.0\.0\.1:\$\{/g) ?? []).length, 9); - assert.match(read('infrastructure/local/README.md'), /Every published port is bound to `127\.0\.0\.1`/u); + assert.match( + read('infrastructure/local/README.md'), + /Every published port is bound to `127\.0\.0\.1`/u, + ); }); test('local bootstrap is credential-free and creates every owned module schema', () => { From 432fe769b3dfd243c4ae84ded09c6b79599edf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:57:16 +0700 Subject: [PATCH 83/89] docs(ci): record external release protection gate --- docs/operations/foundation-ci-supply-chain-2026-08-02.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/operations/foundation-ci-supply-chain-2026-08-02.md b/docs/operations/foundation-ci-supply-chain-2026-08-02.md index d4fe3238..f62c5d2b 100644 --- a/docs/operations/foundation-ci-supply-chain-2026-08-02.md +++ b/docs/operations/foundation-ci-supply-chain-2026-08-02.md @@ -37,6 +37,12 @@ Hosted CI remains authoritative for the complete dependency, SAST, container, OpenTofu, build, and release-environment gates. The generators never write runtime evidence inside the repository during these checks. +The `release` environment's required reviewers and branch restrictions are +GitHub repository settings rather than workflow YAML. Before promoting to +`main`, an administrator must verify those settings and record the check in +the release evidence; the repository policy checker deliberately verifies the +workflow's environment reference but cannot infer external protection rules. + ## Rollback Revert the focused CI or generator commit that introduced the behavior, rerun From 68218b235be615cd44c5018668e868b16002010a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 19:57:41 +0700 Subject: [PATCH 84/89] docs(infra): list complete local command surface --- docs/operations/foundation-local-infrastructure-2026-08-02.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index 5bed1597..90cf4adc 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -22,6 +22,9 @@ Task: `FND-003 — Close local infrastructure gaps` check, providing the entry point for persistence evidence. - `persistence-check` writes a five-minute Redis sentinel, restarts only Redis, verifies the sentinel, and deletes it; it never flushes a database or volume. +- The documented lifecycle command set includes `config`, `preflight`, `check`, + `start`, `stop`, `reset`, `restart-check`, `persistence-check`, `status`, + `logs`, and the legacy `smoke` entry point. ## Verification From 48a596ae8b1e84fa702398826d4d196473d03eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 20:01:28 +0700 Subject: [PATCH 85/89] style(local): format service lifecycle code --- tools/repo-cli/src/local-services.mjs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 5323e021..1d8409a3 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -228,12 +228,15 @@ function inspectHealth(service, values) { const idResult = runDocker([...composeArgs(values), 'ps', '-q', service], { allowFailure: true }); const id = idResult.stdout.trim(); if (!id) return { state: 'missing', health: 'unknown', detail: 'no container' }; - const inspect = runDocker([ - 'inspect', - '--format', - '{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health{{end}}', - id, - ], { allowFailure: true }); + const inspect = runDocker( + [ + 'inspect', + '--format', + '{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health{{end}}', + id, + ], + { allowFailure: true }, + ); const inspection = inspect.stdout?.trim(); if (inspect.error || inspect.status !== 0 || !inspection) { return { state: 'unknown', health: 'unknown', detail: 'unknown/unknown (inspect unavailable)' }; @@ -364,13 +367,10 @@ export async function main(argv = process.argv.slice(2)) { } if (command === 'logs') { const selected = options.service ? [options.service] : logServices; - runDocker([ - ...composeArgs(values), - 'logs', - '--no-color', - `--tail=${options.tail}`, - ...selected, - ], { capture: false, timeoutMs: 120_000 }); + runDocker( + [...composeArgs(values), 'logs', '--no-color', `--tail=${options.tail}`, ...selected], + { capture: false, timeoutMs: 120_000 }, + ); return; } if (command === 'stop') { From 8118456846f37f754610cb21af2cd88b863cded9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 20:20:11 +0700 Subject: [PATCH 86/89] test(telemetry): assert normalized hostile errors --- packages/telemetry/test/telemetry-v1.test.mjs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/telemetry/test/telemetry-v1.test.mjs b/packages/telemetry/test/telemetry-v1.test.mjs index e8aaa7fc..d4a53d02 100644 --- a/packages/telemetry/test/telemetry-v1.test.mjs +++ b/packages/telemetry/test/telemetry-v1.test.mjs @@ -97,7 +97,13 @@ test('telemetry rejects proxies that fail during reflection without exposing tra assert.deepEqual(sanitizeTelemetryAttributesV1(hostileAttributes), {}); assert.throws( () => assertSafeTelemetryAttributesV1(hostileAttributes), - UnsafeTelemetryAttributeErrorV1, + (error) => { + assert.ok(error instanceof UnsafeTelemetryAttributeErrorV1); + assert.equal(error.key, 'unreadable'); + assert.equal(error.message, 'Telemetry attribute is not allowed: unreadable'); + assert.doesNotMatch(error.message, /attribute trap cause/u); + return true; + }, ); const hostileHeaders = new Proxy( @@ -108,7 +114,14 @@ test('telemetry rejects proxies that fail during reflection without exposing tra }, }, ); - assert.throws(() => correlationFromHeadersV1(hostileHeaders), /Unreadable telemetry/u); + assert.throws( + () => correlationFromHeadersV1(hostileHeaders), + (error) => { + assert.equal(error.message, 'Unreadable telemetry x-correlation-id header'); + assert.doesNotMatch(error.message, /header trap cause/u); + return true; + }, + ); }); test('correlation headers round-trip without accepting malformed identifiers', () => { @@ -146,7 +159,10 @@ test('correlation headers round-trip without accepting malformed identifiers', ( assert.throws(() => correlationFromHeadersV1({})); assert.throws( () => correlationFromHeadersV1({ 'x-correlation-id': 1 }), - /Unreadable telemetry x-correlation-id header/u, + (error) => { + assert.equal(error.message, 'Unreadable telemetry x-correlation-id header'); + return true; + }, ); assert.throws(() => correlationFromHeadersV1({ 'x-correlation-id': [correlationId, correlationId] }), From a19238d12767ce78966864474caaf8d0848a143f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 20:20:15 +0700 Subject: [PATCH 87/89] fix(infra): scan Terraform blocks safely --- .../repo-cli/src/check-aws-infrastructure.mjs | 34 +---- tools/repo-cli/src/terraform-safety.mjs | 139 ++++++++++++++++++ tools/repo-cli/test/terraform-safety.test.mjs | 41 ++++++ 3 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 tools/repo-cli/src/terraform-safety.mjs create mode 100644 tools/repo-cli/test/terraform-safety.test.mjs diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 3fd0fd52..5cbf42f2 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -3,6 +3,7 @@ import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { balancedBlocks } from './terraform-safety.mjs'; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); const infrastructureRoot = path.join(repositoryRoot, 'infrastructure', 'aws'); @@ -32,39 +33,6 @@ const allTerraform = requiredFiles .map((relativePath) => readFileSync(path.join(infrastructureRoot, relativePath), 'utf8')) .join('\n'); -function balancedBlocks(text, keyword) { - const blocks = []; - const startPattern = new RegExp(`\\b${keyword}\\s*\\{`, 'g'); - for (const match of text.matchAll(startPattern)) { - const openingBrace = text.indexOf('{', match.index); - let depth = 0; - let quoted = false; - let escaped = false; - for (let index = openingBrace; index < text.length; index += 1) { - const character = text[index]; - if (quoted) { - if (escaped) escaped = false; - else if (character === '\\') escaped = true; - else if (character === '"') quoted = false; - continue; - } - if (character === '"') { - quoted = true; - continue; - } - if (character === '{') depth += 1; - if (character === '}') { - depth -= 1; - if (depth === 0) { - blocks.push(text.slice(openingBrace, index + 1)); - break; - } - } - } - } - return blocks; -} - for (const requiredText of [ 'ap-southeast-1', 'hashicorp/aws', diff --git a/tools/repo-cli/src/terraform-safety.mjs b/tools/repo-cli/src/terraform-safety.mjs new file mode 100644 index 00000000..f5aceed8 --- /dev/null +++ b/tools/repo-cli/src/terraform-safety.mjs @@ -0,0 +1,139 @@ +function maskTerraformNonCode(text) { + const masked = text.split(''); + let quoted = false; + let escaped = false; + let lineComment = false; + let blockComment = false; + let heredoc = undefined; + let lineStart = true; + + const mask = (index) => { + if (masked[index] !== '\n') masked[index] = ' '; + }; + + for (let index = 0; index < text.length; index += 1) { + const character = text[index]; + const next = text[index + 1]; + + if (heredoc !== undefined) { + if (lineStart) { + const lineEnd = text.indexOf('\n', index) === -1 ? text.length : text.indexOf('\n', index); + const rawLine = text.slice(index, lineEnd).replace(/\r$/u, ''); + const candidate = heredoc.allowIndent ? rawLine.trim() : rawLine; + for (let position = index; position < lineEnd; position += 1) mask(position); + if (candidate === heredoc.delimiter) heredoc = undefined; + lineStart = false; + index = lineEnd - 1; + continue; + } + mask(index); + if (character === '\n') lineStart = true; + continue; + } + + if (lineComment) { + mask(index); + if (character === '\n') { + lineComment = false; + lineStart = true; + } + continue; + } + + if (blockComment) { + mask(index); + if (character === '*' && next === '/') { + mask(index + 1); + index += 1; + blockComment = false; + } + lineStart = character === '\n'; + continue; + } + + if (quoted) { + if (escaped) escaped = false; + else if (character === '\\') escaped = true; + else if (character === '"') quoted = false; + if (character === '\n') lineStart = true; + else lineStart = false; + continue; + } + + if (character === '"') { + quoted = true; + lineStart = false; + continue; + } + if (character === '#') { + lineComment = true; + mask(index); + lineStart = false; + continue; + } + if (character === '/' && next === '/') { + lineComment = true; + mask(index); + mask(index + 1); + index += 1; + lineStart = false; + continue; + } + if (character === '/' && next === '*') { + blockComment = true; + mask(index); + mask(index + 1); + index += 1; + lineStart = false; + continue; + } + if (character === '<' && next === '<') { + const match = text.slice(index).match(/^<<(-?)([A-Za-z_][A-Za-z0-9_-]*)/u); + if (match) { + for (let position = index; position < index + match[0].length; position += 1) { + mask(position); + } + heredoc = { allowIndent: match[1] === '-', delimiter: match[2] }; + index += match[0].length - 1; + lineStart = false; + continue; + } + } + lineStart = character === '\n'; + } + return masked.join(''); +} + +export function balancedBlocks(text, keyword) { + const masked = maskTerraformNonCode(text); + const blocks = []; + const startPattern = new RegExp(`\\b${keyword}\\s*\\{`, 'g'); + for (const match of masked.matchAll(startPattern)) { + const openingBrace = masked.indexOf('{', match.index); + let depth = 0; + let quoted = false; + let escaped = false; + for (let index = openingBrace; index < masked.length; index += 1) { + const character = masked[index]; + if (quoted) { + if (escaped) escaped = false; + else if (character === '\\') escaped = true; + else if (character === '"') quoted = false; + continue; + } + if (character === '"') { + quoted = true; + continue; + } + if (character === '{') depth += 1; + if (character === '}') { + depth -= 1; + if (depth === 0) { + blocks.push(masked.slice(openingBrace, index + 1)); + break; + } + } + } + } + return blocks; +} diff --git a/tools/repo-cli/test/terraform-safety.test.mjs b/tools/repo-cli/test/terraform-safety.test.mjs new file mode 100644 index 00000000..b29e0816 --- /dev/null +++ b/tools/repo-cli/test/terraform-safety.test.mjs @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { balancedBlocks } from '../src/terraform-safety.mjs'; + +test('Terraform block scanning ignores comments, heredocs, and braces in strings', () => { + const source = ` +# ingress { cidr_blocks = ["0.0.0.0/0"] } +/* ingress { cidr_blocks = ["0.0.0.0/0"] } */ +locals { + description = <<-EOT + ingress { + cidr_blocks = ["0.0.0.0/0"] + } + braces: { }; + EOT +} +ingress { + description = "literal } brace" + ${' '.repeat(500)} + cidr_blocks = ["10.0.0.0/8"] +} +`; + const blocks = balancedBlocks(source, 'ingress'); + assert.equal(blocks.length, 1); + assert.match(blocks[0], /10\.0\.0\.0\/8/u); + assert.doesNotMatch(blocks[0], /0\.0\.0\.0\/0/u); +}); + +test('Terraform principal scanning ignores commented wildcard identifiers', () => { + const source = ` +/* principals { identifiers = ["*"] } */ +principals { + type = "Service" + identifiers = ["example.amazonaws.com"] +} +`; + const blocks = balancedBlocks(source, 'principals'); + assert.equal(blocks.length, 1); + assert.doesNotMatch(blocks[0], /identifiers\s*=\s*\[[^\]]*"\*"/u); +}); From 01dc6641a9ee367730b83637bd63fd7eeaf994ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 20:20:18 +0700 Subject: [PATCH 88/89] fix(ci): require commands in run steps --- tools/repo-cli/src/check-ci-policy.mjs | 13 +++----- tools/repo-cli/test/ci-policy.test.mjs | 45 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/tools/repo-cli/src/check-ci-policy.mjs b/tools/repo-cli/src/check-ci-policy.mjs index 6c97dd12..d75d426b 100644 --- a/tools/repo-cli/src/check-ci-policy.mjs +++ b/tools/repo-cli/src/check-ci-policy.mjs @@ -38,14 +38,10 @@ function walk(value, visit) { for (const child of Object.values(value)) walk(child, visit); } -function containsText(value, expected) { +function containsRunText(value, expected) { let found = false; walk(value, (node) => { - if ( - Object.values(node).some((child) => typeof child === 'string' && child.includes(expected)) - ) { - found = true; - } + if (typeof node.run === 'string' && node.run.includes(expected)) found = true; }); return found; } @@ -128,13 +124,14 @@ export function checkCiPolicy(root = process.cwd()) { 'check-container-policy.mjs', 'generate-sbom.mjs', ]) { - if (!containsText(security, required)) throw new Error(`security.yml is missing ${required}`); + if (!containsRunText(security, required)) + throw new Error(`security.yml is missing ${required}`); } const release = workflows['release.yml']; if (!isRecord(release.permissions) || release.permissions['id-token'] !== 'write') { throw new Error('release.yml must request OIDC id-token permission explicitly'); } - if (!containsText(release, 'generate-provenance.mjs')) { + if (!containsRunText(release, 'generate-provenance.mjs')) { throw new Error('release.yml must generate a provenance record'); } let hasReleaseEnvironment = false; diff --git a/tools/repo-cli/test/ci-policy.test.mjs b/tools/repo-cli/test/ci-policy.test.mjs index 526347c1..b8e0b8df 100644 --- a/tools/repo-cli/test/ci-policy.test.mjs +++ b/tools/repo-cli/test/ci-policy.test.mjs @@ -167,3 +167,48 @@ test('CI policy rejects artifact steps without missing-output failure', () => { fs.writeFileSync(path.join(root, '.github/workflows', name), text); assert.throws(() => checkCiPolicy(root), /artifact uploads must fail/u); }); + +test('CI policy requires security and release commands in run values', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'databreeze-ci-command-fields-')); + fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true }); + const checkout = 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683'; + const checkedOutJob = [ + 'jobs:', + ' check:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ].join('\n'); + const security = [ + 'name: "pnpm audit check-secret-patterns.mjs check-license-policy.mjs check-container-policy.mjs generate-sbom.mjs"', + 'permissions:', + ' contents: read', + checkedOutJob, + ].join('\n'); + const release = [ + 'name: "generate-provenance.mjs"', + 'permissions:', + ' contents: read', + ' id-token: write', + 'jobs:', + ' release:', + ' runs-on: ubuntu-24.04', + ' timeout-minutes: 10', + ' environment: release', + ' steps:', + ` - uses: ${checkout}`, + ' with:', + ' persist-credentials: false', + ].join('\n'); + const quality = ['name: q', 'permissions:', ' contents: read', checkedOutJob].join('\n'); + for (const [name, text] of Object.entries({ + 'quality.yml': quality, + 'security.yml': security, + 'release.yml': release, + })) + fs.writeFileSync(path.join(root, '.github/workflows', name), text); + assert.throws(() => checkCiPolicy(root), /security\.yml is missing pnpm audit/u); +}); From bfb1214110e2324cf34b0b11843d485e82c5f84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Sun, 2 Aug 2026 20:31:28 +0700 Subject: [PATCH 89/89] test(infra): cover Terraform slash comments --- tools/repo-cli/test/terraform-safety.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/repo-cli/test/terraform-safety.test.mjs b/tools/repo-cli/test/terraform-safety.test.mjs index b29e0816..34f43d26 100644 --- a/tools/repo-cli/test/terraform-safety.test.mjs +++ b/tools/repo-cli/test/terraform-safety.test.mjs @@ -6,6 +6,7 @@ import { balancedBlocks } from '../src/terraform-safety.mjs'; test('Terraform block scanning ignores comments, heredocs, and braces in strings', () => { const source = ` # ingress { cidr_blocks = ["0.0.0.0/0"] } +// ingress { cidr_blocks = ["0.0.0.0/0"] } } /* ingress { cidr_blocks = ["0.0.0.0/0"] } */ locals { description = <<-EOT