test(ql3): accept local cancellation live gate

This commit is contained in:
whyour
2026-08-12 20:22:41 +08:00
parent d85cd88c2d
commit bd0621c9f2
12 changed files with 1311 additions and 22 deletions
@@ -0,0 +1,706 @@
#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const http = require('node:http');
const net = require('node:net');
const path = require('node:path');
const { createRequire } = require('node:module');
const { spawn } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const TIMEOUT_MS = 45_000;
function fail(message) {
throw new Error(
`QingLong Local API cancellation scenario failed: ${message}`,
);
}
function options(argv) {
if (argv.length !== 3)
fail('usage: artifact-root evidence-root edge|standalone');
const [artifactRoot, evidenceRoot, profile] = argv;
for (const [value, label] of [
[artifactRoot, 'artifact root'],
[evidenceRoot, 'evidence root'],
]) {
if (!path.isAbsolute(value) || path.normalize(value) !== value) {
fail(`${label} must be absolute and normalized`);
}
}
if (!['edge', 'standalone'].includes(profile)) fail('profile is invalid');
return Object.freeze({ artifactRoot, evidenceRoot, profile });
}
function privateDirectory(directory) {
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
fs.chmodSync(directory, 0o700);
const stat = fs.lstatSync(directory);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
(stat.mode & 0o777) !== 0o700
) {
fail(`private directory is invalid: ${directory}`);
}
}
function privateJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, {
flag: 'wx',
mode: 0o600,
});
}
function procStartTicks(pid) {
const fields = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').trim().split(' ');
const ticks = fields[21];
if (!/^[1-9][0-9]*$/.test(ticks ?? ''))
fail('process start ticks are invalid');
return ticks;
}
function sameProcessExists(pid, startTicks) {
try {
return procStartTicks(pid) === startTicks;
} catch (error) {
if (error?.code === 'ENOENT' || error?.code === 'ESRCH') return false;
throw error;
}
}
function rssBytes(pid) {
const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
const match = /^VmRSS:\s+(\d+) kB$/m.exec(status);
if (!match) fail('process RSS is unavailable');
return Number(match[1]) * 1024;
}
function request(port, token, requestPath, values = {}) {
return new Promise((resolve, reject) => {
const outgoing = http.request(
{
host: '127.0.0.1',
port,
path: requestPath,
method: values.method ?? 'GET',
headers: {
authorization: `Bearer ${token}`,
connection: 'close',
...(values.headers ?? {}),
},
},
(response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
try {
resolve({
statusCode: response.statusCode,
body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
});
} catch (error) {
reject(error);
}
});
},
);
outgoing.once('error', reject);
if (values.body !== undefined) outgoing.write(values.body);
outgoing.end();
});
}
function reservePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
server.close((error) => {
if (error) reject(error);
else if (!address || typeof address === 'string') {
reject(new Error('dynamic loopback port is unavailable'));
} else resolve(address.port);
});
});
});
}
async function waitFor(probe, label) {
const deadline = Date.now() + TIMEOUT_MS;
let lastError;
while (Date.now() < deadline) {
try {
const value = await probe();
if (value) return value;
} catch (error) {
lastError = error;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
fail(`${label} did not converge${lastError ? `: ${lastError.message}` : ''}`);
}
function query(databasePath, sql, ...parameters) {
const database = new DatabaseSync(databasePath, {
readOnly: true,
timeout: 100,
});
try {
return database.prepare(sql).get(...parameters);
} finally {
database.close();
}
}
function insertTaskAndIdentity(
modules,
databasePath,
pepperKeyId,
pepper,
now,
) {
const {
apiCredentialSecretDigest,
createBuiltInTaskSpecSemanticRegistry,
createTaskDefinitionRecord,
compileLocalCommandTaskDefinition,
formatApiCredentialToken,
} = modules;
const credentialId = 'local-live-operator';
const subjectId = 'local-live-user';
const secret = crypto.randomBytes(32).toString('base64url');
const taskSemantics = createBuiltInTaskSpecSemanticRegistry();
const taskCommand = {
projectId: 'default',
taskId: 'live-cancellation-task',
expectedRevision: null,
mutationId: '019f8700-0000-7000-8000-000000000001',
name: 'Local API cancellation live task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/sh',
args: ['-c', 'trap "exit 0" TERM INT; while :; do sleep 1; done'],
},
},
},
labels: {},
enabled: true,
occurredAtMs: now,
};
const definition = createTaskDefinitionRecord(
{
...taskCommand,
spec: taskSemantics.normalize({
projectId: taskCommand.projectId,
taskId: taskCommand.taskId,
kind: taskCommand.kind,
spec: taskCommand.spec,
}),
},
now,
);
const execution = compileLocalCommandTaskDefinition(
definition,
taskSemantics,
);
const database = new DatabaseSync(databasePath, { timeout: 100 });
try {
database.exec('PRAGMA foreign_keys = ON; BEGIN IMMEDIATE');
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', ?, 'active', 1, ?, ?)`,
)
.run(subjectId, now, now);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentials" (
"credential_id", "version", "state", "subject_type", "subject_id",
"secret_digest", "created_at_ms", "not_before_at_ms", "expires_at_ms"
) VALUES (?, 1, 'active', 'user', ?, ?, ?, ?, ?)`,
)
.run(
credentialId,
subjectId,
apiCredentialSecretDigest(pepper, credentialId, secret),
now,
now,
now + 3_600_000,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(credentialId, pepperKeyId);
database
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', ?, 1, 'active', 'operator', ?, 'system',
'local-live-gate', ?)`,
)
.run(subjectId, 'local-live-role-binding', now);
database
.prepare(
`INSERT INTO "QingLong3TaskDefinitions" (
"project_id", "task_id", "current_revision", "created_at_ms",
"updated_at_ms"
) VALUES (?, ?, ?, ?, ?)`,
)
.run(
definition.projectId,
definition.taskId,
definition.revision,
now,
now,
);
database
.prepare(
`INSERT INTO "QingLong3TaskDefinitionRevisions" (
"project_id", "task_id", "revision", "mutation_id", "name",
"description", "kind", "spec_json", "labels_json", "enabled",
"content_digest", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, 1, ?, ?)`,
)
.run(
definition.projectId,
definition.taskId,
definition.revision,
definition.mutationId,
definition.name,
definition.kind,
JSON.stringify(definition.spec),
JSON.stringify(definition.labels),
definition.contentDigest,
now,
);
database
.prepare(
`INSERT INTO "QingLong3LocalExecutionContextRecipes" (
"context_ref", "environment_json", "content_digest", "created_at_ms"
) VALUES (?, ?, ?, ?)`,
)
.run(
execution.contextRecipe.contextRef,
JSON.stringify(execution.contextRecipe.environment),
execution.contextRecipe.contentDigest,
execution.contextRecipe.createdAtMs,
);
database
.prepare(
`INSERT INTO "QingLong3LocalTaskExecutionRevisions" (
"project_id", "task_id", "task_revision", "executor_type",
"command_json", "working_directory", "timeout_ms", "context_ref",
"content_digest", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
execution.executionRevision.projectId,
execution.executionRevision.taskId,
execution.executionRevision.taskRevision,
execution.executionRevision.executorType,
JSON.stringify(execution.executionRevision.command),
execution.executionRevision.workingDirectory ?? null,
execution.executionRevision.timeoutMs ?? null,
execution.executionRevision.contextRef,
execution.executionRevision.contentDigest,
execution.executionRevision.createdAtMs,
);
database.exec('COMMIT');
} catch (error) {
if (database.isTransaction) database.exec('ROLLBACK');
throw error;
} finally {
database.close();
}
return Object.freeze({
definition,
token: formatApiCredentialToken(credentialId, secret),
});
}
function startApi(executable, configPath) {
const child = spawn(process.execPath, [executable, '--config', configPath], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, NODE_ENV: 'production' },
});
const events = [];
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout += chunk;
while (stdout.includes('\n')) {
const index = stdout.indexOf('\n');
const line = stdout.slice(0, index);
stdout = stdout.slice(index + 1);
if (line) events.push(JSON.parse(line));
}
});
child.stderr.on('data', (chunk) => {
stderr = (stderr + chunk).slice(-16_384);
});
const exit = new Promise((resolve, reject) => {
child.once('error', reject);
child.once('exit', (code, signal) => resolve({ code, signal }));
});
return Object.freeze({ child, events, exit, stderr: () => stderr });
}
async function stopApi(active) {
active.child.kill('SIGTERM');
const outcome = await Promise.race([
active.exit,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('API stop timeout')), 30_000),
),
]);
if (outcome.code !== 0 || outcome.signal !== null) {
fail(
`API process did not stop cleanly: ${JSON.stringify({
...outcome,
stderr: active.stderr(),
})}`,
);
}
return outcome;
}
async function main(argv = process.argv.slice(2)) {
const value = options(argv);
if (process.platform !== 'linux' || !fs.existsSync('/proc/self/stat')) {
fail('a real Linux /proc runtime is required');
}
privateDirectory(value.evidenceRoot);
const deploymentRoot = path.join(value.evidenceRoot, 'deployment');
for (const directory of [
deploymentRoot,
path.join(deploymentRoot, 'owner-peppers'),
path.join(deploymentRoot, 'receipts'),
path.join(deploymentRoot, 'artifacts'),
path.join(deploymentRoot, 'plugin-staging'),
path.join(deploymentRoot, 'plugin-activation'),
])
privateDirectory(directory);
const artifactRequire = createRequire(
path.join(value.artifactRoot, 'package.json'),
);
const { migrateLocalSqlitePath } = artifactRequire(
'@qinglong/local-sqlite/migration',
);
const { LocalOwnerPepperKeyringFileProvider, provisionLocalOwnerPepperKey } =
artifactRequire('@qinglong/local-owner-console/pepper-custody');
const { provisionLocalSecretKeyring } = artifactRequire(
'@qinglong/local-secret',
);
const tokenModule = artifactRequire(
'@qinglong/runtime-core/api-credential-token',
);
const definitionModule = artifactRequire(
'@qinglong/runtime-core/task-definition',
);
const compilerModule = artifactRequire(
'@qinglong/runtime-core/task-definition-execution-compiler',
);
const semanticModule = artifactRequire(
'@qinglong/runtime-core/task-spec-semantic',
);
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const pepperKeyId = 'owner-v1';
const now = Date.now();
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: path.join(deploymentRoot, 'owner-peppers'),
pepperKeyId,
});
await migrateLocalSqlitePath({ databasePath, profile: value.profile });
await provisionLocalSecretKeyring(
path.join(deploymentRoot, 'local-secret-keyring.json'),
);
const database = new DatabaseSync(databasePath);
try {
database
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
"pepper_key_id", "material_digest", "backup_digest", "state",
"version", "register_mutation_id", "activate_mutation_id",
"registered_at_ms", "activated_at_ms"
) VALUES (?, ?, ?, 'active', 2, ?, ?, ?, ?)`,
)
.run(
pepperKeyId,
pepperSummary.digest,
'b'.repeat(64),
'019f8700-0000-4000-8000-000000000002',
'019f8700-0000-4000-8000-000000000003',
now,
now,
);
database
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
"generation", "mutation_id", "expected_generation",
"previous_pepper_key_id", "active_pepper_key_id", "material_digest",
"backup_digest", "activated_at_ms"
) VALUES (1, ?, 0, NULL, ?, ?, ?, ?)`,
)
.run(
'019f8700-0000-4000-8000-000000000003',
pepperKeyId,
pepperSummary.digest,
'b'.repeat(64),
now,
);
} finally {
database.close();
}
const pepper = new LocalOwnerPepperKeyringFileProvider(
path.join(deploymentRoot, 'owner-peppers'),
).resolve(pepperKeyId).pepper;
const seeded = insertTaskAndIdentity(
{
...tokenModule,
...definitionModule,
...compilerModule,
...semanticModule,
},
databasePath,
pepperKeyId,
pepper,
now,
);
const port = await reservePort();
const applicationConfigPath = path.join(
deploymentRoot,
'local-application.json',
);
const apiConfigPath = path.join(deploymentRoot, 'local-api.json');
privateJson(applicationConfigPath, {
schema: 'qinglong/local-application-process@v2',
instanceId: `local-api-cancellation-${value.profile}`,
profile: value.profile,
storage: { mode: 'fresh', databasePath, busyTimeoutMs: 100 },
runtime: {
receiptRoot: path.join(deploymentRoot, 'receipts'),
artifactRoot: path.join(deploymentRoot, 'artifacts'),
secretKeyringPath: path.join(deploymentRoot, 'local-secret-keyring.json'),
},
pluginPackages: {
stagingRoot: path.join(deploymentRoot, 'plugin-staging'),
activationRoot: path.join(deploymentRoot, 'plugin-activation'),
recoverySource: { mode: 'disabled' },
pageSize: value.profile === 'edge' ? 4 : 16,
maxPages: 1,
taskPublicationPageSize: value.profile === 'edge' ? 4 : 16,
taskPublicationMaxPages: 1,
},
ai: { deployment: 'excluded' },
});
privateJson(apiConfigPath, {
schema: 'qinglong/local-api-process@v1',
deploymentRoot,
applicationConfigFilePath: applicationConfigPath,
ownerPepperKeyringDirectory: path.join(deploymentRoot, 'owner-peppers'),
listener: { host: '127.0.0.1', port },
});
const executable = path.join(
value.artifactRoot,
'node_modules/@qinglong/local-api/dist/cli.js',
);
let active = startApi(executable, apiConfigPath);
try {
await waitFor(
() => active.events.some((event) => event.event === 'listening'),
'Local API listener',
);
const startBody = JSON.stringify({
schema: 'qinglong/task-start@v1',
mutationId: '019f8700-0000-7000-8000-000000000004',
expectedRevision: seeded.definition.revision,
expectedContentDigest: seeded.definition.contentDigest,
});
const started = await request(
port,
seeded.token,
'/api/v3/projects/default/tasks/live-cancellation-task/runs',
{
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(startBody)),
},
body: startBody,
},
);
if (started.statusCode !== 202 || started.body.status !== 'accepted') {
fail(`task start was rejected: ${JSON.stringify(started)}`);
}
const running = await waitFor(() => {
const row = query(
databasePath,
`SELECT run.status, attempt.status AS attemptStatus, attempt.pid
FROM Runs AS run JOIN RunAttempts AS attempt ON attempt.run_id = run.id
WHERE run.id = ?`,
started.body.runId,
);
return row?.status === 'running' &&
row?.attemptStatus === 'running' &&
row?.pid
? row
: null;
}, 'task process start');
const taskPid = Number(running.pid);
const taskStartTicks = procStartTicks(taskPid);
const apiRssBytes = rssBytes(active.child.pid);
const cancellationBody = JSON.stringify({
schema: 'qinglong/run-cancellation@v1',
mutationId: 'local-live-cancellation-1',
});
const cancellationPath = `/api/v3/projects/default/runs/${started.body.runId}/cancellation`;
const requestOptions = {
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(cancellationBody)),
},
body: cancellationBody,
};
const accepted = await request(
port,
seeded.token,
cancellationPath,
requestOptions,
);
const replay = await request(
port,
seeded.token,
cancellationPath,
requestOptions,
);
if (accepted.statusCode !== 202 || accepted.body.status !== 'accepted') {
fail(`cancellation was rejected: ${JSON.stringify(accepted)}`);
}
if (
replay.statusCode !== 200 ||
replay.body.status !== 'already_requested'
) {
fail(`cancellation replay drifted: ${JSON.stringify(replay)}`);
}
const terminal = await waitFor(() => {
const row = query(
databasePath,
`SELECT run.status, attempt.status AS attemptStatus
FROM Runs AS run JOIN RunAttempts AS attempt ON attempt.run_id = run.id
WHERE run.id = ?`,
started.body.runId,
);
return row?.status === 'cancelled' && row?.attemptStatus === 'cancelled'
? row
: null;
}, 'durable cancellation');
await waitFor(
() => !sameProcessExists(taskPid, taskStartTicks),
'task process identity exit',
);
await stopApi(active);
active = startApi(executable, apiConfigPath);
await waitFor(
() => active.events.some((event) => event.event === 'listening'),
'restarted Local API listener',
);
const observed = await request(
port,
seeded.token,
`/api/v3/projects/default/runs/${started.body.runId}`,
);
if (
observed.statusCode !== 200 ||
observed.body.run.status !== 'cancelled'
) {
fail(`restart observation drifted: ${JSON.stringify(observed)}`);
}
await stopApi(active);
const facts = query(
databasePath,
`SELECT
(SELECT COUNT(*) FROM RunEvents WHERE run_id = ? AND type = 'run.cancel_requested') AS cancelEvents,
(SELECT COUNT(*) FROM RunEvents WHERE run_id = ? AND type = 'run.cancelled') AS cancelledEvents,
(SELECT COUNT(*) FROM QingLong3SecurityAuditEvents WHERE operation_id = 'run.cancel' AND outcome = 'allowed') AS cancelAudits,
(SELECT integrity_check FROM pragma_integrity_check LIMIT 1) AS integrity`,
started.body.runId,
started.body.runId,
);
if (
facts.cancelEvents !== 1 ||
facts.cancelledEvents !== 1 ||
facts.cancelAudits !== 2 ||
facts.integrity !== 'ok'
)
fail(`durable facts drifted: ${JSON.stringify(facts)}`);
const report = {
schemaVersion: 1,
profile: value.profile,
platform: { os: 'linux', architecture: process.arch, procfs: true },
resourceEnvelope: {
memoryBytes:
value.profile === 'edge' ? 128 * 1024 * 1024 : 256 * 1024 * 1024,
pids: value.profile === 'edge' ? 64 : 256,
apiRssBytes,
},
observations: {
taskStartAccepted: true,
cancellationAccepted: true,
exactReplay: true,
durableIntentEvents: facts.cancelEvents,
durableCancellationEvents: facts.cancelledEvents,
durableAllowedAudits: facts.cancelAudits,
processIdentityObserved: true,
processIdentityGone: !sameProcessExists(taskPid, taskStartTicks),
restartObservedCancelled: true,
sqliteIntegrity: facts.integrity,
},
qualification: {
evidenceClass: 'linux_virtualized_live_contract',
physicalDevice: false,
passed: true,
},
compatible: terminal.status === 'cancelled',
};
privateJson(path.join(value.evidenceRoot, 'report.json'), report);
process.stdout.write(`${JSON.stringify(report)}\n`);
} finally {
if (active.child.exitCode === null && active.child.signalCode === null) {
active.child.kill('SIGKILL');
await active.exit.catch(() => undefined);
}
}
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(
`${
error instanceof Error ? error.stack || error.message : String(error)
}\n`,
);
process.exitCode = 1;
});
}
module.exports = { options };
@@ -0,0 +1,164 @@
#!/usr/bin/env node
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const PROFILES = new Set(['edge', 'standalone']);
function exactKeys(value, expected) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value).sort()) ===
JSON.stringify([...expected].sort())
);
}
function validateLocalApiCancellationLiveReport(value) {
const findings = [];
const record = exactKeys(value, [
'artifact',
'compatible',
'observations',
'platform',
'profile',
'qualification',
'resourceEnvelope',
'schemaVersion',
]);
if (!record || value.schemaVersion !== 1 || !PROFILES.has(value.profile)) {
return Object.freeze({
compatible: false,
findings: ['report identity is invalid'],
});
}
const expectedMemory =
value.profile === 'edge' ? 128 * 1024 * 1024 : 256 * 1024 * 1024;
const expectedPids = value.profile === 'edge' ? 64 : 256;
if (
!exactKeys(value.platform, ['architecture', 'os', 'procfs']) ||
value.platform?.os !== 'linux' ||
!['arm64', 'x64'].includes(value.platform?.architecture) ||
value.platform?.procfs !== true
)
findings.push('Linux /proc platform observation is invalid');
if (
!exactKeys(value.resourceEnvelope, [
'apiRssBytes',
'memoryBytes',
'pids',
]) ||
value.resourceEnvelope?.memoryBytes !== expectedMemory ||
value.resourceEnvelope?.pids !== expectedPids ||
!Number.isSafeInteger(value.resourceEnvelope?.apiRssBytes) ||
value.resourceEnvelope.apiRssBytes < 1 ||
value.resourceEnvelope.apiRssBytes > expectedMemory
)
findings.push('resource envelope is invalid');
if (
!exactKeys(value.artifact, [
'bytes',
'compatible',
'files',
'loadedModules',
'profile',
]) ||
value.artifact?.profile !== `${value.profile}-application-api` ||
!Number.isSafeInteger(value.artifact?.bytes) ||
value.artifact.bytes < 1 ||
value.artifact.bytes > 6 * 1024 * 1024 ||
!Number.isSafeInteger(value.artifact?.files) ||
value.artifact.files < 1 ||
value.artifact.files > 640 ||
!Number.isSafeInteger(value.artifact?.loadedModules) ||
value.artifact.loadedModules < 1 ||
value.artifact.loadedModules > 256 ||
value.artifact.compatible !== true
)
findings.push('optional API artifact evidence is invalid');
const observed = value.observations;
if (
!exactKeys(observed, [
'cancellationAccepted',
'durableAllowedAudits',
'durableCancellationEvents',
'durableIntentEvents',
'exactReplay',
'processIdentityGone',
'processIdentityObserved',
'restartObservedCancelled',
'sqliteIntegrity',
'taskStartAccepted',
]) ||
observed?.taskStartAccepted !== true ||
observed?.cancellationAccepted !== true ||
observed?.exactReplay !== true ||
observed?.durableIntentEvents !== 1 ||
observed?.durableCancellationEvents !== 1 ||
observed?.durableAllowedAudits !== 2 ||
observed?.processIdentityObserved !== true ||
observed?.processIdentityGone !== true ||
observed?.restartObservedCancelled !== true ||
observed?.sqliteIntegrity !== 'ok'
)
findings.push('API to durable process-stop observations are incomplete');
if (
!exactKeys(value.qualification, [
'evidenceClass',
'passed',
'physicalDevice',
]) ||
value.qualification?.evidenceClass !== 'linux_virtualized_live_contract' ||
value.qualification?.physicalDevice !== false ||
value.qualification?.passed !== true
)
findings.push('qualification boundary is invalid');
if (value.compatible !== true) findings.push('report is not compatible');
return Object.freeze({
compatible: findings.length === 0,
findings: Object.freeze(findings),
});
}
function reportPath(argv) {
if (
argv.length !== 1 ||
!argv[0].startsWith('--report=') ||
!path.isAbsolute(argv[0].slice('--report='.length))
)
throw new Error(
'usage: ql3-local-api-cancellation-live-audit --report=/absolute/private-report.json',
);
return argv[0].slice('--report='.length);
}
if (require.main === module) {
try {
const filePath = reportPath(process.argv.slice(2));
const stat = fs.lstatSync(filePath);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
(typeof process.getuid === 'function' && stat.uid !== process.getuid()) ||
(stat.mode & 0o077) !== 0 ||
stat.size > 32 * 1024
) {
throw new Error('report must be a private bounded regular file');
}
const audit = validateLocalApiCancellationLiveReport(
JSON.parse(fs.readFileSync(filePath, 'utf8')),
);
process.stdout.write(`${JSON.stringify(audit)}\n`);
if (!audit.compatible) process.exitCode = 1;
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
}
}
module.exports = { validateLocalApiCancellationLiveReport };
@@ -0,0 +1,175 @@
#!/usr/bin/env node
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const {
validateLocalApiCancellationLiveReport,
} = require('./ql3-local-api-cancellation-live-audit.cjs');
const ROOT = path.resolve(__dirname, '..');
const NODE_IMAGE =
'node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d';
function fail(message) {
throw new Error(
`QingLong Local API cancellation live contract failed: ${message}`,
);
}
function argumentsOf(argv) {
if (argv.length !== 2)
fail(
'usage: --profile=edge|standalone --report=/absolute/private-report.json',
);
const values = Object.fromEntries(
argv.map((argument) => {
const match = /^--(profile|report)=(.+)$/.exec(argument);
if (!match) fail(`unsupported argument ${argument}`);
return [match[1], match[2]];
}),
);
if (!['edge', 'standalone'].includes(values.profile))
fail('profile is invalid');
if (
!path.isAbsolute(values.report ?? '') ||
path.normalize(values.report) !== values.report ||
path.parse(values.report).root === values.report ||
fs.existsSync(values.report)
)
fail('report must be a fresh normalized absolute non-root path');
const parent = fs.lstatSync(path.dirname(values.report));
if (!parent.isDirectory() || parent.isSymbolicLink())
fail('report parent must be a real directory');
return Object.freeze(values);
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: ROOT,
encoding: 'utf8',
maxBuffer: 4 * 1024 * 1024,
...options,
});
if (result.error) throw result.error;
if (result.status !== 0) {
fail(
`${command} ${args[0]} failed: ${(result.stderr || result.stdout)
.trim()
.slice(0, 4096)}`,
);
}
return result.stdout.trim();
}
function main(argv = process.argv.slice(2)) {
const selected = argumentsOf(argv);
if (process.env.QL3_LOCAL_API_CANCELLATION_LIVE !== '1') {
fail('refusing to run Docker without QL3_LOCAL_API_CANCELLATION_LIVE=1');
}
const temporaryRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-api-cancel-live-')),
);
fs.chmodSync(temporaryRoot, 0o700);
const artifactRoot = path.join(temporaryRoot, 'artifact');
const evidenceRoot = path.join(temporaryRoot, 'evidence');
fs.mkdirSync(evidenceRoot, { mode: 0o700 });
try {
const artifactOutput = run(process.execPath, [
path.join(ROOT, 'scripts/ql3-local-profile-artifact-audit.cjs'),
`${selected.profile}-application-api`,
`--output-directory=${artifactRoot}`,
]);
const artifact = JSON.parse(artifactOutput.split(/\r?\n/).at(-1));
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
const gid = typeof process.getgid === 'function' ? process.getgid() : null;
if (!Number.isSafeInteger(uid) || !Number.isSafeInteger(gid))
fail('a POSIX identity is required');
const memory = selected.profile === 'edge' ? '128m' : '256m';
const pids = selected.profile === 'edge' ? '64' : '256';
run('docker', [
'run',
'--rm',
'--read-only',
'--user',
`${uid}:${gid}`,
'--network',
'none',
'--cap-drop',
'ALL',
'--security-opt',
'no-new-privileges',
'--memory',
memory,
'--memory-swap',
memory,
'--cpus',
'0.5',
'--pids-limit',
pids,
'--tmpfs',
'/tmp:rw,nosuid,nodev,noexec,size=16m',
'--volume',
`${artifactRoot}:/opt/ql3-artifact:ro`,
'--volume',
`${path.join(ROOT, 'scripts')}:/opt/ql3-scripts:ro`,
'--volume',
`${evidenceRoot}:/evidence`,
NODE_IMAGE,
'node',
'/opt/ql3-scripts/lib/ql3-local-api-cancellation-live-scenario.cjs',
'/opt/ql3-artifact',
'/evidence',
selected.profile,
]);
const scenario = JSON.parse(
fs.readFileSync(path.join(evidenceRoot, 'report.json'), 'utf8'),
);
const report = Object.freeze({
...scenario,
artifact: Object.freeze({
profile: artifact.profile,
bytes: artifact.artifactBytes,
files: artifact.artifactFiles,
loadedModules: artifact.loadedModuleCount,
compatible: artifact.compatible,
}),
});
const audit = validateLocalApiCancellationLiveReport(report);
assert.deepEqual(audit.findings, []);
fs.writeFileSync(selected.report, `${JSON.stringify(report, null, 2)}\n`, {
flag: 'wx',
mode: 0o600,
});
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
profile: selected.profile,
reportWritten: true,
compatible: true,
})}\n`,
);
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${
error instanceof Error ? error.stack || error.message : String(error)
}\n`,
);
process.exitCode = 1;
}
}
module.exports = { argumentsOf };
+2 -2
View File
@@ -284,8 +284,8 @@ function auditWorkflow(contents, findings) {
'scripts/ql3-local-image-inventory.cjs',
'--inventory-root=/opt/qinglong/node_modules',
'node ../../scripts/ql3-build-package-closure.cjs',
'--profile=edge',
'--profile=standalone',
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=edge',
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=standalone',
];
for (const value of required) {
if (!job.includes(value)) {
+59 -17
View File
@@ -48,6 +48,35 @@ function boundedPositiveInteger(value, fallback, label) {
return normalized;
}
function outputDirectoryArgument(value) {
if (value === undefined) return undefined;
if (
value.length < 1 ||
Buffer.byteLength(value, 'utf8') > 4_096 ||
value.includes('\0') ||
!path.isAbsolute(value) ||
path.normalize(value) !== value ||
path.parse(value).root === value
) {
fail(
'output directory must be a normalized bounded absolute non-root path',
);
}
if (fs.existsSync(value)) {
fail('output directory must not already exist');
}
const parent = path.dirname(value);
const stat = fs.lstatSync(parent);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
fs.realpathSync(parent) !== parent
) {
fail('output directory parent must be a canonical real directory');
}
return value;
}
function parseArguments(argv) {
const profile = argv[0];
if (
@@ -70,13 +99,23 @@ function parseArguments(argv) {
'Profile must be edge, standalone, an adopted/application variant, an AI/API variant, or an MCP variant',
);
}
const values = Object.fromEntries(
argv.slice(1).map((argument) => {
const match = /^--([a-z-]+)=(\d+)$/.exec(argument);
if (!match) fail(`unsupported argument ${argument}`);
return [match[1], match[2]];
}),
);
const values = {};
for (const argument of argv.slice(1)) {
const match = /^--([a-z-]+)=(.+)$/.exec(argument);
if (!match || Object.hasOwn(values, match[1])) {
fail(`unsupported argument ${argument}`);
}
values[match[1]] = match[2];
}
const supported = new Set([
'max-artifact-files',
'max-artifact-bytes',
'max-rss-delta-bytes',
'output-directory',
]);
for (const name of Object.keys(values)) {
if (!supported.has(name)) fail(`unsupported argument --${name}`);
}
return Object.freeze({
profile,
runtimeProfile: profile.startsWith('edge') ? 'edge' : 'standalone',
@@ -128,6 +167,7 @@ function parseArguments(argv) {
profile.includes('-application') && profile.endsWith('-ai')
? MIN_APPLICATION_AI_ARTIFACT_HEADROOM_BYTES
: 0,
outputDirectory: outputDirectoryArgument(values['output-directory']),
});
}
@@ -221,8 +261,7 @@ function auditImportClosure(
}
const forbidden = result.loaded.filter(
(filePath) =>
(!allowSemver &&
/node_modules[\\/]semver(?:[\\/]|$)/i.test(filePath)) ||
(!allowSemver && /node_modules[\\/]semver(?:[\\/]|$)/i.test(filePath)) ||
/(?:node_modules[\\/](?:croner|pg|drizzle-orm|sequelize|sqlite3)(?:[\\/]|$)|node_modules[\\/]@qinglong[\\/]cluster-|local-sqlite[\\/]dist[\\/]migration\.js$|local-sqlite[\\/]dist[\\/]migrations[\\/])/i.test(
filePath,
),
@@ -309,8 +348,7 @@ function auditLocalApiExecutable(artifactDirectory) {
artifactDirectory,
);
if (
output !==
'Usage: ql3-local-api --config /absolute/private-config.json'
output !== 'Usage: ql3-local-api --config /absolute/private-config.json'
) {
fail('local API executable help output is invalid');
}
@@ -670,6 +708,13 @@ function main() {
`import RSS delta is ${closure.rssDeltaBytes} bytes, budget is ${options.maxRssDeltaBytes}`,
);
}
if (options.outputDirectory !== undefined) {
fs.cpSync(artifactDirectory, options.outputDirectory, {
recursive: true,
errorOnExist: true,
force: false,
});
}
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
@@ -680,8 +725,7 @@ function main() {
maxArtifactFiles: options.maxArtifactFiles,
maxArtifactBytes: options.maxArtifactBytes,
artifactHeadroomBytes,
minimumArtifactHeadroomBytes:
options.minimumArtifactHeadroomBytes,
minimumArtifactHeadroomBytes: options.minimumArtifactHeadroomBytes,
prunedRuntimeDevelopmentFiles: prunedRuntimeArtifact.development.files,
prunedRuntimeDevelopmentBytes: prunedRuntimeArtifact.development.bytes,
runtimeJavaScriptFilesBefore:
@@ -716,10 +760,8 @@ function main() {
prunedRuntimeArtifact.packageManifests.runtimeExports
.excludedSpecifiers,
prunedRuntimeArtifactBytes: prunedRuntimeArtifact.savedBytes,
prunedMcpExternalDevelopmentFiles:
prunedMcpExternalDevelopment.files,
prunedMcpExternalDevelopmentBytes:
prunedMcpExternalDevelopment.bytes,
prunedMcpExternalDevelopmentFiles: prunedMcpExternalDevelopment.files,
prunedMcpExternalDevelopmentBytes: prunedMcpExternalDevelopment.bytes,
loadedModuleCount: closure.loadedModuleCount,
rssDeltaBytes: closure.rssDeltaBytes,
maxRssDeltaBytes: options.maxRssDeltaBytes,