From c8ad5ea36875bdd8b98683108e704386d89051b7 Mon Sep 17 00:00:00 2001 From: whyour Date: Sat, 22 Aug 2026 13:44:19 +0800 Subject: [PATCH] fix(ci): restore ql3 multi-profile gates --- .github/workflows/ql3-ci.yml | 21 ++-- .../src/task-start/taskStartRepository.ts | 60 ++++----- .../test/postgres.integration.test.cjs | 35 ++++-- .../test/taskStartRepository.test.cjs | 35 +++--- .../workerPosixExecutionExecutor.test.cjs | 1 + ...ql3-cloudnativepg-barman-live-contract.cjs | 1 + scripts/ql3-cloudnativepg-live-contract.cjs | 5 + scripts/ql3-cluster-oci-layout-audit.cjs | 49 +++++--- scripts/ql3-local-owner-readonly-contract.cjs | 119 ++++++++++++++++++ ...gin-package-recovery-e2e-live-contract.cjs | 15 ++- scripts/ql3-postgres-ha-evidence-audit.cjs | 2 +- ...edential-test-kubernetes-live-contract.cjs | 1 + ...l3CloudNativePgBarmanLiveContract.test.cjs | 1 + test/back/ql3ClusterOciLayoutAudit.test.cjs | 35 ++++-- .../ql3LocalOwnerReadonlyContract.test.cjs | 45 +++++++ test/back/ql3PostgresHaEvidenceAudit.test.cjs | 5 +- ...geTaskReconciliationRepositoryContract.cjs | 1 + 17 files changed, 320 insertions(+), 111 deletions(-) create mode 100644 scripts/ql3-local-owner-readonly-contract.cjs create mode 100644 test/back/ql3LocalOwnerReadonlyContract.test.cjs diff --git a/.github/workflows/ql3-ci.yml b/.github/workflows/ql3-ci.yml index cedc966c..ead7d733 100644 --- a/.github/workflows/ql3-ci.yml +++ b/.github/workflows/ql3-ci.yml @@ -140,6 +140,10 @@ jobs: run: node -e "if (process.arch !== '${{ matrix.arch }}') throw new Error('unexpected architecture ' + process.arch)" - name: Install workspace dependencies without lifecycle scripts run: pnpm install --frozen-lockfile --ignore-scripts + - name: Initialize the public test environment + run: | + cp .env.example .env + mkdir -p data/db - name: Rebuild the reviewed native SQLite binding run: pnpm rebuild @whyour/sqlite3 - name: Build benchmark targets once @@ -855,9 +859,7 @@ jobs: --volume "${{ github.workspace }}:/workspace:ro" --workdir /workspace node:24.18.0-bookworm-slim - node --test --test-concurrency=1 - packages/ql3-local-command-file/test/*.test.cjs - packages/ql3-local-owner-*/test/*.test.cjs + node scripts/ql3-local-owner-readonly-contract.cjs --mode=root - name: Exercise Owner authorities as non-root in a read-only container run: >- docker run --rm --read-only @@ -870,9 +872,7 @@ jobs: --volume "${{ github.workspace }}:/workspace:ro" --workdir /workspace node:24.18.0-bookworm-slim - node --test --test-concurrency=1 - packages/ql3-local-command-file/test/*.test.cjs - packages/ql3-local-owner-*/test/*.test.cjs + node scripts/ql3-local-owner-readonly-contract.cjs --mode=nonroot - name: Build and audit production-only local Profile artifacts run: | pnpm audit:artifact:edge:ql3 @@ -963,18 +963,21 @@ jobs: docker exec ${{ job.services.postgres.id }} psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "ALTER DATABASE ql3_contract OWNER TO ql3_migration" - name: Create schema and exact grants through the reviewed migration stream env: - QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract - run: node --test packages/ql3-cluster-postgres/test/postgres.integration.test.cjs + QL3_POSTGRES_MIGRATION_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract + QL3_POSTGRES_TLS_MODE: disable + QL3_POSTGRES_ALLOW_INSECURE: 'true' + run: node packages/ql3-cluster-postgres/dist/migration/migrationCli.js - name: Test readiness, isolated roles, shared Repositories, rollback and SQLSTATE mapping env: QL3_TEST_POSTGRES_MIGRATION_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_RUNTIME_URL: postgresql://ql3_runtime:ql3_runtime_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_ADMIN_URL: postgresql://ql3_admin:ql3_admin_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL: postgresql://ql3_automation_manager:ql3_automation_manager_test@127.0.0.1:5432/ql3_contract + QL3_TEST_POSTGRES_RUN_MANAGER_URL: postgresql://ql3_run_manager:ql3_run_manager_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_PACKAGE_MANAGER_URL: postgresql://ql3_package_manager:ql3_package_manager_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_PACKAGE_EXECUTOR_URL: postgresql://ql3_package_executor:ql3_package_executor_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_WORKER_INGRESS_URL: postgresql://ql3_worker_ingress:ql3_worker_ingress_test@127.0.0.1:5432/ql3_contract - run: node --test packages/ql3-cluster-postgres/test/postgres.integration.test.cjs + run: node --test --test-concurrency=1 packages/ql3-cluster-postgres/test/postgres.integration.test.cjs - name: Test automation management concurrency and post-commit response loss over mTLS env: QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract diff --git a/packages/ql3-cluster-postgres/src/task-start/taskStartRepository.ts b/packages/ql3-cluster-postgres/src/task-start/taskStartRepository.ts index a962921f..54f304ec 100644 --- a/packages/ql3-cluster-postgres/src/task-start/taskStartRepository.ts +++ b/packages/ql3-cluster-postgres/src/task-start/taskStartRepository.ts @@ -6,7 +6,6 @@ import { TaskStartUnavailableError, normalizeTaskStartCommand, normalizeTaskStartResult, - type TaskStartAllowedRole, type TaskStartCommand, type TaskStartRepository, type TaskStartResult, @@ -32,12 +31,6 @@ import { type Row = Record; -const ALLOWED_ROLES = new Set([ - 'owner', - 'admin', - 'operator', -]); - function unavailable(options?: ErrorOptions): TaskStartUnavailableError { return new TaskStartUnavailableError(options); } @@ -193,34 +186,30 @@ export class PostgresTaskStartRepository implements TaskStartRepository { try { await configurePostgresDefinitionTransaction(client); began = true; - const project = await client.query(` - SELECT status AS "projectStatus", version AS "projectVersion" - FROM "ql3"."projects" WHERE id = $1 FOR UPDATE - `, [command.projectId]); - if (project.rows.length === 0) throw new TaskStartNotFoundError(); - if (project.rows.length !== 1) throw unavailable(); - const binding = await client.query(` - SELECT version AS "bindingVersion", state AS "bindingState", - role AS "bindingRole" - FROM "ql3"."project_role_bindings" - WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3 - ORDER BY version DESC LIMIT 1 - FOR SHARE - `, [command.projectId, command.subject.type, command.subject.id]); - const currentProject = project.rows[0]!; - const currentBinding = binding.rows[0]; - if ( - text(currentProject, 'projectStatus') !== 'active' || - integer(currentProject, 'projectVersion') !== - command.policyFence.projectVersion || - !currentBinding || - integer(currentBinding, 'bindingVersion') !== - command.policyFence.bindingVersion || - text(currentBinding, 'bindingState') !== 'active' || - !ALLOWED_ROLES.has( - text(currentBinding, 'bindingRole') as TaskStartAllowedRole, - ) - ) { + const authorization = await client.query( + `SELECT "ql3"."lock_run_management_policy_fence"( + $1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer + ) AS "matches"`, + [ + command.projectId, + command.subject.type, + command.subject.id, + command.policyFence.projectVersion, + command.policyFence.bindingVersion, + ], + ); + if (authorization.rows.length !== 1) throw unavailable(); + if (authorization.rows[0]?.matches !== true) { + const project = await client.query( + `SELECT EXISTS ( + SELECT 1 FROM "ql3"."projects" WHERE id = $1 + ) AS "exists"`, + [command.projectId], + ); + if (project.rows.length !== 1) throw unavailable(); + if (!postgresRequiredBoolean(project.rows[0]!.exists, unavailable)) { + throw new TaskStartNotFoundError(); + } throw new TaskStartFenceRejectedError('authorization_changed'); } @@ -261,7 +250,6 @@ export class PostgresTaskStartRepository implements TaskStartRepository { AND revision.task_id = head.task_id AND revision.revision = head.current_revision WHERE head.project_id = $1 AND head.task_id = $2 - FOR UPDATE OF head `, [command.projectId, command.taskId]); if (task.rows.length === 0) throw new TaskStartNotFoundError(); if (task.rows.length !== 1) throw unavailable(); diff --git a/packages/ql3-cluster-postgres/test/postgres.integration.test.cjs b/packages/ql3-cluster-postgres/test/postgres.integration.test.cjs index 8c26a5ed..d3c0001a 100644 --- a/packages/ql3-cluster-postgres/test/postgres.integration.test.cjs +++ b/packages/ql3-cluster-postgres/test/postgres.integration.test.cjs @@ -626,6 +626,8 @@ const adminConnectionString = const automationManagerConnectionString = process.env.QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL ?? migrationConnectionString; +const runManagerConnectionString = + process.env.QL3_TEST_POSTGRES_RUN_MANAGER_URL ?? migrationConnectionString; const packageManagerConnectionString = process.env.QL3_TEST_POSTGRES_PACKAGE_MANAGER_URL ?? migrationConnectionString; @@ -678,6 +680,8 @@ if (!migrationConnectionString) { ? adminConnectionString : role === 'automation-manager' ? automationManagerConnectionString + : role === 'run-manager' + ? runManagerConnectionString : role === 'package-manager' ? packageManagerConnectionString : role === 'package-executor' @@ -1045,7 +1049,7 @@ if (!migrationConnectionString) { const taskId = 'task-start-command'; const subjectId = 'usr_task_start_integration'; const migrationDatabase = await open('migration'); - let runtimeDatabase; + let runManagerDatabase; try { await runPostgresMigrations({ pool: migrationDatabase.pool }); await migrationDatabase.pool.query( @@ -1102,11 +1106,13 @@ if (!migrationConnectionString) { }) ).definition; - runtimeDatabase = - runtimeConnectionString === migrationConnectionString + runManagerDatabase = + runManagerConnectionString === migrationConnectionString ? migrationDatabase - : await open('runtime'); - const repository = new PostgresTaskStartRepository(runtimeDatabase.pool); + : await open('run-manager'); + const repository = new PostgresTaskStartRepository( + runManagerDatabase.pool, + ); const command = { projectId, taskId, @@ -1158,8 +1164,8 @@ if (!migrationConnectionString) { }, ]); } finally { - if (runtimeDatabase && runtimeDatabase !== migrationDatabase) { - await runtimeDatabase.close(); + if (runManagerDatabase && runManagerDatabase !== migrationDatabase) { + await runManagerDatabase.close(); } await migrationDatabase.close(); } @@ -2193,6 +2199,9 @@ if (!migrationConnectionString) { const migrationDatabase = await open('migration'); try { await runPostgresMigrations({ pool: migrationDatabase.pool }); + await migrationDatabase.pool.query( + 'TRUNCATE TABLE "ql3"."runs" CASCADE', + ); await observeContractPublisherTrust(migrationDatabase.pool); await migrationDatabase.pool.query( `INSERT INTO "ql3"."projects" ( @@ -2577,6 +2586,9 @@ if (!migrationConnectionString) { const migrationDatabase = await open('migration'); try { await runPostgresMigrations({ pool: migrationDatabase.pool }); + await migrationDatabase.pool.query( + 'TRUNCATE TABLE "ql3"."plugin_package_installs" CASCADE', + ); await observeContractPublisherTrust(migrationDatabase.pool); await migrationDatabase.pool.query( `INSERT INTO "ql3"."projects" ( @@ -2589,7 +2601,7 @@ if (!migrationConnectionString) { await migrationDatabase.close(); } const executorDatabase = await open('package-executor'); - const adminDatabase = await open('admin'); + const automationManagerDatabase = await open('automation-manager'); return { repository: new PostgresPluginPackageTaskReconciliationRepository( executorDatabase.pool, @@ -2607,11 +2619,14 @@ if (!migrationConnectionString) { ), ), taskRepository: new PostgresTaskDefinitionRepository( - adminDatabase.pool, + automationManagerDatabase.pool, fixture.registry, ), close: async () => { - await Promise.all([executorDatabase.close(), adminDatabase.close()]); + await Promise.all([ + executorDatabase.close(), + automationManagerDatabase.close(), + ]); }, }; }, diff --git a/packages/ql3-cluster-postgres/test/taskStartRepository.test.cjs b/packages/ql3-cluster-postgres/test/taskStartRepository.test.cjs index 3d4b0aae..d03a72a3 100644 --- a/packages/ql3-cluster-postgres/test/taskStartRepository.test.cjs +++ b/packages/ql3-cluster-postgres/test/taskStartRepository.test.cjs @@ -136,19 +136,12 @@ function fixture(options = {}) { normalized.startsWith('SELECT set_config') || normalized.startsWith('INSERT INTO') ) return { rows: [], rowCount: normalized.startsWith('INSERT') ? 1 : 0 }; - if (normalized.includes('FROM "ql3"."projects"')) { - const rows = options.projectRows ?? [{ - projectStatus: 'active', - projectVersion: 2, - }]; + if (normalized.includes('lock_run_management_policy_fence')) { + const rows = options.authorizationRows ?? [{ matches: true }]; return { rows, rowCount: rows.length }; } - if (normalized.includes('FROM "ql3"."project_role_bindings"')) { - const rows = options.bindingRows ?? [{ - bindingVersion: 3, - bindingState: 'active', - bindingRole: 'operator', - }]; + if (normalized.includes('SELECT EXISTS')) { + const rows = options.projectExistenceRows ?? [{ exists: true }]; return { rows, rowCount: rows.length }; } if (normalized.includes('FROM "ql3"."runs"')) { @@ -203,11 +196,11 @@ test('revalidates Policy and Task/execution digests before one atomic Run aggreg executionRevisionDigest: EXECUTION.contentDigest, createdAtMs: 1_000, }); - const project = calls.findIndex(({ sql }) => sql.includes('FROM "ql3"."projects"')); - const binding = calls.findIndex(({ sql }) => sql.includes('project_role_bindings')); + const authorization = calls.findIndex(({ sql }) => + sql.includes('lock_run_management_policy_fence')); const task = calls.findIndex(({ sql }) => sql.includes('task_definitions')); const execution = calls.findIndex(({ sql }) => sql.includes('task_execution_revisions')); - assert.ok(project < binding && binding < task && task < execution); + assert.ok(authorization < task && task < execution); assert.equal(calls.filter(({ sql }) => sql.startsWith('INSERT INTO')).length, 4); assert.equal(calls.some(({ sql }) => sql === 'COMMIT'), true); }); @@ -273,15 +266,17 @@ test('returns the original durable identities for an exact replay', async () => test('rejects missing, authorization, definition and disabled fences', async () => { await assert.rejects( - fixture({ projectRows: [] }).repository.startTask(command()), + fixture({ + authorizationRows: [{ matches: null }], + projectExistenceRows: [{ exists: false }], + }).repository.startTask(command()), TaskStartNotFoundError, ); await assert.rejects( - fixture({ bindingRows: [{ - bindingVersion: 4, - bindingState: 'revoked', - bindingRole: null, - }] }).repository.startTask(command()), + fixture({ + authorizationRows: [{ matches: false }], + projectExistenceRows: [{ exists: true }], + }).repository.startTask(command()), (error) => error instanceof TaskStartFenceRejectedError && error.reason === 'authorization_changed', diff --git a/packages/ql3-worker-runtime/test/workerPosixExecutionExecutor.test.cjs b/packages/ql3-worker-runtime/test/workerPosixExecutionExecutor.test.cjs index ee9de589..f9a04d49 100644 --- a/packages/ql3-worker-runtime/test/workerPosixExecutionExecutor.test.cjs +++ b/packages/ql3-worker-runtime/test/workerPosixExecutionExecutor.test.cjs @@ -170,6 +170,7 @@ test('accepts timeout only when starting ACK supplied a durable deadline', async })); assert.equal(result.status, 'started'); assert.equal(barriers, 1); + await waitForReceipt(roots.receiptRoot); }); test('propagates unknown outcome when durable identity capture fails after spawn', async (t) => { diff --git a/scripts/ql3-cloudnativepg-barman-live-contract.cjs b/scripts/ql3-cloudnativepg-barman-live-contract.cjs index b05257d9..bc80433a 100644 --- a/scripts/ql3-cloudnativepg-barman-live-contract.cjs +++ b/scripts/ql3-cloudnativepg-barman-live-contract.cjs @@ -55,6 +55,7 @@ const POSTGRES_ROLE_NAMES = Object.freeze([ 'ql3_migration', 'ql3_package_executor', 'ql3_package_manager', + 'ql3_run_manager', 'ql3_runtime', 'ql3_worker_credential_executor', 'ql3_worker_credential_manager', diff --git a/scripts/ql3-cloudnativepg-live-contract.cjs b/scripts/ql3-cloudnativepg-live-contract.cjs index 443e8b76..21eedbfb 100644 --- a/scripts/ql3-cloudnativepg-live-contract.cjs +++ b/scripts/ql3-cloudnativepg-live-contract.cjs @@ -53,6 +53,7 @@ const ROLE_NAMES = Object.freeze([ 'ql3_migration', 'ql3_package_executor', 'ql3_package_manager', + 'ql3_run_manager', 'ql3_runtime', 'ql3_worker_credential_executor', 'ql3_worker_credential_manager', @@ -733,10 +734,14 @@ async function main() { ql3_migration: randomSecret(), ql3_runtime: randomSecret(), ql3_admin: randomSecret(), + ql3_ai_credential_manager: randomSecret(), + ql3_ai_credential_tester: randomSecret(), + ql3_ai_maintenance: randomSecret(), ql3_automation_manager: randomSecret(), ql3_approval_manager: randomSecret(), ql3_package_manager: randomSecret(), ql3_package_executor: randomSecret(), + ql3_run_manager: randomSecret(), ql3_worker_credential_manager: randomSecret(), ql3_worker_credential_executor: randomSecret(), ql3_worker_ingress: randomSecret(), diff --git a/scripts/ql3-cluster-oci-layout-audit.cjs b/scripts/ql3-cluster-oci-layout-audit.cjs index 52105b54..070b0198 100644 --- a/scripts/ql3-cluster-oci-layout-audit.cjs +++ b/scripts/ql3-cluster-oci-layout-audit.cjs @@ -16,6 +16,7 @@ const QL3_VERSION = readReleaseIdentity(DEFAULT_ROOT).version; const OCI_INDEX_MEDIA_TYPE = 'application/vnd.oci.image.index.v1+json'; const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json'; const OCI_CONFIG_MEDIA_TYPE = 'application/vnd.oci.image.config.v1+json'; +const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json'; const OCI_LAYER_MEDIA_TYPE = 'application/vnd.oci.image.layer.v1.tar+gzip'; const IN_TOTO_MEDIA_TYPE = 'application/vnd.in-toto+json'; const SPDX_PREDICATE_TYPE = 'https://spdx.dev/Document'; @@ -447,23 +448,39 @@ function auditAttestation( OCI_MANIFEST_MEDIA_TYPE, MAX_INDEX_BYTES, ); - const config = blobReader.readJson( - manifest.config, - OCI_CONFIG_MEDIA_TYPE, - MAX_CONFIG_BYTES, - ); - if ( - config.architecture !== 'unknown' || - config.os !== 'unknown' || - JSON.stringify(config.config) !== JSON.stringify({}) || - !Array.isArray(config.rootfs?.diff_ids) || - !Array.isArray(manifest.layers) || - JSON.stringify(config.rootfs.diff_ids) !== - JSON.stringify(manifest.layers.map((layer) => layer.digest)) - ) { - throw new Error( - 'OCI attestation config must bind an empty unknown platform', + if (manifest.config?.mediaType === OCI_EMPTY_CONFIG_MEDIA_TYPE) { + normalizeDescriptor( + manifest.config, + OCI_EMPTY_CONFIG_MEDIA_TYPE, + MAX_CONFIG_BYTES, ); + const emptyConfig = blobReader.read( + manifest.config, + MAX_CONFIG_BYTES, + true, + ); + if (emptyConfig.toString('utf8') !== '{}') { + throw new Error('OCI attestation empty config must be canonical'); + } + } else { + const config = blobReader.readJson( + manifest.config, + OCI_CONFIG_MEDIA_TYPE, + MAX_CONFIG_BYTES, + ); + if ( + config.architecture !== 'unknown' || + config.os !== 'unknown' || + JSON.stringify(config.config) !== JSON.stringify({}) || + !Array.isArray(config.rootfs?.diff_ids) || + !Array.isArray(manifest.layers) || + JSON.stringify(config.rootfs.diff_ids) !== + JSON.stringify(manifest.layers.map((layer) => layer.digest)) + ) { + throw new Error( + 'OCI attestation config must bind an empty unknown platform', + ); + } } if (!Array.isArray(manifest.layers) || manifest.layers.length !== 2) { throw new Error('OCI attestation must contain exactly SBOM and provenance'); diff --git a/scripts/ql3-local-owner-readonly-contract.cjs b/scripts/ql3-local-owner-readonly-contract.cjs new file mode 100644 index 00000000..de98428a --- /dev/null +++ b/scripts/ql3-local-owner-readonly-contract.cjs @@ -0,0 +1,119 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const repositoryRoot = path.resolve(__dirname, '..'); + +const HOST_BOUND_TESTS = Object.freeze({ + 'packages/ql3-local-owner-cli/test/adoptedDeploymentBundle.test.cjs': + 'requires repository-owned deployment entrypoint material', + 'packages/ql3-local-owner-cli/test/localDeployment.test.cjs': + 'exercises mutable deployment staging and host ownership', + 'packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs': + 'exercises sealed SQLite WAL and SHM capture assets', + 'packages/ql3-local-owner-cli/test/serviceBridgeRoot.test.cjs': + 'installs systemd and OpenRC descriptors into host service directories', +}); + +function collectReadonlyOwnerTests(root = repositoryRoot) { + const packagesRoot = path.join(root, 'packages'); + const packageNames = fs + .readdirSync(packagesRoot, { withFileTypes: true }) + .filter( + (entry) => + entry.isDirectory() && + (entry.name === 'ql3-local-command-file' || + entry.name.startsWith('ql3-local-owner-')), + ) + .map(({ name }) => name) + .sort(); + const discovered = []; + for (const packageName of packageNames) { + const testRoot = path.join(packagesRoot, packageName, 'test'); + if (!fs.existsSync(testRoot)) continue; + for (const entry of fs.readdirSync(testRoot, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith('.test.cjs')) { + discovered.push( + path.posix.join('packages', packageName, 'test', entry.name), + ); + } + } + } + discovered.sort(); + const missingClassifications = Object.keys(HOST_BOUND_TESTS).filter( + (file) => !discovered.includes(file), + ); + if (missingClassifications.length !== 0) { + throw new Error( + `reviewed host-bound Owner tests are missing: ${missingClassifications.join( + ', ', + )}`, + ); + } + const tests = discovered.filter((file) => !(file in HOST_BOUND_TESTS)); + if (tests.length === 0) { + throw new Error('read-only Owner test plan is empty'); + } + return Object.freeze({ + tests: Object.freeze(tests), + hostBound: HOST_BOUND_TESTS, + }); +} + +function assertWriteRejected(target) { + try { + fs.writeFileSync(target, 'unexpected-write', { flag: 'wx' }); + } catch (error) { + if (error?.code === 'EROFS' || error?.code === 'EACCES') return; + throw error; + } + try { + fs.unlinkSync(target); + } finally { + throw new Error(`read-only boundary accepted a write: ${target}`); + } +} + +function assertContainerBoundary(mode) { + if (!['root', 'nonroot'].includes(mode)) { + throw new Error( + 'usage: ql3-local-owner-readonly-contract.cjs --mode=root|nonroot', + ); + } + const uid = typeof process.getuid === 'function' ? process.getuid() : null; + if (mode === 'root' ? uid !== 0 : uid === null || uid === 0) { + throw new Error(`Owner read-only actor identity is invalid for ${mode}`); + } + const temporaryProbe = fs.mkdtempSync('/tmp/ql3-owner-readonly-'); + fs.rmSync(temporaryProbe, { recursive: true }); + assertWriteRejected(`/ql3-owner-readonly-root-${process.pid}`); + assertWriteRejected( + path.join(repositoryRoot, `.ql3-owner-readonly-workspace-${process.pid}`), + ); +} + +function main(argv = process.argv.slice(2)) { + const modeArgument = argv.find((value) => value.startsWith('--mode=')); + const mode = modeArgument?.slice('--mode='.length); + assertContainerBoundary(mode); + const plan = collectReadonlyOwnerTests(); + const result = spawnSync( + process.execPath, + ['--test', '--test-concurrency=1', ...plan.tests], + { cwd: repositoryRoot, stdio: 'inherit' }, + ); + if (result.error) throw result.error; + if (result.signal) { + throw new Error(`read-only Owner tests terminated by ${result.signal}`); + } + if (result.status !== 0) process.exitCode = result.status ?? 1; +} + +if (require.main === module) main(); + +module.exports = { + HOST_BOUND_TESTS, + collectReadonlyOwnerTests, +}; diff --git a/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs b/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs index 3e0b12ba..0685a8ac 100644 --- a/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs +++ b/scripts/ql3-plugin-package-recovery-e2e-live-contract.cjs @@ -31,6 +31,7 @@ const KIND_NODE_IMAGE = const POSTGRES_IMAGE = 'postgres:18.4-bookworm'; const POSTGRES_INDEX_DIGEST = 'sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296'; +const POSTGRES_IMAGE_REFERENCE = `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`; const POSTGRES_REPOSITORY_DIGEST = `postgres@${POSTGRES_INDEX_DIGEST}`; const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e'; const DEFAULT_CONTROL_IMAGE = @@ -277,13 +278,13 @@ function buildImages(revision) { } function ensurePostgresImage() { - if (!imageExists(POSTGRES_IMAGE)) { - run(DOCKER, ['pull', `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`], { + if (!imageExists(POSTGRES_IMAGE_REFERENCE)) { + run(DOCKER, ['pull', POSTGRES_IMAGE_REFERENCE], { label: 'pull digest-pinned PostgreSQL 18.4 fixture image', }); } const inspection = JSON.parse( - run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE], { + run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE_REFERENCE], { capture: true, quiet: true, }).stdout, @@ -485,7 +486,7 @@ createdb --username "$POSTGRES_USER" --owner ql3_migration qinglong containers: [ { name: 'postgres', - image: POSTGRES_IMAGE, + image: POSTGRES_IMAGE_REFERENCE, imagePullPolicy: 'Never', env: [ { name: 'POSTGRES_USER', value: 'postgres' }, @@ -1726,7 +1727,11 @@ async function main(argv = process.argv.slice(2)) { }, ); created = true; - for (const image of [ADMIN_IMAGE, CONTROL_IMAGE, POSTGRES_IMAGE]) { + for (const image of [ + ADMIN_IMAGE, + CONTROL_IMAGE, + POSTGRES_IMAGE_REFERENCE, + ]) { kind( [ 'load', diff --git a/scripts/ql3-postgres-ha-evidence-audit.cjs b/scripts/ql3-postgres-ha-evidence-audit.cjs index e9ec7380..29d85f1c 100644 --- a/scripts/ql3-postgres-ha-evidence-audit.cjs +++ b/scripts/ql3-postgres-ha-evidence-audit.cjs @@ -117,7 +117,7 @@ function auditPostgresHaEvidence(report) { 'POSTGRES_REPO_DIGESTS_MISSING', ); add( - postgres.architecture === 'x64' || postgres.architecture === 'arm64', + postgres.architecture === 'amd64' || postgres.architecture === 'arm64', 'POSTGRES_ARCHITECTURE_INVALID', ); add( diff --git a/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs b/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs index 84ad894e..4e779b35 100644 --- a/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs +++ b/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs @@ -81,6 +81,7 @@ const ROLE_NAMES = Object.freeze([ 'ql3_package_executor', 'ql3_automation_manager', 'ql3_approval_manager', + 'ql3_run_manager', 'ql3_worker_credential_manager', 'ql3_worker_credential_executor', 'ql3_worker_ingress', diff --git a/test/back/ql3CloudNativePgBarmanLiveContract.test.cjs b/test/back/ql3CloudNativePgBarmanLiveContract.test.cjs index d0aead2f..983c65cb 100644 --- a/test/back/ql3CloudNativePgBarmanLiveContract.test.cjs +++ b/test/back/ql3CloudNativePgBarmanLiveContract.test.cjs @@ -47,6 +47,7 @@ const POSTGRES_ROLES = [ 'ql3_migration', 'ql3_package_executor', 'ql3_package_manager', + 'ql3_run_manager', 'ql3_runtime', 'ql3_worker_credential_executor', 'ql3_worker_credential_manager', diff --git a/test/back/ql3ClusterOciLayoutAudit.test.cjs b/test/back/ql3ClusterOciLayoutAudit.test.cjs index ecb55d71..4108a243 100644 --- a/test/back/ql3ClusterOciLayoutAudit.test.cjs +++ b/test/back/ql3ClusterOciLayoutAudit.test.cjs @@ -248,18 +248,20 @@ function createFixture(t, options = {}) { const attestationLayers = options.omitProvenance ? [spdx] : [spdx, provenance]; - const attestationConfig = blob( - { - architecture: 'unknown', - os: 'unknown', - config: {}, - rootfs: { - type: 'layers', - diff_ids: attestationLayers.map((layer) => layer.digest), - }, - }, - 'application/vnd.oci.image.config.v1+json', - ); + const attestationConfig = options.modernAttestationConfig + ? blob({}, 'application/vnd.oci.empty.v1+json') + : blob( + { + architecture: 'unknown', + os: 'unknown', + config: {}, + rootfs: { + type: 'layers', + diff_ids: attestationLayers.map((layer) => layer.digest), + }, + }, + 'application/vnd.oci.image.config.v1+json', + ); const attestationManifest = blob( { schemaVersion: 2, @@ -325,6 +327,15 @@ test('accepts two exact images with bound SBOM and provenance', (t) => { ); }); +test('accepts OCI 1.1 empty attestation configs emitted by current BuildKit', (t) => { + const report = auditClusterOciLayout({ + root, + layoutRoot: createFixture(t, { modernAttestationConfig: true }), + expectedRevision: revision, + }); + assert.equal(report.platforms.length, 2); +}); + test('accepts the independent cluster-admin image and attestation closure', (t) => { const report = auditClusterOciLayout({ root, diff --git a/test/back/ql3LocalOwnerReadonlyContract.test.cjs b/test/back/ql3LocalOwnerReadonlyContract.test.cjs new file mode 100644 index 00000000..97b80f54 --- /dev/null +++ b/test/back/ql3LocalOwnerReadonlyContract.test.cjs @@ -0,0 +1,45 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { test } = require('node:test'); + +const { + HOST_BOUND_TESTS, + collectReadonlyOwnerTests, +} = require('../../scripts/ql3-local-owner-readonly-contract.cjs'); + +const repositoryRoot = path.resolve(__dirname, '../..'); +const workflowPath = path.join(repositoryRoot, '.github/workflows/ql3-ci.yml'); + +test('classifies only reviewed host-bound Owner tests outside the read-only gate', () => { + const plan = collectReadonlyOwnerTests(repositoryRoot); + assert.deepEqual(Object.keys(HOST_BOUND_TESTS).sort(), [ + 'packages/ql3-local-owner-cli/test/adoptedDeploymentBundle.test.cjs', + 'packages/ql3-local-owner-cli/test/localDeployment.test.cjs', + 'packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs', + 'packages/ql3-local-owner-cli/test/serviceBridgeRoot.test.cjs', + ]); + assert.equal(plan.tests.length > 0, true); + for (const file of plan.tests) { + assert.equal(fs.existsSync(path.join(repositoryRoot, file)), true); + assert.equal(file in HOST_BOUND_TESTS, false); + } +}); + +test('runs the reviewed Owner contract as root and non-root in read-only containers', () => { + const workflow = fs.readFileSync(workflowPath, 'utf8'); + const job = workflow.match( + / local-profiles:\n([\s\S]*?)\n cluster-postgres:/, + )?.[1]; + assert.ok(job, 'local-profiles job is missing'); + assert.equal(job.match(/docker run --rm --read-only/g)?.length, 2); + assert.equal( + job.match(/scripts\/ql3-local-owner-readonly-contract\.cjs/g)?.length, + 2, + ); + assert.match(job, /--mode=root/); + assert.match(job, /--user 65532:65532[\s\S]*--mode=nonroot/); + assert.doesNotMatch(job, /packages\/ql3-local-owner-\*\/test\/\*\.test\.cjs/); +}); diff --git a/test/back/ql3PostgresHaEvidenceAudit.test.cjs b/test/back/ql3PostgresHaEvidenceAudit.test.cjs index b82a88a0..d26782eb 100644 --- a/test/back/ql3PostgresHaEvidenceAudit.test.cjs +++ b/test/back/ql3PostgresHaEvidenceAudit.test.cjs @@ -6,7 +6,7 @@ const { auditPostgresHaEvidence, } = require('../../scripts/ql3-postgres-ha-evidence-audit.cjs'); -function fixture() { +function fixture(architecture = 'arm64') { const gates = { passed: true }; for (let index = 0; index < 100; index += 1) gates[`gate${index}`] = true; return { @@ -16,7 +16,7 @@ function fixture() { image: 'postgres:18', imageId: `sha256:${'a'.repeat(64)}`, repoDigests: [`postgres@sha256:${'b'.repeat(64)}`], - architecture: 'arm64', + architecture, version: '18.4', versionNumber: 180004, }, @@ -85,6 +85,7 @@ test('accepts complete PostgreSQL HA evidence', () => { compatible: true, findings: [], }); + assert.equal(auditPostgresHaEvidence(fixture('amd64')).compatible, true); }); test('rejects false gates, promotion drift and hidden private material', () => { diff --git a/test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs b/test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs index 9b568bbb..4e90685a 100644 --- a/test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs +++ b/test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs @@ -338,6 +338,7 @@ function publisherProvenanceInstallRepository(repository, provenance) { findLock: (...args) => repository.findLock(...args), create: (...args) => repository.create(...args), listRecoveryPage: (...args) => repository.listRecoveryPage(...args), + listCurrentPage: (...args) => repository.listCurrentPage(...args), async commit(command) { if (command.record.state !== 'staged') { return repository.commit(command);