Files
qinglong/scripts/ql3-local-alpha-trial-kit-live-contract.cjs
T

825 lines
24 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawn, spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const MAX_OUTPUT_BYTES = 64 * 1024;
const ACTIVE_TIMEOUT_MS = 45_000;
const IMAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,511}$/;
function fail(message) {
throw new Error(`QingLong Local Alpha trial kit failed: ${message}`);
}
function argumentsFrom(argv) {
const values = {};
for (const argument of argv) {
const match =
/^--(application-image|operator-image|profile|variant)=(.+)$/u.exec(
argument,
);
if (!match || Object.hasOwn(values, match[1]))
fail('arguments are invalid');
values[match[1]] = match[2];
}
if (
Object.keys(values).length !== 4 ||
!IMAGE_PATTERN.test(values['application-image'] ?? '') ||
!IMAGE_PATTERN.test(values['operator-image'] ?? '') ||
!['edge', 'standalone'].includes(values.profile) ||
!['headless', 'console'].includes(values.variant)
) {
fail(
'usage: --application-image=... --operator-image=... --profile=edge|standalone --variant=headless|console',
);
}
return Object.freeze({
applicationImage: values['application-image'],
operatorImage: values['operator-image'],
profile: values.profile,
variant: values.variant,
});
}
function docker(args, options = {}) {
const result = spawnSync('docker', args, {
encoding: 'utf8',
maxBuffer: MAX_OUTPUT_BYTES,
...options,
});
if (result.error) throw result.error;
if (result.status !== 0) {
fail(
`docker ${args[0]} failed: ${(result.stderr || result.stdout)
.trim()
.slice(0, 2048)}`,
);
}
return result.stdout.trim();
}
function inspectImages(applicationImage, operatorImage, variant) {
const application = JSON.parse(
docker(['image', 'inspect', applicationImage]),
)[0];
const operator = JSON.parse(docker(['image', 'inspect', operatorImage]))[0];
const applicationLabels = application?.Config?.Labels;
const operatorLabels = operator?.Config?.Labels;
const expectedTitle =
variant === 'console'
? 'QingLong 3.0 Local Console Application'
: 'QingLong 3.0 Local Application';
const expectedProfile =
variant === 'console'
? 'edge-application-api,standalone-application-api'
: 'edge,standalone';
if (
!/^sha256:[0-9a-f]{64}$/u.test(application?.Id ?? '') ||
!/^sha256:[0-9a-f]{64}$/u.test(operator?.Id ?? '') ||
application?.Architecture !== operator?.Architecture ||
!['amd64', 'arm64'].includes(application?.Architecture) ||
application?.Config?.User !== '65532:65532' ||
operator?.Config?.User !== '65532:65532' ||
applicationLabels?.['org.opencontainers.image.title'] !== expectedTitle ||
applicationLabels?.['io.qinglong.profile'] !== expectedProfile ||
applicationLabels?.['io.qinglong.ai'] !== 'excluded' ||
(variant === 'console'
? applicationLabels?.['io.qinglong.local.console'] !== 'offline-loopback'
: applicationLabels?.['io.qinglong.local.console'] !== undefined) ||
operatorLabels?.['io.qinglong.lifecycle'] !== 'short-lived' ||
operatorLabels?.['io.qinglong.authority'] !== 'local-owner-management' ||
!/^[0-9a-f]{40}$/u.test(
applicationLabels?.['org.opencontainers.image.revision'] ?? '',
) ||
applicationLabels?.['org.opencontainers.image.revision'] !==
operatorLabels?.['org.opencontainers.image.revision'] ||
applicationLabels?.['org.opencontainers.image.version'] !==
operatorLabels?.['org.opencontainers.image.version'] ||
!/^3\.0\.0-alpha\.[0-9]+$/u.test(
applicationLabels?.['org.opencontainers.image.version'] ?? '',
)
) {
fail('image identity, architecture or authority labels drifted');
}
return Object.freeze({
architecture: application.Architecture,
applicationId: application.Id,
operatorId: operator.Id,
});
}
function writePrivateJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, {
encoding: 'utf8',
mode: 0o600,
flag: 'wx',
});
}
function operatorArguments(state, command, ...argv) {
return [
'run',
'--rm',
'--read-only',
'--user',
`${state.uid}:${state.gid}`,
'--network',
'none',
'--cap-drop',
'ALL',
'--security-opt',
'no-new-privileges',
'--memory',
'128m',
'--memory-swap',
'128m',
'--cpus',
'0.5',
'--pids-limit',
'32',
'--tmpfs',
'/tmp:rw,nosuid,nodev,noexec,size=8m',
'--volume',
`${state.root}:/var/lib/qinglong3`,
state.operatorImage,
command,
...argv,
];
}
function runOperator(state, command, commandFileName) {
let output;
try {
output = docker(
operatorArguments(
state,
command,
'run',
'--command-file',
`/var/lib/qinglong3/${commandFileName}`,
),
);
} catch (error) {
fail(
`operator stage ${command}/${commandFileName} failed: ${
error instanceof Error ? error.message : 'unknown failure'
}`,
);
}
let result;
try {
result = JSON.parse(output);
} catch {
fail('operator emitted non-JSON output');
}
if (!result || typeof result !== 'object' || Array.isArray(result)) {
fail('operator result shape is invalid');
}
return result;
}
function ownerCommand(state, fileName, operation, request) {
writePrivateJson(path.join(state.root, fileName), {
schemaVersion: 1,
operation,
options: {
deploymentRoot: '/var/lib/qinglong3',
databasePath: '/var/lib/qinglong3/qinglong3.sqlite',
pepperPath: '/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper',
pepperKeyId: 'owner-v1',
secretDeliveryDirectory: '/var/lib/qinglong3/owner-delivery',
profile: state.profile,
busyTimeoutMs: 100,
},
request,
});
return runOperator(state, 'owner', fileName);
}
function prepareFreshAuthority(state) {
for (const directory of [
'owner-peppers',
'owner-pepper-backup',
'owner-delivery',
'receipts',
'artifacts',
'plugin-staging',
'plugin-activation',
]) {
fs.mkdirSync(path.join(state.root, directory), { mode: 0o700 });
}
writePrivateJson(path.join(state.root, 'setup.json'), {
schemaVersion: 1,
operation: 'local.setup.prepare',
options: {
deploymentRoot: '/var/lib/qinglong3',
databasePath: '/var/lib/qinglong3/qinglong3.sqlite',
profile: state.profile,
ownerPepperKeyringDirectory: '/var/lib/qinglong3/owner-peppers',
ownerPepperBackupDirectory: '/var/lib/qinglong3/owner-pepper-backup',
ownerPepperKeyId: 'owner-v1',
localSecretKeyringPath: '/var/lib/qinglong3/local-secret-keyring.json',
busyTimeoutMs: 100,
},
request: {
registerMutationId: '019f8680-143d-4000-8000-000000000011',
activateMutationId: '019f8680-143d-4000-8000-000000000012',
registeredAtMs: 1_785_254_400_000,
activatedAtMs: 1_785_254_400_001,
},
});
const prepared = runOperator(state, 'setup', 'setup.json');
const replay = runOperator(state, 'setup', 'setup.json');
if (prepared.status !== 'prepared' || replay.status !== 'existing') {
fail('fresh setup did not converge through the operator image');
}
return Object.freeze({ prepared: true, replay: true });
}
function establishFirstOwner(state) {
const credentialMutationId = '019f8680-143d-4000-8000-000000000021';
const challengeMutationId = '019f8680-143d-4000-8000-000000000022';
const provisioned = ownerCommand(
state,
'owner-provision.json',
'owner.identity.provision',
{
mutationId: credentialMutationId,
requestId: 'alpha-trial-owner-provision',
},
);
const issued = ownerCommand(
state,
'owner-challenge.json',
'owner.challenge.issue',
{
projectId: 'default',
mutationId: challengeMutationId,
requestId: 'alpha-trial-owner-challenge',
},
);
const claimed = ownerCommand(
state,
'owner-claim.json',
'owner.claim.from-deliveries',
{
projectId: 'default',
mutationId: '019f8680-143d-4000-8000-000000000023',
requestId: 'alpha-trial-owner-claim',
credentialMutationId,
challengeMutationId,
},
);
if (
provisioned.status !== 'inserted' ||
issued.status !== 'inserted' ||
claimed.status !== 'inserted' ||
claimed.role !== 'owner'
) {
fail('first Owner ceremony did not converge');
}
writePrivateJson(path.join(state.root, 'owner-credential-install.json'), {
schemaVersion: 1,
operation: 'owner.credential-presentation.install-from-delivery',
options: {
deploymentRoot: '/var/lib/qinglong3',
databasePath: '/var/lib/qinglong3/qinglong3.sqlite',
pepperPath: '/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper',
pepperKeyId: 'owner-v1',
secretDeliveryDirectory: '/var/lib/qinglong3/owner-delivery',
profile: state.profile,
busyTimeoutMs: 100,
},
request: {
credentialMutationId,
destinationFilePath: '/var/lib/qinglong3/owner-credential.json',
},
});
const presentation = runOperator(
state,
'owner',
'owner-credential-install.json',
);
if (
presentation.status !== 'installed' ||
presentation.credentialMutationId !== credentialMutationId
) {
fail('Owner credential presentation did not install');
}
for (const acknowledgement of [
{
file: 'owner-credential-ack.json',
purpose: 'credential-provisioning',
mutationId: credentialMutationId,
digest: provisioned.delivery?.deliveryDigest,
},
{
file: 'owner-challenge-ack.json',
purpose: 'challenge',
mutationId: challengeMutationId,
digest: issued.delivery?.deliveryDigest,
},
]) {
if (!/^[0-9a-f]{64}$/u.test(acknowledgement.digest ?? '')) {
fail('Owner delivery digest is unavailable');
}
ownerCommand(state, acknowledgement.file, 'owner.delivery.acknowledge', {
purpose: acknowledgement.purpose,
mutationId: acknowledgement.mutationId,
expectedDeliveryDigest: acknowledgement.digest,
});
}
return Object.freeze({
provisioned: true,
challenged: true,
claimed: true,
credentialPresentationInstalled: true,
acknowledged: true,
});
}
function createFirstAutomationTask(state) {
if (state.variant !== 'console') {
return Object.freeze({ status: 'not_applicable' });
}
writePrivateJson(path.join(state.root, 'alpha-first-task.json'), {
schemaVersion: 1,
operation: 'task.put',
options: {
deploymentRoot: '/var/lib/qinglong3',
databasePath: '/var/lib/qinglong3/qinglong3.sqlite',
profile: state.profile,
ownerPepperKeyringDirectory: '/var/lib/qinglong3/owner-peppers',
credentialFilePath: '/var/lib/qinglong3/owner-credential.json',
busyTimeoutMs: 100,
},
request: {
projectId: 'default',
taskId: 'alpha-first-automation',
expectedRevision: null,
mutationId: '019f8680-143d-4000-8000-000000000031',
requestId: 'alpha-trial-first-task',
failureAuditEventId: '019f8680-143d-4000-8000-000000000032',
name: 'QingLong 3.0 first automation',
description:
'A bounded offline task proving the fresh Alpha installation can execute work',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/echo',
args: ['qinglong3-alpha-first-automation'],
},
},
},
labels: { 'qinglong.alpha.example': 'true' },
enabled: true,
occurredAtMs: 1_785_254_400_031,
},
});
const result = runOperator(state, 'task', 'alpha-first-task.json');
if (
result.status !== 'created' ||
result.task?.taskId !== 'alpha-first-automation' ||
result.task?.revision !== 1 ||
result.task?.enabled !== true
) {
fail('first automation Task did not converge');
}
return Object.freeze({
status: 'created',
taskId: result.task.taskId,
revision: result.task.revision,
});
}
function writeApplicationConfig(state) {
writePrivateJson(path.join(state.root, 'local-application.json'), {
schema: 'qinglong/local-application-process@v2',
instanceId: 'alpha-trial-local',
profile: state.profile,
storage: {
mode: 'fresh',
databasePath: '/var/lib/qinglong3/qinglong3.sqlite',
busyTimeoutMs: 100,
},
runtime: {
receiptRoot: '/var/lib/qinglong3/receipts',
artifactRoot: '/var/lib/qinglong3/artifacts',
secretKeyringPath: '/var/lib/qinglong3/local-secret-keyring.json',
},
pluginPackages: {
stagingRoot: '/var/lib/qinglong3/plugin-staging',
activationRoot: '/var/lib/qinglong3/plugin-activation',
recoverySource: { mode: 'disabled' },
pageSize: 4,
maxPages: 4,
taskPublicationPageSize: 4,
taskPublicationMaxPages: 4,
},
ai: { deployment: 'excluded' },
});
if (state.variant === 'console') {
writePrivateJson(path.join(state.root, 'local-api.json'), {
schema: 'qinglong/local-api-process@v1',
deploymentRoot: '/var/lib/qinglong3',
applicationConfigFilePath: '/var/lib/qinglong3/local-application.json',
ownerPepperKeyringDirectory: '/var/lib/qinglong3/owner-peppers',
listener: { host: '127.0.0.1', port: 5700 },
});
}
}
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function credentialToken(state) {
const value = JSON.parse(
fs.readFileSync(path.join(state.root, 'owner-credential.json'), 'utf8'),
);
if (
value?.schemaVersion !== 1 ||
value?.kind !== 'qinglong3-local-identity-credential-presentation' ||
typeof value?.token !== 'string' ||
!/^ql3c_own_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{43}$/u.test(value.token)
) {
fail('installed Owner credential presentation is invalid');
}
return value.token;
}
async function apiRequest(pathname, token, options = {}) {
const response = await fetch(`http://127.0.0.1:5700${pathname}`, {
redirect: 'manual',
signal: AbortSignal.timeout(2_000),
...options,
headers: {
authorization: `Bearer ${token}`,
...(options.body === undefined
? {}
: { 'content-type': 'application/json' }),
...(options.headers ?? {}),
},
});
const body = await response.json();
return Object.freeze({ status: response.status, body });
}
async function consoleSurfaceContract(state, adapters = {}) {
const request = adapters.apiRequest ?? apiRequest;
const fetchSurface = adapters.fetch ?? fetch;
const wait = adapters.delay ?? delay;
const readCredential = adapters.credentialToken ?? credentialToken;
let ready = false;
let lastReadyError;
for (let attempt = 0; attempt < 20; attempt += 1) {
try {
const root = await fetchSurface('http://127.0.0.1:5700/', {
redirect: 'manual',
signal: AbortSignal.timeout(2_000),
});
const api = await fetchSurface(
'http://127.0.0.1:5700/api/v3/projects/default/tasks',
{
redirect: 'manual',
signal: AbortSignal.timeout(2_000),
},
);
await root.body?.cancel();
await api.body?.cancel();
if (root.status !== 200 || api.status !== 401) {
fail(
`Console HTTP contract drifted: root=${root.status}, unauthenticatedApi=${api.status}`,
);
}
ready = true;
break;
} catch (error) {
lastReadyError = error;
await wait(250);
}
}
if (!ready) {
throw lastReadyError || new Error('Console listener did not become ready');
}
const token = readCredential(state);
const task = await request(
'/api/v3/projects/default/tasks/alpha-first-automation',
token,
);
if (
task.status !== 200 ||
task.body?.task?.revision !== 1 ||
!/^[0-9a-f]{64}$/u.test(task.body?.task?.contentDigest ?? '')
) {
fail('starter Task is not visible through the authenticated Console API');
}
const started = await request(
'/api/v3/projects/default/tasks/alpha-first-automation/runs',
token,
{
method: 'POST',
body: JSON.stringify({
schema: 'qinglong/task-start@v1',
mutationId: '019f8680-143d-7000-8000-000000000041',
expectedRevision: task.body.task.revision,
expectedContentDigest: task.body.task.contentDigest,
}),
},
);
const accepted =
(started.status === 202 && started.body?.status === 'accepted') ||
(started.status === 200 && started.body?.status === 'existing');
if (
!accepted ||
typeof started.body?.runId !== 'string' ||
typeof started.body?.attemptId !== 'string'
) {
fail(
`starter Task did not converge one fenced Run: status=${
started.status
}, code=${
started.body?.code ?? started.body?.status ?? 'unknown'
}, reason=${started.body?.reason ?? 'none'}`,
);
}
let terminal;
for (let attempt = 0; attempt < 80; attempt += 1) {
const current = await request(
`/api/v3/projects/default/runs/${started.body.runId}`,
token,
);
if (current.status !== 200) {
fail(
`starter Run became unreadable: status=${current.status}, code=${
current.body?.code ?? 'unknown'
}`,
);
}
if (
['succeeded', 'failed', 'cancelled', 'timed_out'].includes(
current.body?.run?.status,
)
) {
terminal = current.body.run.status;
break;
}
await wait(250);
}
if (terminal !== 'succeeded') {
fail(`starter Run did not succeed: ${terminal ?? 'timeout'}`);
}
let logText;
for (let attempt = 0; attempt < 20; attempt += 1) {
const log = await request(
`/api/v3/projects/default/runs/${started.body.runId}/attempts/${started.body.attemptId}/log?offset=0&length=32768`,
token,
);
if (log.status === 200 && log.body?.status === 'available') {
logText = Buffer.from(log.body.content, 'base64').toString('utf8');
break;
}
if (log.status !== 202 || log.body?.status !== 'pending') {
fail(
`starter Run log became unavailable: status=${log.status}, code=${
log.body?.code ?? log.body?.status ?? 'unknown'
}`,
);
}
await wait(250);
}
if (!logText?.includes('qinglong3-alpha-first-automation')) {
fail('starter Run log does not contain the bounded work marker');
}
return Object.freeze({
listener: '127.0.0.1:5700',
rootStatus: 200,
unauthenticatedApiStatus: 401,
firstAutomation: Object.freeze({
taskId: 'alpha-first-automation',
runStatus: terminal,
logMarkerObserved: true,
}),
});
}
async function runApplication(state) {
const name = `ql3-alpha-trial-${process.pid}-${crypto
.randomUUID()
.slice(0, 8)}`;
const memory = state.profile === 'edge' ? '128m' : '256m';
const child = spawn(
'docker',
[
'run',
'--rm',
'--name',
name,
'--read-only',
'--user',
`${state.uid}:${state.gid}`,
'--network',
state.variant === 'console' ? 'host' : 'none',
'--cap-drop',
'ALL',
'--security-opt',
'no-new-privileges',
'--memory',
memory,
'--memory-swap',
memory,
'--cpus',
'0.5',
'--pids-limit',
state.profile === 'edge' ? '64' : '256',
'--tmpfs',
'/tmp:rw,nosuid,nodev,noexec,size=16m',
'--volume',
`${state.root}:/var/lib/qinglong3`,
state.applicationImage,
'--config',
state.variant === 'console'
? '/var/lib/qinglong3/local-api.json'
: '/var/lib/qinglong3/local-application.json',
],
{ stdio: ['ignore', 'pipe', 'pipe'] },
);
let stdout = '';
let stderr = '';
let active = false;
let surfaceError;
let surface = Object.freeze({ status: 'not_applicable' });
let surfacePromise = Promise.resolve();
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout += chunk;
if (
!active &&
stdout.split('\n').some((line) => {
try {
return JSON.parse(line).event === 'active';
} catch {
return false;
}
})
) {
active = true;
surfacePromise = (async () => {
try {
if (state.variant === 'console') {
surface = await consoleSurfaceContract(state);
}
} catch (error) {
surfaceError = error;
} finally {
docker(['stop', '--time', '30', name]);
}
})();
}
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
try {
const outcome = await new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error('application lifecycle timed out')),
ACTIVE_TIMEOUT_MS,
);
child.once('error', reject);
child.once('exit', (code, signal) => {
clearTimeout(timeout);
resolve({ code, signal });
});
});
await surfacePromise;
if (surfaceError) throw surfaceError;
const events = stdout
.trim()
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line));
if (
outcome.code !== 0 ||
outcome.signal !== null ||
stderr !== '' ||
!active ||
!events.some(
({ event, stopResult }) =>
event === 'stopped' && stopResult === 'stopped',
)
) {
fail(
`application lifecycle drifted: ${JSON.stringify({
outcome,
stderr: stderr.slice(0, 2048),
events,
})}`,
);
}
return Object.freeze({ active: true, gracefulStop: true, surface });
} finally {
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
}
}
async function main() {
if (process.versions.node.split('.')[0] !== '24') fail('Node 24 is required');
if (
typeof process.getuid !== 'function' ||
typeof process.getgid !== 'function'
) {
fail('a POSIX identity is required');
}
const options = argumentsFrom(process.argv.slice(2));
const images = inspectImages(
options.applicationImage,
options.operatorImage,
options.variant,
);
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-trial-')),
);
fs.chmodSync(root, 0o700);
const state = Object.freeze({
...options,
...images,
root,
uid: process.getuid(),
gid: process.getgid(),
});
try {
const setup = prepareFreshAuthority(state);
const owner = establishFirstOwner(state);
const firstAutomation = createFirstAutomationTask(state);
writeApplicationConfig(state);
const lifecycle = await runApplication(state);
const database = new DatabaseSync(path.join(root, 'qinglong3.sqlite'), {
readOnly: true,
});
let integrity;
let ownerCount;
try {
integrity = database
.prepare('PRAGMA integrity_check')
.get().integrity_check;
ownerCount = database
.prepare(
`SELECT COUNT(*) AS count FROM "QingLong3ProjectRoleBindings" WHERE "project_id" = 'default' AND "role" = 'owner' AND "state" = 'active'`,
)
.get().count;
} finally {
database.close();
}
if (integrity !== 'ok' || ownerCount !== 1)
fail('durable SQLite result is invalid');
process.stdout.write(
`${JSON.stringify({
schemaVersion: 3,
schema: 'qinglong/local-alpha-trial-kit-live@v3',
variant: options.variant,
profile: options.profile,
architecture: images.architecture,
images: {
applicationId: images.applicationId,
operatorId: images.operatorId,
},
setup,
owner,
firstAutomation,
lifecycle,
sqliteIntegrity: integrity,
activeOwnerBindings: ownerCount,
operatorNetwork: 'none',
compatible: true,
})}\n`,
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
});
}
module.exports = Object.freeze({ consoleSurfaceContract });