feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,503 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
inspectLegacyCrontabAdoptionDiagnostics,
inspectLegacySqlitePath,
verifyReviewedLegacyCrontabAdoptionDecisionAuthorizationFile,
} = require('@qinglong/local-admin');
const {
LegacyCrontabDecisionIssuerKeyringFileProvider,
provisionLegacyCrontabDecisionIssuerKeyring,
} = require('@qinglong/local-admin/decision-issuer');
const {
LocalOwnerPepperKeyringFileProvider,
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console/pepper-custody');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const {
LegacyCrontabAdoptionCliConfigurationError,
runLegacyCrontabAdoptionCommandFile,
} = require('../dist/lifecycle/adoption');
const DECISION_ID = '019a2b3c-4d5e-7f60-8123-456789abcdef';
const MUTATION_ID = '12345678-1234-4123-8123-123456789ace';
const CREDENTIAL_ID = 'owner-adoption';
const PEPPER_KEY_ID = 'owner-v1';
const PEPPER = Buffer.alloc(32, 83).toString('base64url');
const SECRET = Buffer.alloc(32, 84).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, SECRET);
const OTHER_CREDENTIAL_ID = 'other-adoption';
const OTHER_SECRET = Buffer.alloc(32, 85).toString('base64url');
const OTHER_TOKEN = formatApiCredentialToken(OTHER_CREDENTIAL_ID, OTHER_SECRET);
async function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-adoption-cli-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const authorizationDirectory = path.join(deploymentRoot, 'authorizations');
const pepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
for (const directory of [
commandsDirectory,
authorizationDirectory,
pepperKeyringDirectory,
]) {
fs.mkdirSync(directory, { mode: 0o700 });
}
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const sourcePath = path.join(deploymentRoot, 'legacy.sqlite');
const reviewFilePath = path.join(deploymentRoot, 'review.ndjson');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
const issuerKeyringPath = path.join(
deploymentRoot,
'decision-issuer.keyring',
);
const authorizationPath = path.join(
authorizationDirectory,
'decision.ndjson',
);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: pepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 83),
});
const now = Date.now();
const target = new DatabaseSync(databasePath);
try {
target
.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(
PEPPER_KEY_ID,
pepperSummary.digest,
'b'.repeat(64),
'00000000-0000-4000-8000-000000000a01',
'00000000-0000-4000-8000-000000000a02',
now - 2_000,
now - 1_500,
);
target
.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(
'00000000-0000-4000-8000-000000000a02',
PEPPER_KEY_ID,
pepperSummary.digest,
'b'.repeat(64),
now - 1_500,
);
target
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
target
.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', 'owner-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
now - 1_000,
now - 1_000,
now + 10 * 60 * 1_000,
);
target
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
target
.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', 'owner-user', 1, 'active', 'owner',
'adoption-cli-owner-binding', 'user', 'owner-user', ?
)`,
)
.run(now - 500);
} finally {
target.close();
}
fs.chmodSync(databasePath, 0o600);
const source = new DatabaseSync(sourcePath);
source.exec(`
CREATE TABLE "Auths" (id INTEGER PRIMARY KEY, type TEXT, info TEXT);
CREATE TABLE "Crontabs" (
id INTEGER PRIMARY KEY,
name TEXT,
command TEXT NOT NULL,
schedule TEXT
);
CREATE TABLE "Envs" (id INTEGER PRIMARY KEY, name TEXT, value TEXT);
INSERT INTO "Crontabs" (id, name, command, schedule)
VALUES (1, 'Reviewed task', 'task /scripts/reviewed.sh', '0 0 * * *');
`);
source.close();
fs.chmodSync(sourcePath, 0o600);
const plan = inspectLegacySqlitePath({
sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
});
const page = inspectLegacyCrontabAdoptionDiagnostics({
sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: plan.planDigest,
limit: 16,
});
const decision = {
rowOrdinal: page.diagnostics[0].rowOrdinal,
sourceDigest: page.diagnostics[0].sourceDigest,
disposition: 'adopt',
reason: 'reviewed_lossless',
};
const reviewRecords = [
{
schemaVersion: 1,
kind: 'qinglong3-legacy-crontab-decision-review-file-header',
decisionId: DECISION_ID,
profile: 'edge',
planDigest: plan.planDigest,
inventoryDigest: plan.tasks.inventoryDigest,
},
{
schemaVersion: 1,
kind: 'qinglong3-legacy-crontab-decision-review-file-row',
decision,
},
];
fs.writeFileSync(
reviewFilePath,
`${reviewRecords.map((record) => JSON.stringify(record)).join('\n')}\n`,
{ mode: 0o600 },
);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
await provisionLegacyCrontabDecisionIssuerKeyring(issuerKeyringPath);
const options = {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: pepperKeyringDirectory,
issuerKeyringPath,
credentialFilePath,
sourcePath,
reviewFilePath,
authorizationPath,
expectedPlanDigest: plan.planDigest,
decisionId: DECISION_ID,
legacyTimezone: 'UTC',
lifetimeMs: 30_000,
};
const commandFilePath = path.join(commandsDirectory, 'issue.json');
fs.writeFileSync(
commandFilePath,
`${JSON.stringify({
schemaVersion: 1,
operation: 'legacy-crontab.decision.issue',
options,
})}\n`,
{ mode: 0o600 },
);
return {
...options,
commandFilePath,
plan,
pepperProvider: new LocalOwnerPepperKeyringFileProvider(
pepperKeyringDirectory,
),
};
}
function writeCommitCommand(value, name = 'commit') {
const commandFilePath = path.join(
path.dirname(value.commandFilePath),
`${name}.json`,
);
fs.writeFileSync(
commandFilePath,
`${JSON.stringify({
schemaVersion: 1,
operation: 'legacy-crontab.adoption.commit',
options: {
deploymentRoot: value.deploymentRoot,
targetPath: value.databasePath,
profile: value.profile,
ownerPepperKeyringDirectory: value.ownerPepperKeyringDirectory,
issuerKeyringPath: value.issuerKeyringPath,
credentialFilePath: value.credentialFilePath,
sourcePath: value.sourcePath,
authorizationPath: value.authorizationPath,
expectedPlanDigest: value.expectedPlanDigest,
expectedDecisionId: DECISION_ID,
projectId: 'default',
mutationId: MUTATION_ID,
requestId: `legacy-adoption-cli-${name}`,
legacyTimezone: 'UTC',
},
})}\n`,
{ mode: 0o600 },
);
return commandFilePath;
}
test('issues a reviewed authorization through the ql3-adoption product binary', async (t) => {
const value = await fixture(t);
const child = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/lifecycle/adoptionCli.js'),
'run',
'--command-file',
value.commandFilePath,
],
{ encoding: 'utf8' },
);
assert.equal(child.status, 0, child.stderr);
assert.equal(child.stderr, '');
assert.equal(child.stdout.includes(TOKEN), false);
const result = JSON.parse(child.stdout);
assert.equal(result.operation, 'legacy-crontab.decision.issue');
assert.equal(result.receipt.reviewerSubjectId, 'owner-user');
assert.equal(result.authorization.decisionCount, 1);
assert.match(result.review.fileDigest, /^[0-9a-f]{64}$/);
assert.equal(fs.statSync(value.authorizationPath).mode & 0o777, 0o600);
const verified =
await verifyReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: value.plan.planDigest,
expectedDecisionId: DECISION_ID,
authorizationPath: value.authorizationPath,
keyProvider: new LegacyCrontabDecisionIssuerKeyringFileProvider(
value.issuerKeyringPath,
),
observedAtMs: result.receipt.issuedAtMs + 1,
});
assert.equal(verified.file.fileDigest, result.authorization.fileDigest);
assert.equal(verified.receipt.reviewer.assurance, 'local_console');
});
test('commits the signed adoption with the same current operator', async (t) => {
const value = await fixture(t);
const binaryPath = path.join(__dirname, '../dist/lifecycle/adoptionCli.js');
const issued = spawnSync(
process.execPath,
[binaryPath, 'run', '--command-file', value.commandFilePath],
{ encoding: 'utf8' },
);
assert.equal(issued.status, 0, issued.stderr);
const commitCommandPath = writeCommitCommand(value);
const committed = spawnSync(
process.execPath,
[binaryPath, 'run', '--command-file', commitCommandPath],
{ encoding: 'utf8' },
);
assert.equal(committed.status, 0, committed.stderr);
assert.equal(committed.stderr, '');
assert.equal(committed.stdout.includes(TOKEN), false);
const result = JSON.parse(committed.stdout);
assert.equal(result.operation, 'legacy-crontab.adoption.commit');
assert.equal(result.status, 'inserted');
assert.equal(result.adoption.mutationId, MUTATION_ID);
assert.equal(result.adoption.adoptedTaskCount, 1);
assert.equal(result.adoption.adoptedTriggerCount, 1);
const target = new DatabaseSync(value.databasePath, { readOnly: true });
assert.equal(
target
.prepare('SELECT COUNT(*) AS count FROM "QingLong3LegacyAdoptions"')
.get().count,
1,
);
assert.equal(
target
.prepare('SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitions"')
.get().count,
1,
);
target.close();
});
test('rejects a valid current operator who is not the signed reviewer', async (t) => {
const value = await fixture(t);
const binaryPath = path.join(__dirname, '../dist/lifecycle/adoptionCli.js');
const issued = spawnSync(
process.execPath,
[binaryPath, 'run', '--command-file', value.commandFilePath],
{ encoding: 'utf8' },
);
assert.equal(issued.status, 0, issued.stderr);
const now = Date.now();
const target = new DatabaseSync(value.databasePath);
target
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'other-user', 'active', 1, ?, ?)`,
)
.run(now, now);
target
.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', 'other-user', ?, ?, ?, ?)`,
)
.run(
OTHER_CREDENTIAL_ID,
apiCredentialSecretDigest(PEPPER, OTHER_CREDENTIAL_ID, OTHER_SECRET),
now,
now,
now + 10 * 60 * 1_000,
);
target
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(OTHER_CREDENTIAL_ID, PEPPER_KEY_ID);
target.close();
fs.chmodSync(value.databasePath, 0o600);
fs.writeFileSync(
value.credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: OTHER_TOKEN,
})}\n`,
{ mode: 0o600 },
);
const rejected = spawnSync(
process.execPath,
[
binaryPath,
'run',
'--command-file',
writeCommitCommand(value, 'mismatched-reviewer'),
],
{ encoding: 'utf8' },
);
assert.equal(rejected.status, 1);
assert.equal(rejected.stdout, '');
assert.equal(rejected.stderr.includes(OTHER_TOKEN), false);
assert.equal(
JSON.parse(rejected.stderr).code,
'LEGACY_CRONTAB_ADOPTION_CLI_AUTHENTICATION_FAILED',
);
const stored = new DatabaseSync(value.databasePath, { readOnly: true });
assert.equal(
stored
.prepare('SELECT COUNT(*) AS count FROM "QingLong3LegacyAdoptions"')
.get().count,
0,
);
stored.close();
});
test('keeps credential material outside command JSON and fails closed on widened intent', async (t) => {
const value = await fixture(t);
const commandText = fs.readFileSync(value.commandFilePath, 'utf8');
assert.equal(commandText.includes(TOKEN), false);
assert.equal(commandText.includes(SECRET), false);
const widenedPath = path.join(
path.dirname(value.commandFilePath),
'widened.json',
);
const widened = JSON.parse(commandText);
widened.options.token = TOKEN;
fs.writeFileSync(widenedPath, `${JSON.stringify(widened)}\n`, {
mode: 0o600,
});
await assert.rejects(
runLegacyCrontabAdoptionCommandFile(widenedPath),
LegacyCrontabAdoptionCliConfigurationError,
);
assert.equal(fs.existsSync(value.authorizationPath), false);
const help = spawnSync(
process.execPath,
[path.join(__dirname, '../dist/lifecycle/adoptionCli.js'), '--help'],
{ encoding: 'utf8' },
);
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3-adoption run --command-file /);
});
test('rejects an invalid credential without publishing authorization', async (t) => {
const value = await fixture(t);
const presentation = JSON.parse(
fs.readFileSync(value.credentialFilePath, 'utf8'),
);
presentation.token = formatApiCredentialToken(
CREDENTIAL_ID,
Buffer.alloc(32, 90).toString('base64url'),
);
fs.writeFileSync(
value.credentialFilePath,
`${JSON.stringify(presentation)}\n`,
{ mode: 0o600 },
);
await assert.rejects(
runLegacyCrontabAdoptionCommandFile(value.commandFilePath),
);
assert.equal(fs.existsSync(value.authorizationPath), false);
});
@@ -0,0 +1,613 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
migrateLocalModelInvocationFeature,
} = require('@qinglong/ai/model-invocation-migration');
const {
createLocalAiFeatureCommandRunner,
runLocalAiFeatureCommandFile,
} = require('@qinglong/local-owner-cli/ai-feature-command');
const {
establishAuthenticatedLocalCommand,
} = require('@qinglong/local-owner-console/authenticated-command');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console');
const {
openLocalSqliteAuthenticatedManagementDatabase,
} = require('@qinglong/local-sqlite/authenticated-management');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const CREDENTIAL_ID = 'ai-feature-owner';
const PEPPER_KEY_ID = 'ai-feature-owner-v1';
const PEPPER = Buffer.alloc(32, 91).toString('base64url');
const SECRET = Buffer.alloc(32, 92).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, SECRET);
async function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-ai-feature-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const summary = provisionLocalOwnerPepperKey({
keyringDirectory: ownerPepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 91),
});
const now = Date.now();
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(
PEPPER_KEY_ID,
summary.digest,
'c'.repeat(64),
'51000000-0000-4000-8000-000000000001',
'51000000-0000-4000-8000-000000000002',
now - 2_000,
now - 1_500,
);
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(
'51000000-0000-4000-8000-000000000002',
PEPPER_KEY_ID,
summary.digest,
'c'.repeat(64),
now - 1_500,
);
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
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', 'owner-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
now - 1_000,
now - 1_000,
now + 10 * 60 * 1_000,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
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', 'owner-user', 1, 'active', 'owner',
'ai-feature-owner-binding', 'user', 'owner-user', ?
)`,
)
.run(now - 500);
} finally {
database.close();
}
fs.chmodSync(databasePath, 0o600);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
return {
deploymentRoot,
commandsDirectory,
databasePath,
credentialFilePath,
ownerPepperKeyringDirectory,
options: {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory,
credentialFilePath,
},
};
}
function commandFile(value, operation, request, name, extra = {}) {
const commandPath = path.join(value.commandsDirectory, `${name}.json`);
fs.writeFileSync(
commandPath,
`${JSON.stringify({
schemaVersion: 1,
operation,
options: value.options,
request,
...extra,
})}\n`,
{ mode: 0o600 },
);
return commandPath;
}
function inspectRequest(suffix) {
return {
requestId: `ai-feature-inspect-${suffix}`,
failureAuditEventId: `52000000-0000-4000-8000-00000000000${suffix}`,
};
}
function assertNoSensitiveMaterial(value) {
const serialized = JSON.stringify(value);
assert.equal(serialized.includes(TOKEN), false);
assert.equal(serialized.includes(SECRET), false);
assert.doesNotMatch(serialized, /authenticationId|principal|subjectId/);
}
test('runs explicit inspect, activate, replay and non-destructive deactivate', async (t) => {
const value = await fixture(t);
const inspectFile = commandFile(
value,
'ai-feature.inspect',
inspectRequest('1'),
'01-inspect',
);
const before = await runLocalAiFeatureCommandFile(inspectFile);
assert.equal(before.schemaState, 'absent');
assert.equal(before.activation, null);
assert.equal(before.runtimeAction, 'none');
assert.equal(
before.migrationPlanDigest,
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
);
assertNoSensitiveMaterial(before);
const activateFile = commandFile(
value,
'ai-feature.activate',
{
requestId: 'ai-feature-activate-1',
failureAuditEventId: '52000000-0000-4000-8000-000000000002',
mutationId: 'ai-feature-activation-1',
expectedGeneration: 0,
expectedState: null,
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'fresh_database',
backupEvidenceDigest: null,
},
},
'02-activate',
);
const activated = await runLocalAiFeatureCommandFile(activateFile);
assert.equal(activated.status, 'created');
assert.equal(activated.schemaState, 'ready');
assert.equal(activated.runtimeAction, 'restart_required');
assert.deepEqual(
{
generation: activated.activation.generation,
state: activated.activation.state,
},
{ generation: 1, state: 'active' },
);
assertNoSensitiveMaterial(activated);
const replayed = await runLocalAiFeatureCommandFile(activateFile);
assert.equal(replayed.status, 'existing');
assert.deepEqual(replayed.activation, activated.activation);
assert.equal(replayed.runtimeAction, 'restart_required');
const child = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/ai-management/aiFeatureCli.js'),
'run',
'--command-file',
inspectFile,
],
{ encoding: 'utf8' },
);
assert.equal(child.status, 0, child.stderr);
assert.equal(child.stderr, '');
assert.equal(JSON.parse(child.stdout).activation.state, 'active');
assert.equal(child.stdout.includes(TOKEN), false);
const deactivateFile = commandFile(
value,
'ai-feature.deactivate',
{
requestId: 'ai-feature-deactivate-1',
failureAuditEventId: '52000000-0000-4000-8000-000000000003',
mutationId: 'ai-feature-deactivation-1',
expectedGeneration: 1,
expectedState: 'active',
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'preserve_existing',
backupEvidenceDigest: null,
},
},
'03-deactivate',
);
const deactivated = await runLocalAiFeatureCommandFile(deactivateFile);
assert.equal(deactivated.status, 'created');
assert.deepEqual(
{
generation: deactivated.activation.generation,
state: deactivated.activation.state,
},
{ generation: 2, state: 'inactive' },
);
assert.equal(deactivated.runtimeAction, 'restart_required');
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...database
.prepare(
`SELECT
(SELECT count(*) FROM "ModelInvocationFeatureTransitions") AS transitions,
(SELECT count(*) FROM "ModelInvocationFeatureHead") AS heads,
(SELECT count(*) FROM "ModelInvocationStarts") AS starts,
(SELECT count(*) FROM "ModelPriceCatalogPublications") AS publications`,
)
.get(),
},
{ transitions: 2, heads: 1, starts: 0, publications: 0 },
);
} finally {
database.close();
}
});
test('requires the reviewed migration digest and rejects widened command before SQLite', async (t) => {
const value = await fixture(t);
let opened = 0;
const runner = createLocalAiFeatureCommandRunner({
async openDatabase() {
opened += 1;
throw new Error('must not open');
},
authenticate: establishAuthenticatedLocalCommand,
migrate: migrateLocalModelInvocationFeature,
now: Date.now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'ai-feature.inspect',
{
...inspectRequest('4'),
principal: { subject: { type: 'user', id: 'attacker' } },
},
'widened',
),
),
{ code: 'LOCAL_AI_FEATURE_COMMAND_CONFIGURATION_INVALID' },
);
assert.equal(opened, 0);
await assert.rejects(
runLocalAiFeatureCommandFile(
commandFile(
value,
'ai-feature.activate',
{
requestId: 'ai-feature-plan-drift',
failureAuditEventId: '52000000-0000-4000-8000-000000000005',
mutationId: 'ai-feature-plan-drift',
expectedGeneration: 0,
expectedState: null,
expectedMigrationDigest: 'f'.repeat(64),
safety: {
mode: 'fresh_database',
backupEvidenceDigest: null,
},
},
'plan-drift',
),
),
{ code: 'LOCAL_MODEL_INVOCATION_FEATURE_TRANSITION_CONFLICT' },
);
});
test('deactivation refuses an in-flight invocation and preserves active state', async (t) => {
const value = await fixture(t);
await runLocalAiFeatureCommandFile(
commandFile(
value,
'ai-feature.activate',
{
requestId: 'ai-feature-in-flight-activate',
failureAuditEventId: '52000000-0000-4000-8000-000000000008',
mutationId: 'ai-feature-in-flight-activate',
expectedGeneration: 0,
expectedState: null,
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'fresh_database',
backupEvidenceDigest: null,
},
},
'in-flight-activate',
),
);
const managementDatabase =
await openLocalSqliteAuthenticatedManagementDatabase({
databasePath: value.databasePath,
profile: 'edge',
});
const database = managementDatabase.authority.client;
try {
database.exec('PRAGMA foreign_keys = OFF');
const start = {
schema: 'qinglong/model-invocation-start@v1',
invocationId: 'in-flight-invocation',
projectId: 'default',
runId: 'in-flight-run',
stepRunId: 'in-flight-step',
traceId: 'in-flight-trace',
provider: 'test-provider',
model: 'test-model',
policyRevision: 'test-policy',
requestDigest: `sha256:${'a'.repeat(64)}`,
inputBytes: 1,
maxOutputTokens: 1,
deadlineAtMs: 2,
admittedAtMs: 1,
stepRunMutationId: 'in-flight-mutation',
stepRunMutationDigest: 'b'.repeat(64),
runEventId: 'in-flight-event',
startDigest: 'c'.repeat(64),
};
database
.prepare(
`INSERT INTO "ModelInvocationStarts" (
invocation_id, project_id, run_id, step_run_id, trace_id,
provider, model, policy_revision, request_digest, input_bytes,
max_output_tokens, deadline_at_ms, admitted_at_ms, mutation_id,
mutation_digest, run_event_id, start_digest, record_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
start.invocationId,
start.projectId,
start.runId,
start.stepRunId,
start.traceId,
start.provider,
start.model,
start.policyRevision,
start.requestDigest,
start.inputBytes,
start.maxOutputTokens,
start.deadlineAtMs,
start.admittedAtMs,
start.stepRunMutationId,
start.stepRunMutationDigest,
start.runEventId,
start.startDigest,
JSON.stringify(start),
);
} catch (error) {
await managementDatabase.close();
throw error;
}
const runner = createLocalAiFeatureCommandRunner({
async openDatabase() {
return managementDatabase;
},
authenticate: establishAuthenticatedLocalCommand,
migrate: migrateLocalModelInvocationFeature,
now: Date.now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'ai-feature.deactivate',
{
requestId: 'ai-feature-in-flight-deactivate',
failureAuditEventId: '52000000-0000-4000-8000-000000000009',
mutationId: 'ai-feature-in-flight-deactivate',
expectedGeneration: 1,
expectedState: 'active',
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'preserve_existing',
backupEvidenceDigest: null,
},
},
'in-flight-deactivate',
),
),
{ code: 'LOCAL_AI_FEATURE_IN_FLIGHT_INVOCATION' },
);
const inspection = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...inspection
.prepare(
`SELECT generation, state
FROM "ModelInvocationFeatureHead"
WHERE feature_id = 'model-invocation'`,
)
.get(),
},
{ generation: 1, state: 'active' },
);
} finally {
inspection.close();
}
});
test('resumes a pre-migrated empty schema only with backup evidence', async (t) => {
const value = await fixture(t);
const database = new DatabaseSync(value.databasePath);
try {
await migrateLocalModelInvocationFeature(database);
} finally {
database.close();
}
const result = await runLocalAiFeatureCommandFile(
commandFile(
value,
'ai-feature.activate',
{
requestId: 'ai-feature-backup-activate',
failureAuditEventId: '52000000-0000-4000-8000-000000000006',
mutationId: 'ai-feature-backup-activate',
expectedGeneration: 0,
expectedState: null,
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'backup_verified',
backupEvidenceDigest: 'd'.repeat(64),
},
},
'backup-activate',
),
);
assert.equal(result.status, 'created');
assert.equal(result.activation.state, 'active');
});
test('audits transaction-fence revocation and rolls activation back', async (t) => {
const value = await fixture(t);
const runner = createLocalAiFeatureCommandRunner({
openDatabase: openLocalSqliteAuthenticatedManagementDatabase,
async authenticate(...args) {
const authenticated = await establishAuthenticatedLocalCommand(...args);
let revoked = false;
return {
...authenticated,
async confirm() {
await authenticated.confirm();
if (!revoked) {
revoked = true;
const database = new DatabaseSync(value.databasePath);
try {
database
.prepare(
`UPDATE "QingLong3ApiCredentials"
SET state = 'revoked'
WHERE credential_id = ? AND version = 1`,
)
.run(CREDENTIAL_ID);
} finally {
database.close();
}
}
},
};
},
migrate: migrateLocalModelInvocationFeature,
now: Date.now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'ai-feature.activate',
{
requestId: 'ai-feature-revocation-race',
failureAuditEventId: '52000000-0000-4000-8000-000000000007',
mutationId: 'ai-feature-revocation-race',
expectedGeneration: 0,
expectedState: null,
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'fresh_database',
backupEvidenceDigest: null,
},
},
'revocation-race',
),
),
{ code: 'LOCAL_SQLITE_AUTHENTICATED_MANAGEMENT_FENCE_REJECTED' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT count(*) AS count
FROM sqlite_schema
WHERE type = 'table'
AND name = 'ModelInvocationFeatureTransitions'`,
)
.get().count,
0,
);
assert.deepEqual(
{
...database
.prepare(
`SELECT outcome, reasons_json AS reasons
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('52000000-0000-4000-8000-000000000007'),
},
{
outcome: 'denied',
reasons: '["credential_fence_rejected"]',
},
);
} finally {
database.close();
}
});
@@ -0,0 +1,247 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createLocalApprovalCommandRunner,
} = require('@qinglong/local-owner-cli/approval-command');
const {
createApprovalDecisionService,
} = require('@qinglong/runtime-core/approval-decision');
const {
createApprovalRequest,
decideApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const ACTION = Object.freeze({
permission: 'run.start',
actionType: 'tool.invoke',
actionRef: 'tool:run-task-1',
actionDigest: 'a'.repeat(64),
previewDigest: 'b'.repeat(64),
});
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner-1' }),
authenticationId: 'local_approval:auth-1',
authenticatedAtMs: 1_500,
expiresAtMs: 20_000,
assurance: 'local_console',
});
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
function pending() {
return createApprovalRequest({
id: 'approval-1',
projectId: 'default',
action: ACTION,
risk: 'high',
decisionMode: 'separation_of_duty',
requestedBy: { type: 'agent', id: 'agent-1' },
requestedAtMs: 1_000,
expiresAtMs: 10_000,
requestFence: FENCE,
});
}
function options(root) {
return {
deploymentRoot: root,
databasePath: path.join(root, 'qinglong3.sqlite'),
profile: 'edge',
ownerPepperKeyringDirectory: path.join(root, 'owner-keys'),
credentialFilePath: path.join(root, 'credential.json'),
};
}
function writeCommand(root, name, operation, request) {
const filePath = path.join(root, name);
fs.writeFileSync(
filePath,
JSON.stringify({ schemaVersion: 1, operation, options: options(root), request }),
{ mode: 0o600 },
);
return filePath;
}
function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-approval-command-'));
fs.chmodSync(root, 0o700);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
let current = pending();
const audits = [];
let closes = 0;
let confirms = 0;
let activations = 0;
const database = {
profile: 'edge',
readiness: {},
apiCredentials: {},
ownerPepper: {},
projectPolicy: {
async resolve(projectId, subject) {
return {
project: {
id: projectId,
name: 'Default',
slug: 'default',
status: 'active',
version: 1,
createdAtMs: 0,
updatedAtMs: 1,
},
binding: {
projectId,
subject,
version: 1,
state: 'active',
role: 'owner',
mutationId: 'grant-owner-1',
changedBy: { type: 'user', id: 'owner-1' },
createdAtMs: 1,
},
};
},
async append() {
throw new Error('not used');
},
},
approvals: {
async findById(id) {
return id === current.id ? current : null;
},
async decide(command) {
const { requestId: _requestId, audit: _audit, ...decision } = command;
current = decideApprovalRequest(current, decision);
return { status: 'decided', request: current };
},
},
approvalDetails: {
async getApprovalRequestDetail({ projectId, requestId }) {
if (projectId !== current.projectId || requestId !== current.id) return null;
return {
request: current,
preview: {
title: 'Run task',
summary: 'Runs one reviewed task.',
fields: [{ kind: 'identifier', label: 'Task', value: 'task-1' }],
warnings: ['external_effect'],
},
};
},
},
securityAudit: {
async record(record) {
audits.push(record);
},
},
activateUserCredentialFence() {
activations += 1;
},
confirmUserCredentialFence() {
confirms += 1;
},
async close() {
closes += 1;
},
};
const authenticated = {
principal: PRINCIPAL,
databaseFence: {
credentialId: 'owner-credential',
credentialVersion: 1,
pepperKeyId: 'owner-pepper',
materialDigest: 'c'.repeat(64),
subjectType: 'user',
subjectId: 'owner-1',
secretDigest: 'd'.repeat(64),
notBeforeAtMs: 1_000,
expiresAtMs: 20_000,
},
async confirm() {
confirms += 1;
},
};
const runner = createLocalApprovalCommandRunner({
async openDatabase() {
return database;
},
async authenticate() {
return authenticated;
},
createDecisionService: createApprovalDecisionService,
now: () => 2_000,
});
return {
root,
runner,
state: () => ({ current, audits, closes, confirms, activations }),
};
}
function baseRequest() {
return {
projectId: 'default',
approvalRequestId: 'approval-1',
requestId: 'owner-command-1',
auditEventId: '20000000-0000-4000-8000-000000000001',
failureAuditEventId: '20000000-0000-4000-8000-000000000002',
};
}
test('inspects the exact action binding and bounded preview before decision', async (t) => {
const value = fixture(t);
const result = await value.runner.run(
writeCommand(value.root, 'inspect.json', 'approval.inspect', baseRequest()),
);
assert.equal(result.found, true);
assert.deepEqual(result.expectedAction, ACTION);
assert.equal(result.preview.title, 'Run task');
assert.equal(value.state().audits[0].operationId, 'approval.inspect');
assert.equal(value.state().audits[0].outcome, 'allowed');
assert.equal(value.state().closes, 1);
});
test('decides only the inspected action and returns a durable receipt', async (t) => {
const value = fixture(t);
const result = await value.runner.run(
writeCommand(value.root, 'decide.json', 'approval.decide', {
...baseRequest(),
expectedVersion: 1,
expectedAction: ACTION,
decisionId: 'decision-1',
decision: 'approved',
reasonCode: 'reviewed',
}),
);
assert.equal(result.status, 'decided');
assert.equal(result.state, 'approved');
assert.deepEqual(result.action, ACTION);
assert.equal(value.state().current.decidedBy.id, 'owner-1');
assert.equal(value.state().audits.length, 0);
assert.equal(value.state().activations, 1);
assert.ok(value.state().confirms >= 3);
assert.equal(value.state().closes, 1);
});
test('records a denied failure audit when the reviewed binding drifts', async (t) => {
const value = fixture(t);
await assert.rejects(
value.runner.run(
writeCommand(value.root, 'drift.json', 'approval.decide', {
...baseRequest(),
expectedVersion: 1,
expectedAction: { ...ACTION, previewDigest: 'e'.repeat(64) },
decisionId: 'decision-1',
decision: 'approved',
reasonCode: 'reviewed',
}),
),
);
assert.equal(value.state().current.state, 'pending');
assert.equal(value.state().audits.length, 1);
assert.equal(value.state().audits[0].outcome, 'denied');
assert.deepEqual(value.state().audits[0].reasons, ['approval_binding_conflict']);
assert.equal(value.state().closes, 1);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,978 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createLocalIdentityCredentialCommandRunner,
runLocalIdentityCredentialCommandFile,
} = require('@qinglong/local-owner-cli/identity-credential-command');
const {
LocalIdentityCredentialAdministrationAuthorizationError,
createLocalIdentityCredentialAdministrationService,
} = require('@qinglong/local-admin/identity-credential-administration');
const {
establishAuthenticatedLocalCommand,
} = require('@qinglong/local-owner-console/authenticated-command');
const {
FileLocalCredentialAdministrationDelivery,
} = require('@qinglong/local-owner-console/credential-administration-delivery');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console/pepper-custody');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
openLocalSqliteIdentityCredentialAdministrationDatabase,
} = require('@qinglong/local-sqlite/identity-credential-administration');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const {
LocalCredentialOwnerContinuityError,
LocalIdentityCredentialAuthorizationFenceConflictError,
LocalIdentityOwnerBindingConflictError,
} = require('@qinglong/runtime-core/local-identity-credential-administration');
const ISSUE_MUTATION_ID = '83000000-0000-4000-8000-000000000001';
const ACK_MUTATION_ID = '83000000-0000-4000-8000-000000000003';
const PEPPER = Buffer.alloc(32, 83).toString('base64url');
const MATERIAL_DIGEST = 'c'.repeat(64);
function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(fs.realpathSync(os.tmpdir()), 'ql3-identity-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commands = path.join(deploymentRoot, 'commands');
const delivery = path.join(deploymentRoot, 'managed-credentials');
const keyring = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commands, { mode: 0o700 });
fs.mkdirSync(delivery, { mode: 0o700 });
fs.mkdirSync(keyring, { mode: 0o700 });
return {
deploymentRoot,
commands,
delivery,
keyring,
databasePath: path.join(deploymentRoot, 'qinglong3.sqlite'),
credentialFilePath: path.join(deploymentRoot, 'owner-credential.json'),
};
}
function writeCommand(directory, name, value) {
const filePath = path.join(directory, name);
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
return filePath;
}
function baseOptions(state) {
return {
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
};
}
function options(state) {
return {
...baseOptions(state),
credentialDeliveryDirectory: state.delivery,
};
}
test('issues, exactly replays and acknowledges a credential without returning secret material', async (t) => {
const state = fixture(t);
let nowMs = 1_000;
let credentialCalls = 0;
let acknowledgementCalls = 0;
let committed;
const audits = [];
const database = {
apiCredentials: {
async resolve() {
return null;
},
},
ownerPepper: {
async resolveActive() {
return {
generation: 1,
mutationId: 'pepper-active',
expectedGeneration: 0,
activePepperKeyId: 'owner-v1',
materialDigest: MATERIAL_DIGEST,
backupDigest: 'd'.repeat(64),
activatedAtMs: 0,
};
},
async resolveKey() {
return {
pepperKeyId: 'owner-v1',
materialDigest: MATERIAL_DIGEST,
state: 'active',
version: 2,
registeredAtMs: 0,
activatedAtMs: 0,
};
},
},
projectPolicy: {},
identityCredentialAdministration: {
async record(audit) {
audits.push(audit);
},
},
activateUserCredentialFence() {},
async close() {},
};
const service = {
async changeIdentity() {
throw new Error('not used');
},
async changeCredential(request) {
credentialCalls += 1;
if (!committed) {
committed = {
secretDigest: request.secretDigest,
deliveryDigest: request.deliveryDigest,
notBeforeAtMs: request.notBeforeAtMs,
expiresAtMs: request.expiresAtMs,
};
} else {
assert.equal(request.secretDigest, committed.secretDigest);
assert.equal(request.deliveryDigest, committed.deliveryDigest);
assert.equal(request.notBeforeAtMs, committed.notBeforeAtMs);
assert.equal(request.expiresAtMs, committed.expiresAtMs);
}
return {
status: credentialCalls === 1 ? 'inserted' : 'existing',
credential: {
credentialId: request.credentialId,
version: 1,
pepperKeyId: request.pepperKeyId,
state: 'active',
subject: request.target,
subjectStatus: 'active',
secretDigest: request.secretDigest,
createdAtMs: committed.notBeforeAtMs,
notBeforeAtMs: committed.notBeforeAtMs,
expiresAtMs: committed.expiresAtMs,
},
mutation: {
mutationId: request.mutationId,
operation: request.operation,
credentialId: request.credentialId,
credentialVersion: 1,
expectedPreviousVersion: 0,
changedBy: request.principal.subject,
createdAtMs: committed.notBeforeAtMs,
},
delivery: { digest: committed.deliveryDigest },
audit: {},
};
},
async acknowledgeCredentialDelivery(request) {
acknowledgementCalls += 1;
return {
status: acknowledgementCalls === 1 ? 'inserted' : 'existing',
acknowledgement: {
credentialMutationId: request.credentialMutationId,
acknowledgementMutationId: request.mutationId,
projectId: request.projectId,
deliveryDigest: request.expectedDeliveryDigest,
acknowledgedBy: request.principal.subject,
acknowledgedAtMs: nowMs,
},
audit: {},
};
},
};
const runner = createLocalIdentityCredentialCommandRunner({
async openDatabase() {
return database;
},
async authenticate() {
return {
principal: {
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_identity_admin:test',
authenticatedAtMs: 0,
expiresAtMs: 120_000,
assurance: 'local_console',
},
databaseFence: {
credentialId: 'owner-primary',
credentialVersion: 1,
pepperKeyId: 'owner-v1',
materialDigest: MATERIAL_DIGEST,
subjectType: 'user',
subjectId: 'owner-user',
secretDigest: 'e'.repeat(64),
notBeforeAtMs: 0,
expiresAtMs: 120_000,
},
async confirm() {},
};
},
createService() {
return service;
},
createDelivery(directory) {
return new FileLocalCredentialAdministrationDelivery(directory);
},
createPepperProvider() {
return {
resolve() {
return {
pepperKeyId: 'owner-v1',
pepper: PEPPER,
summary: { digest: MATERIAL_DIGEST },
};
},
};
},
randomBytes() {
return Buffer.alloc(32, credentialCalls === 0 ? 84 : 85);
},
now() {
return nowMs;
},
});
const issueCommand = writeCommand(state.commands, 'issue.json', {
schemaVersion: 1,
operation: 'credential.issue',
options: options(state),
request: {
projectId: 'default',
target: { type: 'agent', id: 'agent-planner' },
credentialId: 'agent-planner-primary',
expectedCurrentVersion: 0,
lifetimeMs: 60_000,
mutationId: ISSUE_MUTATION_ID,
requestId: 'managed-credential-issue',
failureAuditEventId: '83000000-0000-4000-8000-000000000002',
},
});
const first = await runner.run(issueCommand);
nowMs = 30_000;
const replay = await runner.run(issueCommand);
assert.equal(first.status, 'inserted');
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.delivery, first.delivery);
const publicResult = JSON.stringify(replay);
assert.equal(publicResult.includes(PEPPER), false);
assert.equal(publicResult.includes('ql3c_'), false);
assert.equal(publicResult.includes(state.delivery), false);
assert.equal(publicResult.includes('secret'), false);
const readyPath = path.join(state.delivery, first.delivery.fileName);
assert.equal(fs.statSync(readyPath).mode & 0o777, 0o600);
const acknowledgeCommand = writeCommand(state.commands, 'ack.json', {
schemaVersion: 1,
operation: 'credential.delivery.acknowledge',
options: options(state),
request: {
projectId: 'default',
credentialMutationId: ISSUE_MUTATION_ID,
expectedDeliveryDigest: first.delivery.digest,
mutationId: ACK_MUTATION_ID,
requestId: 'managed-credential-acknowledge',
failureAuditEventId: '83000000-0000-4000-8000-000000000004',
},
});
const acknowledged = await runner.run(acknowledgeCommand);
const acknowledgedReplay = await runner.run(acknowledgeCommand);
assert.equal(acknowledged.cleanup, 'removed');
assert.equal(acknowledgedReplay.cleanup, 'absent');
assert.equal(fs.existsSync(readyPath), false);
assert.deepEqual(audits, []);
});
test('commits the real SQLite Identity and credential lifecycle behind the Owner fence', async (t) => {
const state = fixture(t);
const ownerCredentialId = 'owner-primary';
const ownerSecret = Buffer.alloc(32, 86).toString('base64url');
const ownerToken = formatApiCredentialToken(ownerCredentialId, ownerSecret);
await migrateLocalSqlitePath({
databasePath: state.databasePath,
profile: 'edge',
});
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: state.keyring,
pepperKeyId: 'owner-v1',
randomBytes: () => Buffer.alloc(32, 83),
});
const nowMs = Date.now();
const ownerDigest = apiCredentialSecretDigest(
PEPPER,
ownerCredentialId,
ownerSecret,
);
const client = new DatabaseSync(state.databasePath);
try {
client
.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(
'owner-v1',
pepperSummary.digest,
'd'.repeat(64),
'84000000-0000-4000-8000-000000000001',
'84000000-0000-4000-8000-000000000002',
nowMs - 2_000,
nowMs - 1_500,
);
client
.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(
'84000000-0000-4000-8000-000000000002',
'owner-v1',
pepperSummary.digest,
'd'.repeat(64),
nowMs - 1_500,
);
client
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
)
.run(nowMs - 1_000, nowMs - 1_000);
client
.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', 'owner-user', ?, ?, ?, ?)`,
)
.run(
ownerCredentialId,
ownerDigest,
nowMs - 1_000,
nowMs - 1_000,
nowMs + 10 * 60_000,
);
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, 'owner-v1')`,
)
.run(ownerCredentialId);
client
.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', 'owner-user', 1, 'active', 'owner',
'owner-binding', 'user', 'owner-user', ?
)`,
)
.run(nowMs - 500);
client
.prepare(
`INSERT INTO "QingLong3Projects" (
"id", "name", "slug", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('secondary', 'Secondary', 'secondary', 'active', 1, ?, ?)`,
)
.run(nowMs - 500, nowMs - 500);
client
.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 (
'secondary', 'user', 'owner-user', 1, 'active', 'owner',
'secondary-owner-binding', 'user', 'owner-user', ?
)`,
)
.run(nowMs - 400);
} finally {
client.close();
}
fs.chmodSync(state.databasePath, 0o600);
fs.writeFileSync(
state.credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: ownerToken,
})}\n`,
{ mode: 0o600 },
);
const register = writeCommand(state.commands, 'register-agent.json', {
schemaVersion: 1,
operation: 'identity.register',
options: {
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
},
request: {
projectId: 'default',
target: { type: 'agent', id: 'agent-real' },
expectedCurrentVersion: 0,
mutationId: '84000000-0000-4000-8000-000000000003',
requestId: 'identity-register-real',
failureAuditEventId: '84000000-0000-4000-8000-000000000004',
},
});
assert.equal(
(await runLocalIdentityCredentialCommandFile(register)).identityStatus,
'active',
);
assert.equal(
(await runLocalIdentityCredentialCommandFile(register)).status,
'existing',
);
const inspectIdentity = writeCommand(
state.commands,
'inspect-agent-identity.json',
{
schemaVersion: 1,
operation: 'identity.inspect',
options: baseOptions(state),
request: {
projectId: 'default',
target: { type: 'agent', id: 'agent-real' },
requestId: 'identity-inspect-real',
auditEventId: '86000000-0000-4000-8000-000000000001',
},
},
);
const inspectedIdentity = await runLocalIdentityCredentialCommandFile(
inspectIdentity,
);
assert.equal(inspectedIdentity.found, true);
assert.equal(inspectedIdentity.version, 1);
assert.equal(inspectedIdentity.identityStatus, 'active');
assert.deepEqual(inspectedIdentity.target, {
type: 'agent',
id: 'agent-real',
});
assert.equal(
Number.isSafeInteger(inspectedIdentity.createdAtMs) &&
inspectedIdentity.createdAtMs === inspectedIdentity.updatedAtMs,
true,
);
const issue = writeCommand(state.commands, 'issue-agent.json', {
schemaVersion: 1,
operation: 'credential.issue',
options: options(state),
request: {
projectId: 'default',
target: { type: 'agent', id: 'agent-real' },
credentialId: 'agent-real-primary',
expectedCurrentVersion: 0,
lifetimeMs: 60_000,
mutationId: '84000000-0000-4000-8000-000000000005',
requestId: 'credential-issue-real',
failureAuditEventId: '84000000-0000-4000-8000-000000000006',
},
});
const issued = await runLocalIdentityCredentialCommandFile(issue);
assert.equal(issued.status, 'inserted');
assert.equal(issued.state, 'active');
assert.equal(
(await runLocalIdentityCredentialCommandFile(issue)).status,
'existing',
);
assert.equal(JSON.stringify(issued).includes('ql3c_'), false);
const inspectCredential = writeCommand(
state.commands,
'inspect-agent-credential.json',
{
schemaVersion: 1,
operation: 'credential.inspect',
options: baseOptions(state),
request: {
projectId: 'default',
credentialId: 'agent-real-primary',
requestId: 'credential-inspect-real',
auditEventId: '86000000-0000-4000-8000-000000000002',
},
},
);
const inspectedCredential = await runLocalIdentityCredentialCommandFile(
inspectCredential,
);
assert.equal(inspectedCredential.found, true);
assert.equal(inspectedCredential.version, 1);
assert.equal(inspectedCredential.state, 'active');
assert.deepEqual(inspectedCredential.target, {
type: 'agent',
id: 'agent-real',
});
const inspectionOutput = JSON.stringify(inspectedCredential);
for (const forbidden of [
ownerDigest,
PEPPER,
'pepperKeyId',
'secretDigest',
'token',
state.deploymentRoot,
]) {
assert.equal(inspectionOutput.includes(forbidden), false);
}
const inspectFromSecondary = writeCommand(
state.commands,
'inspect-credential-from-secondary.json',
{
schemaVersion: 1,
operation: 'credential.inspect',
options: baseOptions(state),
request: {
projectId: 'secondary',
credentialId: 'agent-real-primary',
requestId: 'credential-inspect-secondary-owner',
auditEventId: '86000000-0000-4000-8000-000000000005',
},
},
);
await assert.rejects(
runLocalIdentityCredentialCommandFile(inspectFromSecondary),
LocalIdentityCredentialAdministrationAuthorizationError,
);
const inspectMissing = writeCommand(
state.commands,
'inspect-missing-credential.json',
{
schemaVersion: 1,
operation: 'credential.inspect',
options: baseOptions(state),
request: {
projectId: 'default',
credentialId: 'missing-primary',
requestId: 'credential-inspect-missing',
auditEventId: '86000000-0000-4000-8000-000000000003',
},
},
);
assert.deepEqual(
await runLocalIdentityCredentialCommandFile(inspectMissing),
{
schemaVersion: 1,
operation: 'credential.inspect',
projectId: 'default',
found: false,
},
);
const acknowledge = writeCommand(state.commands, 'ack-agent.json', {
schemaVersion: 1,
operation: 'credential.delivery.acknowledge',
options: options(state),
request: {
projectId: 'default',
credentialMutationId: '84000000-0000-4000-8000-000000000005',
expectedDeliveryDigest: issued.delivery.digest,
mutationId: '84000000-0000-4000-8000-000000000007',
requestId: 'credential-ack-real',
failureAuditEventId: '84000000-0000-4000-8000-000000000008',
},
});
assert.equal(
(await runLocalIdentityCredentialCommandFile(acknowledge)).cleanup,
'removed',
);
const revoke = writeCommand(state.commands, 'revoke-agent.json', {
schemaVersion: 1,
operation: 'credential.revoke',
options: {
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
},
request: {
projectId: 'default',
target: { type: 'agent', id: 'agent-real' },
credentialId: 'agent-real-primary',
expectedCurrentVersion: 1,
mutationId: '84000000-0000-4000-8000-000000000009',
requestId: 'credential-revoke-real',
failureAuditEventId: '84000000-0000-4000-8000-00000000000a',
},
});
assert.equal(
(await runLocalIdentityCredentialCommandFile(revoke)).state,
'revoked',
);
const disable = writeCommand(state.commands, 'disable-agent.json', {
schemaVersion: 1,
operation: 'identity.disable',
options: {
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
},
request: {
projectId: 'default',
target: { type: 'agent', id: 'agent-real' },
expectedCurrentVersion: 1,
mutationId: '84000000-0000-4000-8000-00000000000b',
requestId: 'identity-disable-real',
failureAuditEventId: '84000000-0000-4000-8000-00000000000c',
},
});
assert.equal(
(await runLocalIdentityCredentialCommandFile(disable)).identityStatus,
'disabled',
);
const disableOwner = writeCommand(state.commands, 'disable-owner.json', {
schemaVersion: 1,
operation: 'identity.disable',
options: {
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
},
request: {
projectId: 'default',
target: { type: 'user', id: 'owner-user' },
expectedCurrentVersion: 1,
mutationId: '84000000-0000-4000-8000-00000000000d',
requestId: 'identity-disable-owner-rejected',
failureAuditEventId: '84000000-0000-4000-8000-00000000000e',
},
});
await assert.rejects(
runLocalIdentityCredentialCommandFile(disableOwner),
LocalIdentityOwnerBindingConflictError,
);
const revokeOwner = writeCommand(state.commands, 'revoke-owner.json', {
schemaVersion: 1,
operation: 'credential.revoke',
options: {
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
profile: 'edge',
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
},
request: {
projectId: 'default',
target: { type: 'user', id: 'owner-user' },
credentialId: ownerCredentialId,
expectedCurrentVersion: 1,
mutationId: '84000000-0000-4000-8000-00000000000f',
requestId: 'credential-revoke-owner-rejected',
failureAuditEventId: '85000000-0000-4000-8000-000000000001',
},
});
await assert.rejects(
runLocalIdentityCredentialCommandFile(revokeOwner),
LocalCredentialOwnerContinuityError,
);
const fenceDatabase =
await openLocalSqliteIdentityCredentialAdministrationDatabase({
databasePath: state.databasePath,
profile: 'edge',
});
try {
const authenticated = await establishAuthenticatedLocalCommand(
fenceDatabase,
{
deploymentRoot: state.deploymentRoot,
databasePath: state.databasePath,
ownerPepperKeyringDirectory: state.keyring,
credentialFilePath: state.credentialFilePath,
authenticationNamespace: 'local_identity_admin',
},
);
await authenticated.confirm();
fenceDatabase.activateUserCredentialFence(authenticated.databaseFence);
await assert.rejects(
fenceDatabase.identityCredentialAdministration.inspectAuthorizedIdentity({
target: { type: 'agent', id: 'agent-real' },
authorization: {
projectId: 'secondary',
actor: authenticated.principal.subject,
fence: { projectVersion: 1, bindingVersion: 1 },
},
audit: {
eventId: '86000000-0000-4000-8000-000000000006',
requestId: 'identity-inspect-repository-scope-bypass',
operationId: 'identity.inspect',
projectId: 'secondary',
subject: authenticated.principal.subject,
authenticationId: authenticated.principal.authenticationId,
outcome: 'allowed',
reasons: ['owner_identity_inspect'],
fence: { projectVersion: 1, bindingVersion: 1 },
occurredAtMs: Date.now(),
},
}),
LocalIdentityCredentialAuthorizationFenceConflictError,
);
let changedFence = false;
const repository = new Proxy(
fenceDatabase.identityCredentialAdministration,
{
get(target, property) {
if (property === 'inspectAuthorizedIdentity') {
return async (command) => {
if (!changedFence) {
changedFence = true;
const writer = new DatabaseSync(state.databasePath);
try {
writer
.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', 'owner-user', 2, 'active', 'admin',
'owner-binding-demoted', 'user', 'owner-user', ?
)`,
)
.run(Date.now());
} finally {
writer.close();
}
}
return target.inspectAuthorizedIdentity(command);
};
}
const value = Reflect.get(target, property, target);
return typeof value === 'function' ? value.bind(target) : value;
},
},
);
const service = createLocalIdentityCredentialAdministrationService(
fenceDatabase.projectPolicy,
repository,
);
await assert.rejects(
service.inspectIdentity({
projectId: 'default',
target: { type: 'agent', id: 'agent-real' },
auditEventId: '86000000-0000-4000-8000-000000000004',
requestId: 'identity-inspect-fence-changed',
principal: authenticated.principal,
}),
LocalIdentityCredentialAuthorizationFenceConflictError,
);
} finally {
await fenceDatabase.close();
}
const anchorWriter = new DatabaseSync(state.databasePath);
try {
for (const audit of [
{
eventId: '87000000-0000-4000-8000-000000000001',
requestId: 'secondary-bootstrap-issue',
operationId: 'owner.bootstrap.issue',
occurredAtMs: nowMs - 900,
},
{
eventId: '87000000-0000-4000-8000-000000000002',
requestId: 'secondary-bootstrap-claim',
operationId: 'owner.bootstrap.claim',
occurredAtMs: nowMs - 800,
},
]) {
anchorWriter
.prepare(
`INSERT INTO "QingLong3SecurityAuditEvents" (
"event_id", "request_id", "operation_id", "project_id",
"subject_type", "subject_id", "authentication_id", "outcome",
"reasons_json", "fence_project_version",
"fence_binding_version", "occurred_at_ms"
) VALUES (?, ?, ?, 'secondary', 'user', 'owner-user',
'bootstrap-anchor-test', 'allowed', '["test_anchor"]',
1, NULL, ?)`,
)
.run(
audit.eventId,
audit.requestId,
audit.operationId,
audit.occurredAtMs,
);
}
anchorWriter
.prepare(
`INSERT INTO "QingLong3LocalOwnerBootstrapChallenges" (
"project_id", "version", "issue_mutation_id", "issue_request_id",
"challenge_id", "token_digest", "issuer_authentication_id",
"issuer_authenticated_at_ms", "issuer_expires_at_ms",
"issued_at_ms", "expires_at_ms", "issue_audit_event_id",
"consumed_at_ms", "claim_mutation_id", "claim_request_id",
"claimed_subject_type", "claimed_subject_id", "credential_id",
"credential_version", "claim_authentication_id",
"claim_authenticated_at_ms", "claim_expires_at_ms",
"claim_assurance", "claim_audit_event_id"
) VALUES (
'secondary', 1, ?, 'secondary-bootstrap-issue',
'AAAAAAAAAAAAAAAAAAAAAA', ?, 'bootstrap-anchor-test',
?, ?, ?, ?, ?, ?, ?, 'secondary-bootstrap-claim',
'user', 'owner-user', ?, 1, 'bootstrap-anchor-test',
?, ?, 'single_factor', ?
)`,
)
.run(
'87000000-0000-4000-8000-000000000001',
'f'.repeat(64),
nowMs - 1_000,
nowMs + 60_000,
nowMs - 900,
nowMs + 60_000,
'87000000-0000-4000-8000-000000000001',
nowMs - 800,
'87000000-0000-4000-8000-000000000002',
ownerCredentialId,
nowMs - 1_000,
nowMs + 60_000,
'87000000-0000-4000-8000-000000000002',
);
} finally {
anchorWriter.close();
}
const anchoredDatabase =
await openLocalSqliteIdentityCredentialAdministrationDatabase({
databasePath: state.databasePath,
profile: 'edge',
});
try {
assert.equal(
await anchoredDatabase.identityCredentialAdministration.resolveAuthorityProjectId(),
'secondary',
);
} finally {
await anchoredDatabase.close();
}
const read = new DatabaseSync(state.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...read
.prepare(
`SELECT "status", "version"
FROM "QingLong3IdentitySubjects"
WHERE "subject_type" = 'agent' AND "subject_id" = 'agent-real'`,
)
.get(),
},
{ status: 'disabled', version: 2 },
);
assert.deepEqual(
{
...read
.prepare(
`SELECT "state", "version"
FROM "QingLong3ApiCredentials"
WHERE "credential_id" = 'agent-real-primary'
ORDER BY "version" DESC LIMIT 1`,
)
.get(),
},
{ state: 'revoked', version: 2 },
);
assert.equal(
read
.prepare(
`SELECT "state"
FROM "QingLong3ApiCredentials"
WHERE "credential_id" = ?
ORDER BY "version" DESC LIMIT 1`,
)
.get(ownerCredentialId).state,
'active',
);
assert.equal(
read
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3ApiCredentialAdministrationMutations"
WHERE "credential_id" = 'agent-real-primary'`,
)
.get().count,
2,
);
assert.equal(
read
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3ApiCredentialDeliveryAcknowledgements"
WHERE "credential_mutation_id" =
'84000000-0000-4000-8000-000000000005'`,
)
.get().count,
1,
);
assert.equal(
read
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" IN ('identity.inspect', 'credential.inspect')
AND "outcome" = 'allowed'`,
)
.get().count,
3,
);
assert.deepEqual(
{
...read
.prepare(
`SELECT "outcome", "reasons_json" AS "reasonsJson"
FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = '86000000-0000-4000-8000-000000000005'`,
)
.get(),
},
{
outcome: 'denied',
reasonsJson: '["instance_authority_project_required"]',
},
);
assert.deepEqual(read.prepare('PRAGMA foreign_key_check').all(), []);
} finally {
read.close();
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,211 @@
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const CREDENTIAL_ID = 'automation-owner';
const PEPPER_KEY_ID = 'automation-owner-v1';
const PEPPER = Buffer.alloc(32, 101).toString('base64url');
const CREDENTIAL_SECRET = Buffer.alloc(32, 102).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, CREDENTIAL_SECRET);
async function localManagementFixture(t, { role = 'owner' } = {}) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-automation-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: ownerPepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 101),
});
const now = Date.now();
const secretDigest = apiCredentialSecretDigest(
PEPPER,
CREDENTIAL_ID,
CREDENTIAL_SECRET,
);
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(
PEPPER_KEY_ID,
pepperSummary.digest,
'f'.repeat(64),
'91000000-0000-4000-8000-000000000001',
'91000000-0000-4000-8000-000000000002',
now - 2_000,
now - 1_500,
);
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(
'91000000-0000-4000-8000-000000000002',
PEPPER_KEY_ID,
pepperSummary.digest,
'f'.repeat(64),
now - 1_500,
);
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'automation-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
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', 'automation-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
secretDigest,
now - 1_000,
now - 1_000,
now + 10 * 60 * 1_000,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
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', 'automation-user', 1, 'active', ?,
'automation-owner-binding', 'user', 'automation-user', ?
)`,
)
.run(role, now - 500);
} finally {
database.close();
}
fs.chmodSync(databasePath, 0o600);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
return {
deploymentRoot,
commandsDirectory,
databasePath,
now,
options: {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory,
credentialFilePath,
},
};
}
function writeCommand(value, operation, request, name) {
const filePath = path.join(value.commandsDirectory, `${name}.json`);
fs.writeFileSync(
filePath,
`${JSON.stringify({
schemaVersion: 1,
operation,
options: value.options,
request,
})}\n`,
{ mode: 0o600 },
);
return filePath;
}
function taskPutRequest(value, suffix, overrides = {}) {
return {
projectId: 'default',
taskId: 'task-trigger-product',
expectedRevision: null,
mutationId: `92000000-0000-4000-8000-00000000000${suffix}`,
requestId: `automation-task-put-${suffix}`,
failureAuditEventId: `93000000-0000-4000-8000-00000000000${suffix}`,
name: 'Trigger product task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/echo',
args: ['not-returned'],
},
},
},
labels: { owner: 'product' },
enabled: true,
occurredAtMs: value.now,
...overrides,
};
}
function auditRows(databasePath) {
const database = new DatabaseSync(databasePath, { readOnly: true });
try {
return database
.prepare(
`SELECT event_id AS "eventId", operation_id AS "operationId",
outcome, reasons_json AS "reasonsJson"
FROM "QingLong3SecurityAuditEvents" ORDER BY occurred_at_ms, event_id`,
)
.all();
} finally {
database.close();
}
}
module.exports = {
auditRows,
localManagementFixture,
taskPutRequest,
writeCommand,
};
@@ -0,0 +1,105 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LocalReadinessConfigurationError,
LocalReadinessIncompatibleError,
inspectLocalReadiness,
parseLocalReadinessArguments,
} = require('../dist/lifecycle/localReadiness.js');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
async function fixture(t, profile = 'edge') {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-readiness-')),
);
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile, busyTimeoutMs: 100 });
return { databasePath, directory, profile };
}
test('inspects the exact fresh Profile schema without exposing its path', async (t) => {
const state = await fixture(t);
const result = await inspectLocalReadiness({
databasePath: state.databasePath,
profile: state.profile,
busyTimeoutMs: 100,
});
assert.equal(result.status, 'ready');
assert.equal(result.profile, 'edge');
assert.equal(result.storage.contractName, 'local-control-core');
assert.equal(result.storage.contractVersion, 43);
assert.equal(result.storage.migrationCount, 86);
assert.equal(result.storage.journalMode, 'delete');
assert.equal(JSON.stringify(result).includes(state.directory), false);
});
test('CLI is explicit, content-free and rejects a non-private database', async (t) => {
const state = await fixture(t, 'standalone');
const cli = path.resolve(
__dirname,
'../dist/lifecycle/localReadinessCli.js',
);
const args = [
cli,
`--database=${state.databasePath}`,
'--profile=standalone',
'--busy-timeout-ms=100',
];
const accepted = spawnSync(process.execPath, args, { encoding: 'utf8' });
assert.equal(accepted.status, 0, accepted.stderr);
const result = JSON.parse(accepted.stdout);
assert.equal(result.storage.journalMode, 'wal');
assert.equal(accepted.stdout.includes(state.directory), false);
fs.chmodSync(state.databasePath, 0o644);
const rejected = spawnSync(process.execPath, args, { encoding: 'utf8' });
assert.equal(rejected.status, 1);
assert.equal(
JSON.parse(rejected.stderr).code,
'QL3_LOCAL_READINESS_CONFIGURATION_INVALID',
);
assert.equal(rejected.stderr.includes(state.directory), false);
});
test('rejects implicit, duplicated or cross-Profile inspection', async (t) => {
assert.throws(
() => parseLocalReadinessArguments([]),
LocalReadinessConfigurationError,
);
assert.deepEqual(
parseLocalReadinessArguments([
'--',
'--database=/private/a.sqlite',
'--profile=edge',
]),
{
databasePath: '/private/a.sqlite',
profile: 'edge',
},
);
assert.throws(
() =>
parseLocalReadinessArguments([
'--database=/private/a.sqlite',
'--database=/private/b.sqlite',
'--profile=edge',
]),
LocalReadinessConfigurationError,
);
const state = await fixture(t, 'edge');
await assert.rejects(
inspectLocalReadiness({
databasePath: state.databasePath,
profile: 'standalone',
busyTimeoutMs: 100,
}),
LocalReadinessIncompatibleError,
);
});
@@ -0,0 +1,144 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalSetupConfigurationError,
executeLocalSetup,
} = require('../dist/lifecycle/localSetup.js');
function fixture(t) {
const deploymentRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-setup-')),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() =>
fs.rmSync(deploymentRoot, { recursive: true, force: true }),
);
const ownerPepperKeyringDirectory = path.join(
deploymentRoot,
'owner-peppers',
);
const ownerPepperBackupDirectory = path.join(
deploymentRoot,
'owner-pepper-backup',
);
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperBackupDirectory, { mode: 0o700 });
const command = {
schemaVersion: 1,
operation: 'local.setup.prepare',
options: {
deploymentRoot,
databasePath: path.join(deploymentRoot, 'qinglong3.sqlite'),
profile: 'edge',
ownerPepperKeyringDirectory,
ownerPepperBackupDirectory,
ownerPepperKeyId: 'owner-v1',
localSecretKeyringPath: path.join(
deploymentRoot,
'local-secret-keyring.json',
),
busyTimeoutMs: 100,
},
request: {
registerMutationId: '00000000-0000-4000-8000-000000000f01',
activateMutationId: '00000000-0000-4000-8000-000000000f02',
registeredAtMs: 1_000,
activatedAtMs: 1_001,
},
};
const commandFilePath = path.join(deploymentRoot, 'setup.json');
fs.writeFileSync(commandFilePath, `${JSON.stringify(command)}\n`, {
mode: 0o600,
});
return { command, commandFilePath, deploymentRoot };
}
test('prepares and exactly replays one fresh local authority set', async (t) => {
const state = fixture(t);
const prepared = await executeLocalSetup(state.command);
assert.equal(prepared.status, 'prepared');
assert.equal(prepared.ownerPepper.registerStatus, 'inserted');
assert.equal(prepared.ownerPepper.activateStatus, 'inserted');
assert.equal(prepared.ownerPepper.generation, 1);
assert.equal(prepared.envelopeKeyring.keyCount, 1);
const replay = await executeLocalSetup(state.command);
assert.equal(replay.status, 'existing');
assert.equal(replay.ownerPepper.registerStatus, 'existing');
assert.equal(replay.ownerPepper.activateStatus, 'existing');
assert.deepEqual(replay.storage, prepared.storage);
const database = new DatabaseSync(state.command.options.databasePath, {
readonly: true,
});
assert.equal(
database.prepare('PRAGMA integrity_check').get().integrity_check,
'ok',
);
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalOwnerPepperKeys"',
)
.get().count,
1,
);
database.close();
const serialized = JSON.stringify([prepared, replay]);
assert.equal(serialized.includes(state.deploymentRoot), false);
assert.equal(/token|material|digest/i.test(serialized), false);
});
test('CLI consumes only a private command file and emits a low-sensitivity replay', async (t) => {
const state = fixture(t);
const cli = path.resolve(__dirname, '../dist/lifecycle/localSetupCli.js');
const first = spawnSync(
process.execPath,
[cli, 'run', '--command-file', state.commandFilePath],
{ encoding: 'utf8' },
);
assert.equal(first.status, 0, first.stderr);
assert.equal(JSON.parse(first.stdout).status, 'prepared');
const second = spawnSync(
process.execPath,
[cli, 'run', '--command-file', state.commandFilePath],
{ encoding: 'utf8' },
);
assert.equal(second.status, 0, second.stderr);
assert.equal(JSON.parse(second.stdout).status, 'existing');
assert.equal(second.stdout.includes(state.deploymentRoot), false);
fs.chmodSync(state.commandFilePath, 0o644);
const rejected = spawnSync(
process.execPath,
[cli, 'run', '--command-file', state.commandFilePath],
{ encoding: 'utf8' },
);
assert.equal(rejected.status, 1);
assert.equal(rejected.stderr.includes(state.deploymentRoot), false);
});
test('rejects widened or non-private setup authorities before mutation', async (t) => {
const state = fixture(t);
await assert.rejects(
executeLocalSetup({
...state.command,
options: { ...state.command.options, unexpected: true },
}),
LocalSetupConfigurationError,
);
fs.chmodSync(state.command.options.ownerPepperBackupDirectory, 0o755);
await assert.rejects(
executeLocalSetup(state.command),
LocalSetupConfigurationError,
);
assert.equal(fs.existsSync(state.command.options.databasePath), false);
});
@@ -0,0 +1,729 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
migrateLocalModelInvocationFeature,
} = require('@qinglong/ai/model-invocation-migration');
const {
LocalModelInvocationFeatureActivationRepository,
createLocalModelInvocationFeatureTransitionCommand,
} = require('@qinglong/ai/local-feature-activation');
const {
createLocalModelPriceCatalogCommandRunner,
runLocalModelPriceCatalogCommandFile,
} = require('@qinglong/local-owner-cli/model-price-command');
const {
establishAuthenticatedLocalCommand,
} = require('@qinglong/local-owner-console/authenticated-command');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
openLocalSqlitePluginPackageManagementDatabase,
} = require('@qinglong/local-sqlite/package-management');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const CREDENTIAL_ID = 'model-price-owner';
const PEPPER_KEY_ID = 'model-price-owner-v1';
const PEPPER = Buffer.alloc(32, 81).toString('base64url');
const SECRET = Buffer.alloc(32, 82).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, SECRET);
const PROVIDER = 'openai-compatible';
const MODEL = 'test-model';
const REVISION = '2026-07-27';
async function fixture(t, { aiReady = true, owner = true } = {}) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-model-price-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
if (aiReady) {
const aiDatabase = new DatabaseSync(databasePath);
try {
await migrateLocalModelInvocationFeature(aiDatabase);
new LocalModelInvocationFeatureActivationRepository(
aiDatabase,
).transition(
createLocalModelInvocationFeatureTransitionCommand({
featureId: 'model-invocation',
expectedGeneration: 0,
expectedState: null,
state: 'active',
mutationId: 'model-price-fixture-feature-activation',
requestId: 'model-price-fixture-feature-request',
expectedMigrationDigest:
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'fresh_database',
backupEvidenceDigest: null,
},
principal: {
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_ai_feature:fixture-proof',
authenticatedAtMs: 1,
expiresAtMs: 301_000,
assurance: 'local_console',
},
}),
);
} finally {
aiDatabase.close();
}
}
const summary = provisionLocalOwnerPepperKey({
keyringDirectory: ownerPepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 81),
});
const now = Date.now();
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(
PEPPER_KEY_ID,
summary.digest,
'b'.repeat(64),
'41000000-0000-4000-8000-000000000001',
'41000000-0000-4000-8000-000000000002',
now - 2_000,
now - 1_500,
);
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(
'41000000-0000-4000-8000-000000000002',
PEPPER_KEY_ID,
summary.digest,
'b'.repeat(64),
now - 1_500,
);
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
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', 'owner-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
now - 1_000,
now - 1_000,
now + 10 * 60 * 1_000,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
if (owner) {
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', 'owner-user', 1, 'active', 'owner',
'model-price-owner-binding', 'user', 'owner-user', ?
)`,
)
.run(now - 500);
}
} finally {
database.close();
}
fs.chmodSync(databasePath, 0o600);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
return {
deploymentRoot,
commandsDirectory,
databasePath,
credentialFilePath,
ownerPepperKeyringDirectory,
options: {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory,
credentialFilePath,
},
};
}
function commandFile(value, operation, request, name, extra = {}) {
const commandPath = path.join(value.commandsDirectory, `${name}.json`);
fs.writeFileSync(
commandPath,
`${JSON.stringify({
schemaVersion: 1,
operation,
options: value.options,
request,
...extra,
})}\n`,
{ mode: 0o600 },
);
return commandPath;
}
function baseRequest(suffix, failureAuditEventId) {
return {
requestId: `model-price-${suffix}`,
failureAuditEventId,
provider: PROVIDER,
model: MODEL,
};
}
function mutationRequest(suffix, failureAuditEventId) {
return {
...baseRequest(suffix, failureAuditEventId),
authorizationId: `model-price-authorization-${suffix}`,
mutationId: `model-price-mutation-${suffix}`,
};
}
function assertNoSensitiveMaterial(result) {
const serialized = JSON.stringify(result);
assert.equal(serialized.includes(TOKEN), false);
assert.equal(serialized.includes(SECRET), false);
assert.doesNotMatch(serialized, /authenticationId|principal|subjectId/);
}
test('runs a replay-safe private local Model Price Catalog lifecycle', async (t) => {
const value = await fixture(t);
const publishFile = commandFile(
value,
'model-price.publish',
{
...mutationRequest('publish-1', '42000000-0000-4000-8000-000000000001'),
priceRevision: REVISION,
currency: 'USD',
inputMicrosPerMillionTokens: 150_000,
outputMicrosPerMillionTokens: 600_000,
},
'01-publish',
);
const published = await runLocalModelPriceCatalogCommandFile(publishFile);
assert.equal(published.status, 'created');
assert.equal(published.publication.priceRevision, REVISION);
assert.equal(
published.authorization.policyRevision,
'local_console_platform_owner_v1',
);
assertNoSensitiveMaterial(published);
const replayed = await runLocalModelPriceCatalogCommandFile(publishFile);
assert.equal(replayed.status, 'existing');
assert.deepEqual(replayed.publication, published.publication);
assert.deepEqual(replayed.authorization, published.authorization);
const activateFile = commandFile(
value,
'model-price.activate',
{
...mutationRequest('activate-1', '42000000-0000-4000-8000-000000000002'),
expectedGeneration: 0,
expectedHeadDigest: null,
priceRevision: REVISION,
},
'02-activate',
);
const activated = await runLocalModelPriceCatalogCommandFile(activateFile);
assert.equal(activated.status, 'created');
assert.equal(activated.head.generation, 1);
assert.equal(activated.head.activePriceRevision, REVISION);
const inspectFile = commandFile(
value,
'model-price.inspect',
{
...baseRequest('inspect-1', '42000000-0000-4000-8000-000000000003'),
priceRevision: REVISION,
},
'03-inspect',
);
const inspected = await runLocalModelPriceCatalogCommandFile(inspectFile);
assert.equal(inspected.head.headDigest, activated.head.headDigest);
assert.equal(
inspected.publication.publicationDigest,
published.publication.publicationDigest,
);
assertNoSensitiveMaterial(inspected);
const child = spawnSync(
process.execPath,
[
path.join(
__dirname,
'../dist/ai-management/modelPriceCatalogCli.js',
),
'run',
'--command-file',
inspectFile,
],
{ encoding: 'utf8' },
);
assert.equal(child.status, 0, child.stderr);
assert.equal(child.stderr, '');
assert.equal(JSON.parse(child.stdout).operation, 'model-price.inspect');
assert.equal(child.stdout.includes(TOKEN), false);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...database
.prepare(
`SELECT
(SELECT count(*) FROM "ModelPriceCatalogPublications") AS publications,
(SELECT count(*) FROM "ModelPriceCatalogHeads") AS heads,
(SELECT count(*) FROM "ModelPriceCatalogAuthorizations") AS authorizations`,
)
.get(),
},
{ publications: 1, heads: 1, authorizations: 2 },
);
} finally {
database.close();
}
});
test('fails closed before authentication when AI schema is not activated', async (t) => {
const value = await fixture(t, { aiReady: false });
let authenticated = 0;
const runner = createLocalModelPriceCatalogCommandRunner({
openDatabase: openLocalSqlitePluginPackageManagementDatabase,
async authenticate(...args) {
authenticated += 1;
return establishAuthenticatedLocalCommand(...args);
},
now: Date.now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'model-price.inspect',
{
...baseRequest(
'schema-not-ready',
'43000000-0000-4000-8000-000000000001',
),
priceRevision: null,
},
'schema-not-ready',
),
),
{ code: 'LOCAL_MODEL_INVOCATION_FEATURE_NOT_READY' },
);
assert.equal(authenticated, 0);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT count(*) AS count FROM sqlite_schema
WHERE type = 'table'
AND name LIKE 'ModelPriceCatalog%'`,
)
.get().count,
0,
);
} finally {
database.close();
}
});
test('fails closed before authentication when AI feature is explicitly inactive', async (t) => {
const value = await fixture(t);
const database = new DatabaseSync(value.databasePath);
try {
new LocalModelInvocationFeatureActivationRepository(database).transition(
createLocalModelInvocationFeatureTransitionCommand({
featureId: 'model-invocation',
expectedGeneration: 1,
expectedState: 'active',
state: 'inactive',
mutationId: 'model-price-fixture-feature-deactivation',
requestId: 'model-price-fixture-feature-deactivation-request',
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
safety: {
mode: 'preserve_existing',
backupEvidenceDigest: null,
},
principal: {
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_ai_feature:fixture-proof',
authenticatedAtMs: 1,
expiresAtMs: 301_000,
assurance: 'local_console',
},
}),
);
} finally {
database.close();
}
let authenticated = 0;
const runner = createLocalModelPriceCatalogCommandRunner({
openDatabase: openLocalSqlitePluginPackageManagementDatabase,
async authenticate(...args) {
authenticated += 1;
return establishAuthenticatedLocalCommand(...args);
},
now: Date.now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'model-price.inspect',
{
...baseRequest(
'feature-inactive',
'43000000-0000-4000-8000-000000000002',
),
priceRevision: null,
},
'feature-inactive',
),
),
{ code: 'LOCAL_MODEL_INVOCATION_FEATURE_TRANSITION_UNAVAILABLE' },
);
assert.equal(authenticated, 0);
});
test('rejects caller-supplied authority fields before opening SQLite', async (t) => {
const value = await fixture(t);
let opened = 0;
const runner = createLocalModelPriceCatalogCommandRunner({
async openDatabase() {
opened += 1;
throw new Error('must not open');
},
authenticate: establishAuthenticatedLocalCommand,
now: Date.now,
});
const request = {
...baseRequest('widened', '44000000-0000-4000-8000-000000000001'),
priceRevision: null,
principal: { subject: { type: 'user', id: 'attacker' } },
};
await assert.rejects(
runner.run(commandFile(value, 'model-price.inspect', request, 'widened')),
{ code: 'LOCAL_MODEL_PRICE_CATALOG_COMMAND_CONFIGURATION_INVALID' },
);
assert.equal(opened, 0);
});
test('audits invalid credentials with exact low-sensitive replay', async (t) => {
const value = await fixture(t);
fs.writeFileSync(
value.credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: formatApiCredentialToken(
CREDENTIAL_ID,
Buffer.alloc(32, 99).toString('base64url'),
),
})}\n`,
{ mode: 0o600 },
);
const inspectFile = commandFile(
value,
'model-price.inspect',
{
...baseRequest('bad-credential', '45000000-0000-4000-8000-000000000001'),
priceRevision: null,
},
'bad-credential',
);
await assert.rejects(runLocalModelPriceCatalogCommandFile(inspectFile), {
code: 'AUTHENTICATED_LOCAL_COMMAND_AUTHENTICATION_FAILED',
});
await assert.rejects(runLocalModelPriceCatalogCommandFile(inspectFile), {
code: 'AUTHENTICATED_LOCAL_COMMAND_AUTHENTICATION_FAILED',
});
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
const audits = database
.prepare(
`SELECT outcome, reasons_json AS reasons
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.all('45000000-0000-4000-8000-000000000001')
.map((row) => ({ ...row }));
assert.deepEqual(audits, [
{
outcome: 'authentication_rejected',
reasons: '["credential_rejected"]',
},
]);
} finally {
database.close();
}
});
test('transaction fence blocks credential revocation after precheck', async (t) => {
const value = await fixture(t);
const runner = createLocalModelPriceCatalogCommandRunner({
openDatabase: openLocalSqlitePluginPackageManagementDatabase,
async authenticate(...args) {
const authenticated = await establishAuthenticatedLocalCommand(...args);
let revoked = false;
return {
...authenticated,
async confirm() {
await authenticated.confirm();
if (!revoked) {
revoked = true;
const database = new DatabaseSync(value.databasePath);
try {
database
.prepare(
`UPDATE "QingLong3ApiCredentials"
SET state = 'revoked'
WHERE credential_id = ? AND version = 1`,
)
.run(CREDENTIAL_ID);
} finally {
database.close();
}
}
},
};
},
now: Date.now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'model-price.publish',
{
...mutationRequest(
'revocation-race',
'46000000-0000-4000-8000-000000000001',
),
priceRevision: REVISION,
currency: 'USD',
inputMicrosPerMillionTokens: 150_000,
outputMicrosPerMillionTokens: 600_000,
},
'revocation-race',
),
),
{ code: 'MODEL_PRICE_CATALOG_UNAVAILABLE' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...database
.prepare(
`SELECT
(SELECT count(*) FROM "ModelPriceCatalogPublications") AS publications,
(SELECT count(*) FROM "ModelPriceCatalogAuthorizations") AS authorizations`,
)
.get(),
},
{ publications: 0, authorizations: 0 },
);
assert.deepEqual(
{
...database
.prepare(
`SELECT outcome, reasons_json AS reasons
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('46000000-0000-4000-8000-000000000001'),
},
{
outcome: 'denied',
reasons: '["credential_fence_rejected"]',
},
);
} finally {
database.close();
}
});
test('transaction fence requires the current default Project Owner', async (t) => {
const value = await fixture(t, { owner: false });
await assert.rejects(
runLocalModelPriceCatalogCommandFile(
commandFile(
value,
'model-price.publish',
{
...mutationRequest(
'not-owner',
'47000000-0000-4000-8000-000000000001',
),
priceRevision: REVISION,
currency: 'USD',
inputMicrosPerMillionTokens: 150_000,
outputMicrosPerMillionTokens: 600_000,
},
'not-owner',
),
),
{ code: 'MODEL_PRICE_CATALOG_UNAVAILABLE' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT count(*) AS count
FROM "ModelPriceCatalogPublications"`,
)
.get().count,
0,
);
assert.deepEqual(
{
...database
.prepare(
`SELECT outcome, reasons_json AS reasons
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('47000000-0000-4000-8000-000000000001'),
},
{
outcome: 'denied',
reasons: '["platform_owner_required"]',
},
);
} finally {
database.close();
}
});
test('audits an expired management principal before catalog mutation', async (t) => {
const value = await fixture(t);
const now = Date.now();
const runner = createLocalModelPriceCatalogCommandRunner({
openDatabase: openLocalSqlitePluginPackageManagementDatabase,
async authenticate() {
return {
principal: {
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_model_price:expired-proof',
authenticatedAtMs: now - 10 * 60 * 1_000,
expiresAtMs: now - 1,
assurance: 'local_console',
},
databaseFence: {},
async confirm() {},
};
},
now: () => now,
});
await assert.rejects(
runner.run(
commandFile(
value,
'model-price.publish',
{
...mutationRequest(
'expired-principal',
'48000000-0000-4000-8000-000000000001',
),
priceRevision: REVISION,
currency: 'USD',
inputMicrosPerMillionTokens: 150_000,
outputMicrosPerMillionTokens: 600_000,
},
'expired-principal',
),
),
{ code: 'MODEL_PRICE_CATALOG_MANAGEMENT_AUTHENTICATION_REQUIRED' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT count(*) AS count
FROM "ModelPriceCatalogPublications"`,
)
.get().count,
0,
);
assert.deepEqual(
{
...database
.prepare(
`SELECT outcome, reasons_json AS reasons
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('48000000-0000-4000-8000-000000000001'),
},
{
outcome: 'denied',
reasons: '["strong_authentication_required"]',
},
);
} finally {
database.close();
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,260 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
openLocalSqliteBootstrapDatabase,
} = require('@qinglong/local-sqlite/bootstrap');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
LocalOwnerCliConfigurationError,
createLocalOwnerCommandRunner,
} = require('@qinglong/local-owner-cli');
async function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-owner-cli-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const pepperPath = path.join(deploymentRoot, 'owner.pepper');
const secretDeliveryDirectory = path.join(deploymentRoot, 'secrets');
const commandsDirectory = path.join(deploymentRoot, 'commands');
fs.mkdirSync(secretDeliveryDirectory, { mode: 0o700 });
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.writeFileSync(pepperPath, Buffer.alloc(32, 83).toString('base64url'), {
mode: 0o600,
});
const options = {
deploymentRoot,
databasePath,
pepperPath,
pepperKeyId: 'legacy-v1',
secretDeliveryDirectory,
profile: 'edge',
};
await migrateLocalSqlitePath(options);
const material = fs.readFileSync(pepperPath);
const materialDigest = createHash('sha256')
.update('qinglong.local-owner-pepper.summary.v1\0', 'utf8')
.update(material)
.digest('hex');
material.fill(0);
const database = await openLocalSqliteBootstrapDatabase(options);
await database.ownerPepper.register({
mutationId: '00000000-0000-4000-8000-000000000c91',
pepperKeyId: 'legacy-v1',
materialDigest,
backupDigest: 'b'.repeat(64),
registeredAtMs: 1,
});
await database.ownerPepper.activate({
mutationId: '00000000-0000-4000-8000-000000000c92',
pepperKeyId: 'legacy-v1',
expectedGeneration: 0,
activatedAtMs: 2,
});
await database.close();
return { options, commandsDirectory };
}
function commandFile(state, operation, request, suffix) {
const filePath = path.join(state.commandsDirectory, `${suffix}.json`);
fs.writeFileSync(
filePath,
`${JSON.stringify({
schemaVersion: 1,
operation,
options: state.options,
request,
})}\n`,
{ mode: 0o600 },
);
return filePath;
}
function assertNoSecretFields(value) {
if (!value || typeof value !== 'object') return;
for (const [key, nested] of Object.entries(value)) {
assert.doesNotMatch(key, /secret|token/i);
assertNoSecretFields(nested);
}
}
test('completes fresh Owner and credential recovery ceremonies without returning secrets', async (t) => {
const state = await fixture(t);
const runner = createLocalOwnerCommandRunner();
const credentialMutationId = '00000000-0000-4000-8000-000000000c01';
const challengeMutationId = '00000000-0000-4000-8000-000000000c02';
const claimMutationId = '00000000-0000-4000-8000-000000000c03';
const provisioned = await runner.run(
commandFile(
state,
'owner.identity.provision',
{
mutationId: credentialMutationId,
requestId: 'owner-cli-provision-c01',
},
'01-provision',
),
);
assert.equal(provisioned.status, 'inserted');
assert.equal(provisioned.delivery.kind, 'credential');
const issued = await runner.run(
commandFile(
state,
'owner.challenge.issue',
{
projectId: 'default',
mutationId: challengeMutationId,
requestId: 'owner-cli-issue-c02',
},
'02-issue',
),
);
assert.equal(issued.delivery.kind, 'challenge');
const inspected = await runner.run(
commandFile(
state,
'owner.delivery.inspect',
{ kind: 'challenge', mutationId: challengeMutationId },
'03-inspect',
),
);
assert.equal(
inspected.delivery.deliveryDigest,
issued.delivery.deliveryDigest,
);
const claimFile = commandFile(
state,
'owner.claim.from-deliveries',
{
projectId: 'default',
mutationId: claimMutationId,
requestId: 'owner-cli-claim-c03',
credentialMutationId,
challengeMutationId,
},
'04-claim',
);
const claimed = await runner.run(claimFile);
assert.equal(claimed.status, 'inserted');
assert.equal(claimed.role, 'owner');
assert.equal(JSON.stringify(claimed).includes('secret'), false);
for (const [purpose, mutationId, digest, suffix] of [
[
'credential-provisioning',
credentialMutationId,
provisioned.delivery.deliveryDigest,
'05-ack-credential',
],
[
'challenge',
challengeMutationId,
issued.delivery.deliveryDigest,
'06-ack-challenge',
],
]) {
const acknowledged = await runner.run(
commandFile(
state,
'owner.delivery.acknowledge',
{ purpose, mutationId, expectedDeliveryDigest: digest },
suffix,
),
);
assert.equal(acknowledged.mutationId, mutationId);
}
const replay = await runner.run(claimFile);
assert.equal(replay.status, 'existing');
const recoveryMutationId = '00000000-0000-4000-8000-000000000c04';
const recovery = await runner.run(
commandFile(
state,
'owner.credential-recovery.issue',
{
mutationId: recoveryMutationId,
requestId: 'owner-cli-recovery-c04',
previousCredentialId: provisioned.credentialId,
expectedPreviousVersion: 1,
},
'07-recovery-issue',
),
);
assert.equal(recovery.state, 'issued');
assert.equal(recovery.delivery.kind, 'credential');
await runner.run(
commandFile(
state,
'owner.delivery.acknowledge',
{
purpose: 'credential-recovery',
mutationId: recoveryMutationId,
expectedDeliveryDigest: recovery.delivery.deliveryDigest,
},
'08-recovery-ack',
),
);
const completed = await runner.run(
commandFile(
state,
'owner.credential-recovery.complete',
{
issueMutationId: recoveryMutationId,
mutationId: '00000000-0000-4000-8000-000000000c05',
requestId: 'owner-cli-recovery-complete-c05',
},
'09-recovery-complete',
),
);
assert.equal(completed.state, 'completed');
for (const result of [
provisioned,
issued,
inspected,
claimed,
recovery,
completed,
]) {
assertNoSecretFields(result);
}
});
test('rejects widened command intent and exposes only a command-file binary', async (t) => {
const state = await fixture(t);
const widened = commandFile(
state,
'owner.delivery.inspect',
{
kind: 'credential',
mutationId: '00000000-0000-4000-8000-000000000d01',
secret: 'forbidden',
},
'widened',
);
await assert.rejects(
createLocalOwnerCommandRunner().run(widened),
LocalOwnerCliConfigurationError,
);
const help = spawnSync(
process.execPath,
[path.join(__dirname, '../dist/cli.js'), '--help'],
{ encoding: 'utf8' },
);
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3-owner run --command-file /);
const invalid = spawnSync(
process.execPath,
[path.join(__dirname, '../dist/cli.js'), 'run'],
{ encoding: 'utf8' },
);
assert.equal(invalid.status, 64);
assert.equal(
JSON.parse(invalid.stderr).code,
'LOCAL_OWNER_CLI_USAGE_INVALID',
);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createLocalPluginPackagePromptCommandRunner,
} = require('@qinglong/local-owner-cli/plugin-package-prompt-command');
const {
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
test('Local prompt.inspect reads only the redacted catalog without activating AI', async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-prompt-catalog-'));
fs.chmodSync(root, 0o700);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const source = pluginPackageTaskReconciliationFixture('local-prompt-catalog', {
prompts: [{
schema: 'qinglong/plugin-package-prompt-resource@v1',
id: 'summary',
name: 'Summary',
template: 'Private {{subject}} template.',
parameters: [{ name: 'subject', required: true }],
}],
});
const publication = createInitialPluginPackageAutomationPublication(
source.revision,
source.registry,
1_000,
);
const child = (name) => path.join(root, name);
const commandPath = child('command.json');
fs.writeFileSync(commandPath, JSON.stringify({
schemaVersion: 1,
operation: 'prompt.inspect',
options: {
deploymentRoot: root,
databasePath: child('qinglong3.sqlite'),
profile: 'edge',
ownerPepperKeyringDirectory: child('owner-keys'),
credentialFilePath: child('credential.json'),
},
request: {
projectId: publication.target.projectId,
packageName: publication.target.packageName,
requestId: 'prompt-catalog-request-1',
auditEventId: '00000000-0000-4000-8000-000000000001',
failureAuditEventId: '00000000-0000-4000-8000-000000000002',
},
}), { mode: 0o600 });
const audits = [];
let providerLoads = 0;
let closes = 0;
const principal = {
subject: { type: 'user', id: 'owner-1' },
authenticationId: 'credential-1',
authenticatedAtMs: 1,
expiresAtMs: 10_000,
assurance: 'local_console',
};
const runner = createLocalPluginPackagePromptCommandRunner({
async openDatabase() {
return {
projectPolicy: {
async resolve(projectId, subject) {
return {
project: {
id: projectId,
name: projectId,
slug: projectId,
status: 'active',
version: 1,
createdAtMs: 0,
updatedAtMs: 0,
},
binding: {
projectId,
subject,
version: 1,
state: 'active',
role: 'owner',
mutationId: 'grant-owner',
changedBy: subject,
createdAtMs: 0,
},
};
},
async append() { throw new Error('not used'); },
},
automationPublications: {
async findCurrent() { return publication; },
},
securityAudit: {
async record(audit) { audits.push(audit); },
},
authority: { client: {} },
async close() { closes += 1; },
};
},
async authenticate() {
return {
principal,
databaseFence: {},
async confirm() {},
};
},
async loadProviders() {
providerLoads += 1;
throw new Error('prompt.inspect must not load providers');
},
now: () => 2_000,
});
const result = await runner.run(commandPath);
assert.equal(result.operation, 'prompt.inspect');
assert.equal(result.found, true);
assert.equal(result.prompts[0].id, 'summary');
assert.equal(JSON.stringify(result).includes('Private'), false);
assert.equal(providerLoads, 0);
assert.equal(closes, 1);
assert.equal(audits.length, 1);
assert.equal(audits[0].operationId, 'prompt.inspect');
});
@@ -0,0 +1,918 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
runLocalPluginPackageWorkflowCommandFile,
} = require('@qinglong/local-owner-cli/plugin-package-workflow-command');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('@qinglong/local-sqlite/plugin-package-install');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('@qinglong/local-sqlite/plugin-package-materialized-revision');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('@qinglong/local-sqlite/plugin-package-automation-publication');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const {
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const CREDENTIAL_ID = 'workflow-owner';
const PEPPER_KEY_ID = 'workflow-owner-v1';
const PEPPER = Buffer.alloc(32, 131).toString('base64url');
const CREDENTIAL_SECRET = Buffer.alloc(32, 132).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, CREDENTIAL_SECRET);
async function fixture(t, { role = 'owner' } = {}) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-workflow-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: ownerPepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 131),
});
const now = Date.now();
const workflow = pluginPackageTaskReconciliationFixture(
`workflow-product-${role}`,
{
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [
{ id: 'collect', task: 'alpha', needs: [] },
{ id: 'summarize', task: 'beta', needs: ['collect'] },
],
},
],
},
);
const publication = createInitialPluginPackageAutomationPublication(
workflow.revision,
workflow.registry,
now - 100,
);
const secretDigest = apiCredentialSecretDigest(
PEPPER,
CREDENTIAL_ID,
CREDENTIAL_SECRET,
);
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(
PEPPER_KEY_ID,
pepperSummary.digest,
'f'.repeat(64),
'91000000-0000-4000-8000-000000000001',
'91000000-0000-4000-8000-000000000002',
now - 2_000,
now - 1_500,
);
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(
'91000000-0000-4000-8000-000000000002',
PEPPER_KEY_ID,
pepperSummary.digest,
'f'.repeat(64),
now - 1_500,
);
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'workflow-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
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', 'workflow-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
secretDigest,
now - 1_000,
now - 1_000,
now + 10 * 60_000,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
database
.prepare(
`INSERT INTO "QingLong3Projects" (
"id", "name", "slug", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES (?, ?, ?, 'active', 1, ?, ?)`,
)
.run(
workflow.projectId,
workflow.projectId,
workflow.projectId,
now - 1_000,
now - 1_000,
);
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 (
?, 'user', 'workflow-user', 1, 'active', ?,
?, 'user', 'workflow-user', ?
)`,
)
.run(workflow.projectId, role, `workflow-${role}-binding`, now - 500);
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(database),
workflow,
);
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
database,
workflow.registry,
).publish(workflow.revision);
await new LocalSqlitePluginPackageAutomationPublicationRepository(
database,
).publish(publication);
} finally {
database.close();
}
fs.chmodSync(databasePath, 0o600);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
return {
deploymentRoot,
commandsDirectory,
databasePath,
workflow,
options: {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory,
credentialFilePath,
},
};
}
function writeCommand(value, operation, request, name) {
const filePath = path.join(value.commandsDirectory, `${name}.json`);
fs.writeFileSync(
filePath,
`${JSON.stringify({
schemaVersion: 1,
operation,
options: value.options,
request,
})}\n`,
{ mode: 0o600 },
);
return filePath;
}
function inspectRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
requestId: `workflow-inspect-${suffix}`,
auditEventId: `92000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `93000000-0000-4000-8000-00000000000${suffix}`,
};
}
function startRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
planId: `94000000-0000-4000-8000-00000000000${suffix}`,
runId: `95000000-0000-4000-8000-00000000000${suffix}`,
stepRunIds: {
collect: `96000000-0000-4000-8000-00000000000${suffix}`,
summarize: `97000000-0000-4000-8000-00000000000${suffix}`,
},
requestId: `workflow-start-${suffix}`,
auditEventId: `98000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `99000000-0000-4000-8000-00000000000${suffix}`,
};
}
function cancelRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
runId: `95000000-0000-4000-8000-00000000000${suffix}`,
mutationId: `9a000000-0000-4000-8000-00000000000${suffix}`,
runEventId: `9b000000-0000-4000-8000-00000000000${suffix}`,
requestId: `workflow-cancel-${suffix}`,
auditEventId: `9c000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `9d000000-0000-4000-8000-00000000000${suffix}`,
};
}
function inspectRunRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
runId: `95000000-0000-4000-8000-00000000000${suffix}`,
requestId: `workflow-run-inspect-${suffix}`,
auditEventId: `9e000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `9f000000-0000-4000-8000-00000000000${suffix}`,
};
}
function listRunsRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
limit: 1,
after: null,
requestId: `workflow-run-list-${suffix}`,
auditEventId: `a4000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `a5000000-0000-4000-8000-00000000000${suffix}`,
};
}
function listStepRunsRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
runId: `95000000-0000-4000-8000-00000000000${suffix}`,
limit: 1,
after: null,
requestId: `workflow-step-list-${suffix}`,
auditEventId: `a0000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `a1000000-0000-4000-8000-00000000000${suffix}`,
};
}
function listRunEventsRequest(value, suffix = '1') {
return {
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
runId: `95000000-0000-4000-8000-00000000000${suffix}`,
limit: 2,
afterSequence: 0,
requestId: `workflow-event-list-${suffix}`,
auditEventId: `a2000000-0000-4000-8000-00000000000${suffix}`,
failureAuditEventId: `a3000000-0000-4000-8000-00000000000${suffix}`,
};
}
test('inspects and exactly starts one authenticated Plugin Package Workflow', async (t) => {
const value = await fixture(t);
const inspectPath = writeCommand(
value,
'workflow.inspect',
inspectRequest(value),
'inspect',
);
const inspected = await runLocalPluginPackageWorkflowCommandFile(inspectPath);
assert.deepEqual(inspected.workflows, [
{
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [
{ id: 'collect', task: 'alpha', needs: [] },
{ id: 'summarize', task: 'beta', needs: ['collect'] },
],
},
]);
const startPath = writeCommand(
value,
'workflow.start',
startRequest(value),
'start',
);
const created = await runLocalPluginPackageWorkflowCommandFile(startPath);
assert.deepEqual(created, {
schemaVersion: 1,
operation: 'workflow.start',
status: 'created',
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
runId: '95000000-0000-4000-8000-000000000001',
stepCount: 2,
admittedAtMs: created.admittedAtMs,
});
const replay = await runLocalPluginPackageWorkflowCommandFile(startPath);
assert.equal(replay.status, 'existing');
assert.equal(replay.admittedAtMs, created.admittedAtMs);
const secondStartPath = writeCommand(
value,
'workflow.start',
startRequest(value, '2'),
'start-second',
);
const secondCreated = await runLocalPluginPackageWorkflowCommandFile(
secondStartPath,
);
assert.equal(secondCreated.status, 'created');
const firstRunPagePath = writeCommand(
value,
'workflow.run.list',
listRunsRequest(value),
'list-runs-first',
);
const firstRunPage = await runLocalPluginPackageWorkflowCommandFile(
firstRunPagePath,
);
assert.equal(firstRunPage.operation, 'workflow.run.list');
assert.deepEqual(firstRunPage.after, null);
assert.equal(firstRunPage.runs.length, 1);
assert.equal(firstRunPage.runs[0].runId, secondCreated.runId);
assert.equal(firstRunPage.truncated, true);
assert.deepEqual(firstRunPage.next, {
admittedAtMs: firstRunPage.runs[0].admittedAtMs,
runId: secondCreated.runId,
});
assert.deepEqual(Object.keys(firstRunPage.runs[0]).sort(), [
'admittedAtMs',
'cancelReason',
'cancelRequestedAtMs',
'eventSequence',
'finishedAtMs',
'queuedAtMs',
'runId',
'startedAtMs',
'status',
'stepCount',
'version',
]);
const secondRunPagePath = writeCommand(
value,
'workflow.run.list',
{
...listRunsRequest(value, '2'),
after: firstRunPage.next,
},
'list-runs-second',
);
const secondRunPage = await runLocalPluginPackageWorkflowCommandFile(
secondRunPagePath,
);
assert.deepEqual(
secondRunPage.runs.map(({ runId }) => runId),
[created.runId],
);
assert.equal(secondRunPage.truncated, false);
assert.equal(secondRunPage.next, null);
const serializedRunPage = JSON.stringify(firstRunPage);
for (const forbidden of [
'planDigest',
'receiptDigest',
'definitionDigest',
'inputRef',
'errorSummary',
'leaseOwner',
]) {
assert.equal(serializedRunPage.includes(forbidden), false);
}
const inspectRunPath = writeCommand(
value,
'workflow.run.inspect',
inspectRunRequest(value),
'inspect-run',
);
const inspectedRun = await runLocalPluginPackageWorkflowCommandFile(
inspectRunPath,
);
assert.deepEqual(inspectedRun, {
schemaVersion: 1,
operation: 'workflow.run.inspect',
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
runId: '95000000-0000-4000-8000-000000000001',
found: true,
run: {
status: 'running',
version: 3,
eventSequence: 3,
createdAtMs: created.admittedAtMs,
queuedAtMs: null,
startedAtMs: created.admittedAtMs,
finishedAtMs: null,
cancelRequestedAtMs: null,
cancelReason: null,
},
stepCount: 2,
stepStatusCounts: {
pending: 1,
ready: 1,
waiting_approval: 0,
running: 0,
lost: 0,
succeeded: 0,
failed: 0,
skipped: 0,
cancelled: 0,
timed_out: 0,
},
});
assert.deepEqual(
await runLocalPluginPackageWorkflowCommandFile(inspectRunPath),
inspectedRun,
);
assert.equal(JSON.stringify(inspectedRun).includes('planDigest'), false);
assert.equal(
JSON.stringify(inspectedRun).includes('definitionDigest'),
false,
);
assert.equal(JSON.stringify(inspectedRun).includes('inputRef'), false);
assert.equal(JSON.stringify(inspectedRun).includes('errorSummary'), false);
const firstStepPagePath = writeCommand(
value,
'workflow.step.list',
listStepRunsRequest(value),
'list-steps-first',
);
const firstStepPage = await runLocalPluginPackageWorkflowCommandFile(
firstStepPagePath,
);
assert.equal(firstStepPage.operation, 'workflow.step.list');
assert.equal(firstStepPage.found, true);
assert.equal(firstStepPage.stepRuns.length, 1);
assert.equal(firstStepPage.stepRuns[0].stepKey, 'collect');
assert.equal(firstStepPage.stepRuns[0].status, 'ready');
assert.equal(firstStepPage.truncated, true);
assert.deepEqual(firstStepPage.next, {
stepKey: 'collect',
id: '96000000-0000-4000-8000-000000000001',
});
assert.deepEqual(Object.keys(firstStepPage.stepRuns[0]).sort(), [
'attemptCount',
'createdAtMs',
'finishedAtMs',
'id',
'kind',
'parentStepRunId',
'readyAtMs',
'required',
'resultCode',
'startedAtMs',
'status',
'stepKey',
'updatedAtMs',
'version',
]);
const secondStepPagePath = writeCommand(
value,
'workflow.step.list',
{
...listStepRunsRequest(value, '2'),
runId: '95000000-0000-4000-8000-000000000001',
after: firstStepPage.next,
},
'list-steps-second',
);
const secondStepPage = await runLocalPluginPackageWorkflowCommandFile(
secondStepPagePath,
);
assert.equal(secondStepPage.stepRuns[0].stepKey, 'summarize');
assert.equal(secondStepPage.stepRuns[0].status, 'pending');
assert.equal(secondStepPage.truncated, false);
assert.equal(secondStepPage.next, null);
const serializedStepPage = JSON.stringify(firstStepPage);
for (const forbidden of [
'definitionRef',
'definitionDigest',
'inputRef',
'outputRef',
'approvalRequestId',
'errorSummary',
'lastMutationId',
'stepRunDigest',
]) {
assert.equal(serializedStepPage.includes(forbidden), false);
}
const firstEventPagePath = writeCommand(
value,
'workflow.event.list',
listRunEventsRequest(value),
'list-events-first',
);
const firstEventPage = await runLocalPluginPackageWorkflowCommandFile(
firstEventPagePath,
);
assert.equal(firstEventPage.operation, 'workflow.event.list');
assert.equal(firstEventPage.found, true);
assert.equal(firstEventPage.afterSequence, 0);
assert.equal(firstEventPage.headSequence, 3);
assert.deepEqual(
firstEventPage.events.map(({ sequence }) => sequence),
[1, 2],
);
assert.equal(firstEventPage.truncated, true);
assert.equal(firstEventPage.nextAfterSequence, 2);
assert.deepEqual(Object.keys(firstEventPage.events[0]).sort(), [
'createdAtMs',
'id',
'sequence',
'stepRunId',
'type',
]);
const secondEventPagePath = writeCommand(
value,
'workflow.event.list',
{
...listRunEventsRequest(value, '2'),
runId: '95000000-0000-4000-8000-000000000001',
afterSequence: firstEventPage.nextAfterSequence,
},
'list-events-second',
);
const secondEventPage = await runLocalPluginPackageWorkflowCommandFile(
secondEventPagePath,
);
assert.deepEqual(
secondEventPage.events.map(({ sequence }) => sequence),
[3],
);
assert.equal(secondEventPage.truncated, false);
assert.equal(secondEventPage.nextAfterSequence, null);
const serializedEventPage = JSON.stringify(firstEventPage);
for (const forbidden of [
'payload',
'dedupeKey',
'actorType',
'actorId',
'attemptId',
'errorSummary',
'inputRef',
'outputRef',
]) {
assert.equal(serializedEventPage.includes(forbidden), false);
}
const emptyRunPagePath = writeCommand(
value,
'workflow.run.list',
{
...listRunsRequest(value, '3'),
workflowId: 'other',
},
'list-runs-cross-target',
);
const emptyRunPage = await runLocalPluginPackageWorkflowCommandFile(
emptyRunPagePath,
);
assert.equal(emptyRunPage.workflowId, 'other');
assert.deepEqual(emptyRunPage.runs, []);
assert.equal(emptyRunPage.truncated, false);
assert.equal(emptyRunPage.next, null);
const missingRunPath = writeCommand(
value,
'workflow.run.inspect',
{
...inspectRunRequest(value, '2'),
workflowId: 'other',
runId: '95000000-0000-4000-8000-000000000001',
},
'inspect-run-cross-target',
);
assert.deepEqual(
await runLocalPluginPackageWorkflowCommandFile(missingRunPath),
{
schemaVersion: 1,
operation: 'workflow.run.inspect',
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'other',
runId: '95000000-0000-4000-8000-000000000001',
found: false,
run: null,
stepCount: null,
stepStatusCounts: null,
},
);
const cli = spawnSync(
process.execPath,
[
path.join(
__dirname,
'../dist/plugin-package/pluginPackageWorkflowCli.js',
),
'run',
'--command-file',
startPath,
],
{ encoding: 'utf8' },
);
assert.equal(cli.status, 0, cli.stderr);
assert.equal(JSON.parse(cli.stdout).status, 'existing');
assert.equal(cli.stdout.includes('planDigest'), false);
assert.equal(cli.stdout.includes(value.databasePath), false);
const cancelPath = writeCommand(
value,
'workflow.cancel',
cancelRequest(value),
'cancel',
);
const accepted = await runLocalPluginPackageWorkflowCommandFile(cancelPath);
assert.deepEqual(accepted, {
schemaVersion: 1,
operation: 'workflow.cancel',
status: 'accepted',
projectId: value.workflow.projectId,
packageName: value.workflow.packageName,
workflowId: 'daily',
runId: '95000000-0000-4000-8000-000000000001',
runStatus: 'running',
runVersion: 4,
eventSequence: 4,
cancelRequestedAtMs: accepted.cancelRequestedAtMs,
cancelReason: 'user',
});
const cancelReplay = await runLocalPluginPackageWorkflowCommandFile(
cancelPath,
);
assert.equal(cancelReplay.status, 'existing');
assert.equal(cancelReplay.cancelRequestedAtMs, accepted.cancelRequestedAtMs);
const cancelCli = spawnSync(
process.execPath,
[
path.join(
__dirname,
'../dist/plugin-package/pluginPackageWorkflowCli.js',
),
'run',
'--command-file',
cancelPath,
],
{ encoding: 'utf8' },
);
assert.equal(cancelCli.status, 0, cancelCli.stderr);
assert.equal(JSON.parse(cancelCli.stdout).status, 'existing');
assert.equal(cancelCli.stdout.includes('policy_fence'), false);
assert.equal(cancelCli.stdout.includes(value.databasePath), false);
const secondCancelPath = writeCommand(
value,
'workflow.cancel',
{
...cancelRequest(value, '2'),
runId: '95000000-0000-4000-8000-000000000001',
},
'cancel-already-requested',
);
const alreadyRequested = await runLocalPluginPackageWorkflowCommandFile(
secondCancelPath,
);
assert.equal(alreadyRequested.status, 'already_requested');
assert.equal(
alreadyRequested.cancelRequestedAtMs,
accepted.cancelRequestedAtMs,
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...database
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*) FROM "StepRuns") AS steps,
(SELECT COUNT(*) FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions,
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
WHERE operation_id = 'workflow.start') AS startAudits,
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
WHERE operation_id = 'workflow.cancel') AS cancelAudits,
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
WHERE operation_id = 'workflow.run.read') AS runReadAudits,
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
WHERE operation_id = 'workflow.run.list') AS runListAudits,
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
WHERE operation_id = 'workflow.step.list') AS stepListAudits,
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
WHERE operation_id = 'workflow.event.list') AS eventListAudits,
(SELECT COUNT(*) FROM "RunEvents"
WHERE type = 'run.cancel_requested') AS cancelEvents`,
)
.get(),
},
{
runs: 2,
steps: 4,
admissions: 2,
startAudits: 2,
cancelAudits: 2,
runReadAudits: 2,
runListAudits: 3,
stepListAudits: 2,
eventListAudits: 2,
cancelEvents: 1,
},
);
assert.deepEqual(
{
...database
.prepare(
`SELECT cancel_reason AS "cancelReason",
cancel_requested_at_ms AS "cancelRequestedAtMs"
FROM "Runs" WHERE id = ?`,
)
.get('95000000-0000-4000-8000-000000000001'),
},
{
cancelReason: 'user',
cancelRequestedAtMs: accepted.cancelRequestedAtMs,
},
);
} finally {
database.close();
}
});
test('denies Workflow cancellation without run.stop and preserves the Run', async (t) => {
const value = await fixture(t);
const startPath = writeCommand(
value,
'workflow.start',
startRequest(value, '3'),
'start-before-denied-cancel',
);
await runLocalPluginPackageWorkflowCommandFile(startPath);
const database = new DatabaseSync(value.databasePath);
try {
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 (?, 'user', 'workflow-user', 2, 'active', 'viewer',
?, 'user', 'workflow-user', ?)`,
)
.run(
value.workflow.projectId,
'workflow-viewer-cancel-binding',
Date.now(),
);
} finally {
database.close();
}
const viewerInspectPath = writeCommand(
value,
'workflow.run.inspect',
{
...inspectRunRequest(value, '3'),
runId: '95000000-0000-4000-8000-000000000003',
},
'viewer-inspect-run',
);
const viewerInspection = await runLocalPluginPackageWorkflowCommandFile(
viewerInspectPath,
);
assert.equal(viewerInspection.found, true);
assert.equal(viewerInspection.run.status, 'running');
assert.equal(viewerInspection.stepCount, 2);
const cancelPath = writeCommand(
value,
'workflow.cancel',
cancelRequest(value, '3'),
'viewer-cancel',
);
await assert.rejects(
runLocalPluginPackageWorkflowCommandFile(cancelPath),
(error) =>
error?.code === 'LOCAL_PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_FORBIDDEN',
);
const reader = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...reader
.prepare(
`SELECT cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason"
FROM "Runs" WHERE id = ?`,
)
.get('95000000-0000-4000-8000-000000000003'),
},
{ cancelRequestedAtMs: null, cancelReason: null },
);
assert.equal(
reader
.prepare(
`SELECT COUNT(*) AS count FROM "RunEvents"
WHERE run_id = ? AND type = 'run.cancel_requested'`,
)
.get('95000000-0000-4000-8000-000000000003').count,
0,
);
} finally {
reader.close();
}
});
test('denies Workflow start without run.start and writes no Run', async (t) => {
const value = await fixture(t, { role: 'viewer' });
const startPath = writeCommand(
value,
'workflow.start',
startRequest(value, '2'),
'viewer-start',
);
await assert.rejects(
runLocalPluginPackageWorkflowCommandFile(startPath),
(error) =>
error?.code === 'LOCAL_PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_FORBIDDEN',
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database.prepare('SELECT COUNT(*) AS count FROM "Runs"').get().count,
0,
);
assert.deepEqual(
{
...database
.prepare(
`SELECT outcome, reasons_json AS reasons
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('98000000-0000-4000-8000-000000000002'),
},
{ outcome: 'denied', reasons: '["permission_missing"]' },
);
} finally {
database.close();
}
});
@@ -0,0 +1,199 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const { EventEmitter } = require('node:events');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const packageRoot = path.resolve(__dirname, '..');
const moduleDirectory = path.join(packageRoot, 'dist', 'product-cli');
const cliPath = path.join(moduleDirectory, 'cli.js');
const manifest = JSON.parse(
fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
);
const {
QINGLONG3_PRODUCT_COMMANDS,
loadQingLong3ProductVersion,
qingLong3ProductHelp,
resolveQingLong3ProductCommand,
} = require('../dist/product-cli/productCommand.js');
const {
forwardSignals,
signalExitCode,
} = require('../dist/product-cli/cli.js');
function runCli(args) {
return spawnSync(process.execPath, [cliPath, ...args], {
cwd: packageRoot,
encoding: 'utf8',
});
}
test('catalog maps every product subcommand to an existing same-package binary', () => {
assert.equal(manifest.bin.ql3, 'dist/product-cli/cli.js');
assert.equal(QINGLONG3_PRODUCT_COMMANDS.length, 20);
assert.equal(
new Set(QINGLONG3_PRODUCT_COMMANDS.map(({ name }) => name)).size,
QINGLONG3_PRODUCT_COMMANDS.length,
);
assert.equal(
new Set(QINGLONG3_PRODUCT_COMMANDS.map(({ binary }) => binary)).size,
QINGLONG3_PRODUCT_COMMANDS.length,
);
assert.equal(
QINGLONG3_PRODUCT_COMMANDS.some(
({ binary }) => binary === 'ql3-service-bridge',
),
false,
);
for (const command of QINGLONG3_PRODUCT_COMMANDS) {
assert.equal(manifest.bin[command.binary], `dist/${command.target}`);
assert.equal(
fs.statSync(path.join(packageRoot, 'dist', command.target)).isFile(),
true,
);
}
});
test('help and version are bounded installation-derived product facts', () => {
const help = qingLong3ProductHelp();
assert.match(help, /^Usage: ql3 <command> \[arguments\]/);
assert.match(help, /\n task\s+manage Task definitions\n/);
assert.match(help, /Root service mutation remains isolated/);
assert.equal(help.includes('ql3-service-bridge '), false);
assert.equal(loadQingLong3ProductVersion(moduleDirectory), manifest.version);
assert.deepEqual(resolveQingLong3ProductCommand([], moduleDirectory), {
kind: 'help',
output: help,
});
assert.deepEqual(
resolveQingLong3ProductCommand(['--version'], moduleDirectory),
{ kind: 'version', output: manifest.version },
);
});
test('resolves only a static target and preserves opaque child arguments', () => {
const argv = [
'task',
'run',
'--command-file',
'/private/operator command.json',
'--literal=$() && *',
];
const result = resolveQingLong3ProductCommand(argv, moduleDirectory);
assert.equal(result.kind, 'invoke');
assert.equal(result.command.binary, 'ql3-task');
assert.equal(
result.targetFilePath,
path.join(
packageRoot,
'dist',
'automation-management',
'taskDefinitionCli.js',
),
);
assert.deepEqual(result.argv, argv.slice(1));
assert.equal(Object.isFrozen(result.argv), true);
assert.equal(Object.isFrozen(result), true);
for (const candidate of [
'../../tmp/owned',
'/absolute/command',
'task/../../owned',
'service-bridge',
]) {
const rejected = resolveQingLong3ProductCommand(
[candidate, '--help'],
moduleDirectory,
);
assert.equal(rejected.kind, 'invalid');
assert.equal(rejected.code, 'QL3_PRODUCT_CLI_USAGE_INVALID');
}
});
test('rejects a catalog target that escapes through a symlink', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-product-cli-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const fakeModuleDirectory = path.join(root, 'package', 'dist', 'product-cli');
const targetDirectory = path.join(
root,
'package',
'dist',
'automation-management',
);
fs.mkdirSync(fakeModuleDirectory, { recursive: true });
fs.mkdirSync(targetDirectory, { recursive: true });
const external = path.join(root, 'external.js');
fs.writeFileSync(external, 'process.exit(0);\n');
fs.symlinkSync(external, path.join(targetDirectory, 'taskDefinitionCli.js'));
assert.throws(
() =>
resolveQingLong3ProductCommand(['task', '--help'], fakeModuleDirectory),
/canonical package root/,
);
});
test('product binary exposes help/version and delegates without a shell', () => {
const help = runCli(['--help']);
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3 <command>/);
assert.equal(help.stderr, '');
const version = runCli(['--version']);
assert.equal(version.status, 0);
assert.equal(version.stdout.trim(), manifest.version);
assert.equal(version.stderr, '');
const delegatedHelp = runCli(['task', '--help']);
assert.equal(delegatedHelp.status, 0);
assert.equal(
delegatedHelp.stdout.trim(),
'Usage: ql3-task run --command-file /absolute/private-command.json',
);
assert.equal(delegatedHelp.stderr, '');
const delegatedFailure = runCli(['task']);
assert.equal(delegatedFailure.status, 64);
assert.equal(delegatedFailure.stdout, '');
assert.equal(
JSON.parse(delegatedFailure.stderr).code,
'LOCAL_TASK_DEFINITION_CLI_USAGE_INVALID',
);
const rejected = runCli(['../../tmp/not-a-command']);
assert.equal(rejected.status, 64);
assert.equal(rejected.stdout, '');
const failure = JSON.parse(rejected.stderr);
assert.equal(failure.code, 'QL3_PRODUCT_CLI_USAGE_INVALID');
assert.equal(JSON.stringify(failure).includes('/tmp'), false);
});
test('forwards only bounded signals to the active child and removes handlers', () => {
const signalHost = new EventEmitter();
const received = [];
const child = {
exitCode: null,
signalCode: null,
kill(signal) {
received.push(signal);
return true;
},
};
const remove = forwardSignals(child, signalHost);
signalHost.emit('SIGINT');
signalHost.emit('SIGTERM');
signalHost.emit('SIGHUP');
assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']);
assert.equal(signalExitCode('SIGINT'), 130);
assert.equal(signalExitCode('SIGTERM'), 143);
remove();
signalHost.emit('SIGTERM');
assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']);
child.exitCode = 0;
const removeTerminal = forwardSignals(child, signalHost);
signalHost.emit('SIGTERM');
removeTerminal();
assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,516 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
runLocalSecretCommandFile,
} = require('@qinglong/local-owner-cli/secret-command');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console');
const {
LocalSecretKeyringFileProvider,
provisionLocalSecretKeyring,
} = require('@qinglong/local-secret');
const {
createLocalSecretAdministrationService,
} = require('@qinglong/local-admin/secret-administration');
const {
openLocalSqliteSecretAdministrationDatabase,
} = require('@qinglong/local-sqlite/secret-administration');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
LocalSecretAuthorizationFenceConflictError,
} = require('@qinglong/runtime-core/local-secret-administration');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const { parseSecretRef } = require('@qinglong/runtime-core/secret-reference');
const CREDENTIAL_ID = 'secret-owner';
const PEPPER_KEY_ID = 'secret-owner-v1';
const PEPPER = Buffer.alloc(32, 101).toString('base64url');
const CREDENTIAL_SECRET = Buffer.alloc(32, 102).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, CREDENTIAL_SECRET);
async function fixture(t, { role = 'owner' } = {}) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-secret-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
const secretKeyringPath = path.join(
deploymentRoot,
'local-secret-keyring.json',
);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
await provisionLocalSecretKeyring(secretKeyringPath);
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: ownerPepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 101),
});
const now = Date.now();
const secretDigest = apiCredentialSecretDigest(
PEPPER,
CREDENTIAL_ID,
CREDENTIAL_SECRET,
);
const notBeforeAtMs = now - 1_000;
const expiresAtMs = now + 10 * 60 * 1_000;
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(
PEPPER_KEY_ID,
pepperSummary.digest,
'd'.repeat(64),
'61000000-0000-4000-8000-000000000001',
'61000000-0000-4000-8000-000000000002',
now - 2_000,
now - 1_500,
);
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(
'61000000-0000-4000-8000-000000000002',
PEPPER_KEY_ID,
pepperSummary.digest,
'd'.repeat(64),
now - 1_500,
);
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
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', 'owner-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
secretDigest,
now - 1_000,
notBeforeAtMs,
expiresAtMs,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
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', 'owner-user', 1, 'active', ?,
'secret-owner-binding', 'user', 'owner-user', ?
)`,
)
.run(role, now - 500);
} finally {
database.close();
}
fs.chmodSync(databasePath, 0o600);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
return {
deploymentRoot,
commandsDirectory,
databasePath,
credentialFilePath,
ownerPepperKeyringDirectory,
secretKeyringPath,
pepperSummary,
now,
fence: {
credentialId: CREDENTIAL_ID,
credentialVersion: 1,
pepperKeyId: PEPPER_KEY_ID,
materialDigest: pepperSummary.digest,
subjectType: 'user',
subjectId: 'owner-user',
secretDigest,
notBeforeAtMs,
expiresAtMs,
},
options: {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory,
credentialFilePath,
secretKeyringPath,
},
};
}
function secretValueFile(value, plaintext, name) {
const filePath = path.join(value.commandsDirectory, `${name}.value.json`);
fs.writeFileSync(
filePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-secret-value',
value: plaintext,
})}\n`,
{ mode: 0o600 },
);
return filePath;
}
function commandFile(value, request, name) {
const commandPath = path.join(value.commandsDirectory, `${name}.json`);
fs.writeFileSync(
commandPath,
`${JSON.stringify({
schemaVersion: 1,
operation: 'secret.put',
options: value.options,
request,
})}\n`,
{ mode: 0o600 },
);
return commandPath;
}
function request(value, suffix, expectedCurrentVersion, secretValueFilePath) {
return {
projectId: 'default',
name: 'API_TOKEN',
secretValueFilePath,
mutationId: `62000000-0000-4000-8000-00000000000${suffix}`,
requestId: `secret-command-${suffix}`,
failureAuditEventId: `63000000-0000-4000-8000-00000000000${suffix}`,
expectedCurrentVersion,
};
}
test('creates, replays and rotates an encrypted Secret without echoing material', async (t) => {
const value = await fixture(t);
const firstPlaintext = 'first-secret-never-echo';
const secondPlaintext = 'rotated-secret-never-echo';
const firstFile = commandFile(
value,
request(value, '1', 0, secretValueFile(value, firstPlaintext, 'first')),
'create',
);
const created = await runLocalSecretCommandFile(firstFile);
assert.deepEqual(created, {
schemaVersion: 1,
operation: 'secret.put',
status: 'inserted',
version: 1,
secretRef: created.secretRef,
});
assert.deepEqual(parseSecretRef(created.secretRef), {
projectId: 'default',
name: 'API_TOKEN',
version: 1,
});
assert.equal((await runLocalSecretCommandFile(firstFile)).status, 'existing');
const secondFile = commandFile(
value,
request(value, '2', 1, secretValueFile(value, secondPlaintext, 'second')),
'rotate',
);
const rotated = await runLocalSecretCommandFile(secondFile);
assert.equal(rotated.status, 'inserted');
assert.equal(rotated.version, 2);
assert.deepEqual(parseSecretRef(rotated.secretRef), {
projectId: 'default',
name: 'API_TOKEN',
version: 2,
});
const child = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/security-management/secretCli.js'),
'run',
'--command-file',
secondFile,
],
{ encoding: 'utf8' },
);
assert.equal(child.status, 0, child.stderr);
assert.equal(JSON.parse(child.stdout).status, 'existing');
for (const sensitive of [TOKEN, firstPlaintext, secondPlaintext]) {
assert.equal(JSON.stringify(created).includes(sensitive), false);
assert.equal(child.stdout.includes(sensitive), false);
assert.equal(child.stderr.includes(sensitive), false);
}
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
const secrets = database
.prepare(
`SELECT version, ciphertext
FROM "QingLong3LocalSecretEnvelopes"
WHERE project_id = 'default' AND secret_name = 'API_TOKEN'
ORDER BY version`,
)
.all();
assert.deepEqual(
secrets.map((entry) => entry.version),
[1, 2],
);
const ciphertext = secrets
.map((entry) => Buffer.from(entry.ciphertext).toString('utf8'))
.join('');
assert.equal(ciphertext.includes(firstPlaintext), false);
assert.equal(ciphertext.includes(secondPlaintext), false);
assert.equal(
database
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3SecurityAuditEvents"
WHERE operation_id IN ('secret.create', 'secret.rotate')
AND outcome = 'allowed'`,
)
.get().count,
2,
);
} finally {
database.close();
}
});
test('rejects a non-private value file and records only a low-sensitive failure', async (t) => {
const value = await fixture(t);
const plaintext = 'must-not-appear-in-audit';
const secretFile = secretValueFile(value, plaintext, 'public');
fs.chmodSync(secretFile, 0o644);
const command = commandFile(
value,
request(value, '3', 0, secretFile),
'public-value',
);
await assert.rejects(runLocalSecretCommandFile(command), {
code: 'LOCAL_SECRET_COMMAND_CONFIGURATION_INVALID',
});
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
const row = database
.prepare(
`SELECT outcome, reasons_json
FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('63000000-0000-4000-8000-000000000003');
assert.deepEqual(
{ ...row },
{
outcome: 'denied',
reasons_json: '["secret_value_rejected"]',
},
);
assert.equal(JSON.stringify(row).includes(plaintext), false);
assert.equal(
database
.prepare(
`SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"`,
)
.get().count,
0,
);
} finally {
database.close();
}
});
test('keeps viewer policy denied and never touches the Secret key', async (t) => {
const value = await fixture(t, { role: 'viewer' });
const command = commandFile(
value,
request(
value,
'4',
0,
secretValueFile(value, 'viewer-cannot-write', 'viewer'),
),
'viewer',
);
await assert.rejects(runLocalSecretCommandFile(command), {
code: 'LOCAL_SECRET_ADMINISTRATION_FORBIDDEN',
});
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT outcome FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('62000000-0000-4000-8000-000000000004').outcome,
'denied',
);
assert.equal(
database
.prepare(
`SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"`,
)
.get().count,
0,
);
} finally {
database.close();
}
});
test('rejects a revoked credential before Secret materialization', async (t) => {
const value = await fixture(t);
const database = new DatabaseSync(value.databasePath);
try {
database
.prepare(
`UPDATE "QingLong3ApiCredentials"
SET state = 'revoked'
WHERE credential_id = ? AND version = 1`,
)
.run(CREDENTIAL_ID);
} finally {
database.close();
}
const command = commandFile(
value,
request(
value,
'5',
0,
secretValueFile(value, 'revoked-cannot-write', 'revoked'),
),
'revoked',
);
await assert.rejects(runLocalSecretCommandFile(command), {
code: 'AUTHENTICATED_LOCAL_COMMAND_AUTHENTICATION_FAILED',
});
const read = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
read
.prepare(
`SELECT outcome FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get('63000000-0000-4000-8000-000000000005').outcome,
'authentication_rejected',
);
assert.equal(
read
.prepare(
`SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"`,
)
.get().count,
0,
);
} finally {
read.close();
}
});
test('rechecks the credential fence inside the Secret write transaction', async (t) => {
const value = await fixture(t);
const database = await openLocalSqliteSecretAdministrationDatabase({
databasePath: value.databasePath,
profile: 'edge',
});
t.after(() => database.close());
database.activateUserCredentialFence(value.fence);
const mutator = new DatabaseSync(value.databasePath);
try {
mutator
.prepare(
`UPDATE "QingLong3ApiCredentials"
SET state = 'revoked'
WHERE credential_id = ? AND version = 1`,
)
.run(CREDENTIAL_ID);
} finally {
mutator.close();
}
const service = createLocalSecretAdministrationService(
database.projectPolicy,
database.localSecretAdministration,
database.securityAudit,
new LocalSecretKeyringFileProvider(value.secretKeyringPath),
);
await assert.rejects(
service.put({
projectId: 'default',
name: 'ATOMIC_FENCE',
plaintext: 'must-not-commit',
mutationId: '62000000-0000-4000-8000-000000000006',
requestId: 'secret-command-atomic-fence',
expectedCurrentVersion: 0,
principal: {
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_secret:atomic-fence',
authenticatedAtMs: value.now - 1_000,
expiresAtMs: value.now + 60_000,
assurance: 'local_console',
},
}),
LocalSecretAuthorizationFenceConflictError,
);
const read = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
read
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3LocalSecretEnvelopes"
WHERE secret_name = 'ATOMIC_FENCE'`,
)
.get().count,
0,
);
} finally {
read.close();
}
});
@@ -0,0 +1,350 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createLocalSecurityAuditQueryCommandRunner,
} = require('@qinglong/local-owner-cli/security-audit-query-command');
const {
LocalSqliteAuthenticatedManagementFenceError,
} = require('@qinglong/local-sqlite/authenticated-management');
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner-user' }),
authenticationId: 'local_security_audit:test',
authenticatedAtMs: 1_000,
expiresAtMs: 61_000,
assurance: 'local_console',
});
function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-audit-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandPath = path.join(deploymentRoot, 'command.json');
const value = {
schemaVersion: 1,
operation: 'security.audit.list',
options: {
deploymentRoot,
databasePath: path.join(deploymentRoot, 'qinglong3.sqlite'),
profile: 'edge',
ownerPepperKeyringDirectory: path.join(deploymentRoot, 'owner-keys'),
credentialFilePath: path.join(deploymentRoot, 'credential.json'),
},
request: {
authorityProjectId: 'default',
query: { limit: 1, filter: {} },
requestId: 'audit-query-cli',
auditEventId: '94000000-0000-4000-8000-000000000001',
},
};
fs.writeFileSync(commandPath, `${JSON.stringify(value)}\n`, {
mode: 0o600,
});
return { commandPath, value };
}
function compactionFixture(t, requestOverrides = {}) {
const value = fixture(t);
value.value.operation = 'security.audit.compact';
value.value.request = {
authorityProjectId: 'default',
retentionMs: 2_592_000_000,
eligibleBeforeMs: 1_000,
limit: 64,
mutationId: '94100000-0000-4000-8000-000000000001',
requestId: 'audit-compact-cli',
failureAuditEventId: '94100000-0000-4000-8000-000000000002',
...requestOverrides,
};
fs.writeFileSync(value.commandPath, `${JSON.stringify(value.value)}\n`, {
mode: 0o600,
});
return value;
}
function authenticated() {
return {
principal: PRINCIPAL,
databaseFence: {
credentialId: 'owner',
credentialVersion: 1,
pepperKeyId: 'owner-v1',
materialDigest: 'a'.repeat(64),
subjectType: 'user',
subjectId: 'owner-user',
secretDigest: 'b'.repeat(64),
notBeforeAtMs: 0,
expiresAtMs: 60_000,
},
async confirm() {},
};
}
test('returns bounded audit rows without authentication identifiers', async (t) => {
const value = fixture(t);
let closed = false;
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record() {
throw new Error('not used');
},
},
activateUserCredentialFence() {},
async close() {
closed = true;
},
};
},
async authenticate() {
return authenticated();
},
createService() {
return {
async list() {
return {
records: [
{
eventId: '95000000-0000-4000-8000-000000000001',
requestId: 'denied-operation',
operationId: 'tool.invoke',
projectId: 'project-alpha',
subject: { type: 'agent', id: 'planner' },
authenticationId: 'must-never-be-returned',
outcome: 'denied',
reasons: ['permission_missing'],
fence: { projectVersion: 2, bindingVersion: 3 },
occurredAtMs: 1_500,
},
],
nextCursor: null,
audit: {},
};
},
};
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_000,
});
const result = await runner.run(value.commandPath);
assert.equal(closed, true);
assert.equal(result.records.length, 1);
assert.equal(Object.hasOwn(result.records[0], 'authenticationId'), false);
assert.equal(
JSON.stringify(result).includes('must-never-be-returned'),
false,
);
});
test('audits a final credential fence rejection and closes the database', async (t) => {
const value = fixture(t);
const audits = [];
let closed = false;
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record(audit) {
audits.push(audit);
},
},
activateUserCredentialFence() {
throw new LocalSqliteAuthenticatedManagementFenceError();
},
async close() {
closed = true;
},
};
},
async authenticate() {
return authenticated();
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_000,
});
await assert.rejects(
runner.run(value.commandPath),
LocalSqliteAuthenticatedManagementFenceError,
);
assert.equal(closed, true);
assert.equal(audits.length, 1);
assert.deepEqual(
{
eventId: audits[0].eventId,
outcome: audits[0].outcome,
reasons: audits[0].reasons,
},
{
eventId: value.value.request.auditEventId,
outcome: 'denied',
reasons: ['credential_or_policy_fence_rejected'],
},
);
});
test('returns a redacted bounded compaction receipt through the existing audit CLI', async (t) => {
const value = compactionFixture(t);
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record() {
throw new Error('not used');
},
},
activateUserCredentialFence() {},
async close() {},
};
},
async authenticate() {
return authenticated();
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
return {
async compact(request) {
assert.equal(
request.failureAuditEventId,
value.value.request.failureAuditEventId,
);
return {
status: 'inserted',
record: {
mutationId: request.mutationId,
requestId: request.requestId,
authorityProjectId: request.authorityProjectId,
retentionMs: request.retentionMs,
eligibleBeforeMs: request.eligibleBeforeMs,
batchLimit: request.limit,
deletedCount: 3,
deletedPayloadBytes: 400,
first: {
occurredAtMs: 10,
eventId: '94200000-0000-4000-8000-000000000001',
},
last: {
occurredAtMs: 20,
eventId: '94200000-0000-4000-8000-000000000003',
},
recordsDigest: 'a'.repeat(64),
createdAtMs: 2_592_002_000,
},
audit: {
authenticationId: 'must-never-be-returned',
},
};
},
};
},
now: () => 2_592_002_000,
});
const result = await runner.run(value.commandPath);
assert.deepEqual(
{
operation: result.operation,
status: result.status,
deletedCount: result.deletedCount,
batchLimit: result.batchLimit,
},
{
operation: 'security.audit.compact',
status: 'inserted',
deletedCount: 3,
batchLimit: 64,
},
);
assert.equal(JSON.stringify(result).includes('authenticationId'), false);
assert.equal(JSON.stringify(result).includes('authorityProjectId'), false);
assert.equal(JSON.stringify(result).includes('requestId'), false);
});
test('rejects an Edge compaction batch above 64 before opening SQLite', async (t) => {
const value = compactionFixture(t, { limit: 65 });
let opened = false;
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
opened = true;
throw new Error('must not run');
},
async authenticate() {
throw new Error('must not run');
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_592_002_000,
});
await assert.rejects(
runner.run(value.commandPath),
/compaction identity, retention fence, or limit is invalid/,
);
assert.equal(opened, false);
});
test('uses the separate failure event for a compaction credential fence rejection', async (t) => {
const value = compactionFixture(t);
const audits = [];
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record(record) {
audits.push(record);
},
},
activateUserCredentialFence() {
throw new LocalSqliteAuthenticatedManagementFenceError();
},
async close() {},
};
},
async authenticate() {
return authenticated();
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_592_002_000,
});
await assert.rejects(
runner.run(value.commandPath),
LocalSqliteAuthenticatedManagementFenceError,
);
assert.equal(audits.length, 1);
assert.equal(audits[0].eventId, value.value.request.failureAuditEventId);
assert.equal(audits[0].outcome, 'denied');
});
@@ -0,0 +1,186 @@
const assert = require('node:assert/strict');
const path = require('node:path');
const { test } = require('node:test');
const {
localServiceManagerIntentDigest,
normalizeLocalServiceBridgeCommand,
normalizeLocalServiceManagerIntent,
} = require('../dist/deployment/service-manager/serviceBridgeContract.js');
const {
LocalDeploymentConfigurationError,
} = require('../dist/deployment/foundation/contract.js');
function intent(overrides = {}) {
const root = '/opt/qinglong3';
const actionId = '123e4567-e89b-42d3-a456-426614174001';
const payload = {
schemaVersion: 1,
kind: 'qinglong3-local-service-manager-intent',
actionId,
action: 'install-enable-start',
profile: 'edge',
instanceId: 'router-edge-1',
service: {
kind: 'systemd',
name: 'qinglong3',
uid: 1000,
gid: 1000,
allowRootService: false,
},
deployment: {
root,
applicationConfigPath: path.join(root, 'local-application.json'),
applicationConfigSha256: 'e'.repeat(64),
},
descriptor: {
sourcePath: path.join(root, 'service/qinglong3.service'),
destinationPath: '/etc/systemd/system/qinglong3.service',
sha256: 'a'.repeat(64),
sourceMode: 0o600,
destinationMode: 0o644,
},
lineage: { mode: 'fresh' },
outcomePath: path.join(
root,
'service/service-manager-outcomes',
`${actionId}.json`,
),
requestedAtMs: 1786416000000,
...overrides,
};
return { ...payload, intentDigest: localServiceManagerIntentDigest(payload) };
}
test('normalizes exact systemd fresh and OpenRC adopted intents', () => {
assert.deepEqual(normalizeLocalServiceManagerIntent(intent()), intent());
const root = '/opt/qinglong3-openrc';
const actionId = '123e4567-e89b-42d3-a456-426614174002';
const openrc = intent({
actionId,
action: 'restart',
service: {
kind: 'openrc',
name: 'qinglong3',
uid: 0,
gid: 0,
allowRootService: true,
},
deployment: {
root,
applicationConfigPath: path.join(root, 'local-application.json'),
applicationConfigSha256: 'f'.repeat(64),
},
descriptor: {
sourcePath: path.join(root, 'service/qinglong3.openrc'),
destinationPath: '/etc/init.d/qinglong3',
sha256: 'b'.repeat(64),
sourceMode: 0o700,
destinationMode: 0o755,
},
lineage: {
mode: 'adopted',
cutoverId: 'router-edge-1-cutover',
generation: 2,
expectedActivationDigest: 'c'.repeat(64),
previousRecordDigest: 'd'.repeat(64),
},
outcomePath: path.join(
root,
'service/service-manager-outcomes',
`${actionId}.json`,
),
});
assert.deepEqual(normalizeLocalServiceManagerIntent(openrc), openrc);
});
test('rejects arbitrary destinations, root drift, digest drift and unknown fields', () => {
for (const candidate of [
intent({
descriptor: {
...intent().descriptor,
destinationPath: '/etc/systemd/system/other.service',
},
}),
intent({
service: { ...intent().service, uid: 0 },
}),
{ ...intent(), intentDigest: 'f'.repeat(64) },
{ ...intent(), shell: 'systemctl start qinglong3' },
intent({
action: 'restart',
lineage: {
mode: 'adopted',
cutoverId: 'router-edge-1-cutover',
generation: 1,
expectedActivationDigest: 'c'.repeat(64),
previousRecordDigest: 'd'.repeat(64),
},
}),
intent({
action: 'start',
lineage: {
mode: 'adopted',
cutoverId: 'router-edge-1-cutover',
generation: 2,
expectedActivationDigest: 'c'.repeat(64),
previousRecordDigest: 'd'.repeat(64),
},
}),
]) {
assert.throws(
() => normalizeLocalServiceManagerIntent(candidate),
LocalDeploymentConfigurationError,
);
}
});
test('normalizes manager-specific root bridge commands without shell surface', () => {
const command = {
schemaVersion: 1,
operation: 'local.deployment.service-manager.execute',
options: {
controllerRoot: '/var/lib/qinglong3-service-bridge',
allowRootController: true,
manager: { kind: 'systemd', executable: '/usr/bin/systemctl' },
},
request: {
intentPath: '/opt/qinglong3/service/service-manager-intent.json',
expectedIntentDigest: 'a'.repeat(64),
},
};
assert.deepEqual(normalizeLocalServiceBridgeCommand(command), command);
assert.deepEqual(
normalizeLocalServiceBridgeCommand({
...command,
options: {
...command.options,
manager: {
kind: 'openrc',
serviceExecutable: '/sbin/rc-service',
updateExecutable: '/sbin/rc-update',
},
},
}).options.manager,
{
kind: 'openrc',
serviceExecutable: '/sbin/rc-service',
updateExecutable: '/sbin/rc-update',
},
);
assert.throws(
() =>
normalizeLocalServiceBridgeCommand({
...command,
options: {
...command.options,
manager: {
kind: 'systemd',
executable: '/usr/bin/systemctl',
arguments: ['start', 'anything.service'],
},
},
}),
LocalDeploymentConfigurationError,
);
});
@@ -0,0 +1,454 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
runLocalServiceBridge,
} = require('../dist/deployment/service-manager/serviceBridge.js');
const {
prepareLocalServiceManagerIntent,
consumeLocalServiceManagerOutcome,
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
const roots = [];
const destinations = [
'/etc/systemd/system/qinglong3.service',
'/etc/init.d/qinglong3',
];
afterEach(() => {
for (const root of roots.splice(0)) {
fs.rmSync(root, { force: true, recursive: true });
}
if (process.getuid?.() === 0) {
for (const destination of destinations) {
fs.rmSync(destination, { force: true });
fs.rmSync(
path.join(
path.dirname(destination),
`.${path.basename(destination)}.ql3-service-bridge-stage`,
),
{ force: true },
);
}
}
});
function fixture(kind) {
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), `ql3-service-bridge-${kind}-`)),
);
roots.push(root);
fs.chmodSync(root, 0o700);
const service = path.join(root, 'service');
fs.mkdirSync(service, { mode: 0o700 });
fs.writeFileSync(
path.join(root, 'local-application.json'),
`${JSON.stringify({
schema: 'qinglong/local-application-process@v2',
instanceId: `${kind}-edge-1`,
profile: 'edge',
storage: { mode: 'fresh' },
})}\n`,
{ mode: 0o600 },
);
fs.writeFileSync(
path.join(
service,
kind === 'systemd' ? 'qinglong3.service' : 'qinglong3.openrc',
),
kind === 'systemd'
? '[Service]\nExecStart=/usr/bin/node /opt/qinglong3/app.js\n'
: '#!/sbin/openrc-run\ncommand=/usr/bin/node\n',
{ mode: kind === 'systemd' ? 0o600 : 0o700 },
);
const controllerRoot = path.join(root, 'root-controller');
return { root, controllerRoot };
}
function prepare(root, kind, actionId, action) {
return prepareLocalServiceManagerIntent({
schemaVersion: 1,
operation: 'local.deployment.service-manager.intent.prepare',
options: { deploymentRoot: root, allowRootService: true },
request: {
actionId,
action,
serviceKind: kind,
lineage: { mode: 'fresh' },
requestedAtMs: 1786416100000,
},
});
}
function bridgeCommand(prepared, controllerRoot, kind) {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.execute',
options: {
controllerRoot,
allowRootController: true,
manager:
kind === 'systemd'
? { kind, executable: '/usr/bin/true' }
: {
kind,
serviceExecutable: '/usr/bin/true',
updateExecutable: '/usr/bin/true',
},
},
request: {
intentPath: prepared.intentPath,
expectedIntentDigest: prepared.intentDigest,
},
};
}
function result(status, stdout = '', responseLost = false) {
return { status, signal: null, stdout, stderr: '', responseLost };
}
test(
'root systemd bridge installs, starts, survives response loss and replays without mutation',
{ skip: process.getuid?.() !== 0 },
() => {
const { root, controllerRoot } = fixture('systemd');
const state = { active: false, enabled: false, pid: 0, loseRestart: false };
const calls = [];
const runner = ({ args }) => {
calls.push([...args]);
if (args[0] === 'show') {
return result(
0,
[
'LoadState=loaded',
`ActiveState=${state.active ? 'active' : 'inactive'}`,
`SubState=${state.active ? 'running' : 'dead'}`,
'FragmentPath=/etc/systemd/system/qinglong3.service',
`MainPID=${state.pid}`,
`UnitFileState=${state.enabled ? 'enabled' : 'disabled'}`,
'',
].join('\n'),
);
}
if (args[0] === 'enable') state.enabled = true;
if (args[0] === 'start') {
state.active = true;
state.pid = 4101;
}
if (args[0] === 'restart') {
state.active = true;
state.pid += 1;
if (state.loseRestart) return result(null, '', true);
}
if (args[0] === 'stop') {
state.active = false;
state.pid = 0;
}
return result(0);
};
let now = 1786416100100;
const dependencies = { runManager: runner, now: () => now++ };
const first = prepare(
root,
'systemd',
'123e4567-e89b-42d3-a456-426614174021',
'install-enable-start',
);
const firstResult = runLocalServiceBridge(
bridgeCommand(first, controllerRoot, 'systemd'),
dependencies,
);
assert.equal(firstResult.state, 'active');
assert.equal(fs.statSync('/etc/systemd/system/qinglong3.service').uid, 0);
assert.equal(
fs.statSync('/etc/systemd/system/qinglong3.service').mode & 0o777,
0o644,
);
const callCount = calls.length;
assert.equal(
runLocalServiceBridge(
bridgeCommand(first, controllerRoot, 'systemd'),
dependencies,
).status,
'existing',
);
assert.equal(calls.length, callCount);
state.loseRestart = true;
const restart = prepare(
root,
'systemd',
'123e4567-e89b-42d3-a456-426614174022',
'restart',
);
const restarted = runLocalServiceBridge(
bridgeCommand(restart, controllerRoot, 'systemd'),
dependencies,
);
assert.equal(restarted.state, 'active');
const outcome = JSON.parse(fs.readFileSync(restart.outcomePath, 'utf8'));
assert.equal(outcome.mutationDisposition, 'response-loss-inspected');
assert.equal(outcome.observation.mainPid, 4102);
assert.equal(
consumeLocalServiceManagerOutcome({
schemaVersion: 1,
operation: 'local.deployment.service-manager.outcome.consume',
options: { deploymentRoot: root, allowRootService: true },
request: {
actionId: restart.actionId,
expectedIntentDigest: restart.intentDigest,
},
}).state,
'active',
);
},
);
test(
'root bridge rejects Owner descriptor drift before publishing a barrier',
{ skip: process.getuid?.() !== 0 },
() => {
const { root, controllerRoot } = fixture('systemd');
const prepared = prepare(
root,
'systemd',
'123e4567-e89b-42d3-a456-426614174041',
'install-enable-start',
);
fs.writeFileSync(
path.join(root, 'service', 'qinglong3.service'),
'[Service]\nExecStart=/usr/bin/false\n',
{ mode: 0o600 },
);
assert.throws(
() =>
runLocalServiceBridge(
bridgeCommand(prepared, controllerRoot, 'systemd'),
{ runManager: () => result(0) },
),
/service manager source material drifted/,
);
assert.equal(fs.existsSync(controllerRoot), false);
},
);
test(
'barrier replay never repeats mutation after a crash and replaced installed unit',
{ skip: process.getuid?.() !== 0 },
() => {
const { root, controllerRoot } = fixture('systemd');
const prepared = prepare(
root,
'systemd',
'123e4567-e89b-42d3-a456-426614174042',
'install-enable-start',
);
let crash = true;
const calls = [];
const runner = ({ args }) => {
calls.push([...args]);
if (args[0] === 'show') {
return result(
0,
[
'LoadState=loaded',
'ActiveState=inactive',
'SubState=dead',
'FragmentPath=/etc/systemd/system/qinglong3.service',
'MainPID=0',
'UnitFileState=disabled',
'',
].join('\n'),
);
}
if (args[0] === 'enable' && crash) throw new Error('injected crash');
return result(0);
};
assert.throws(
() =>
runLocalServiceBridge(
bridgeCommand(prepared, controllerRoot, 'systemd'),
{ runManager: runner, now: () => 1786416300000 },
),
/injected crash/,
);
assert.equal(
fs.existsSync(
path.join(controllerRoot, prepared.actionId, 'barrier.json'),
),
true,
);
assert.equal(
fs.existsSync(
path.join(controllerRoot, prepared.actionId, 'outcome.json'),
),
false,
);
fs.writeFileSync(
'/etc/systemd/system/qinglong3.service',
'[Service]\nExecStart=/usr/bin/false\n',
{ mode: 0o644 },
);
crash = false;
const mutationsBeforeReplay = calls.filter(
(args) => args[0] !== 'show',
).length;
const replay = runLocalServiceBridge(
bridgeCommand(prepared, controllerRoot, 'systemd'),
{ runManager: runner, now: () => 1786416300100 },
);
assert.equal(replay.state, 'manual_required');
const outcome = JSON.parse(fs.readFileSync(prepared.outcomePath, 'utf8'));
assert.equal(outcome.manualReason, 'descriptor_install_unproved');
assert.equal(outcome.mutationDisposition, 'replay-inspected');
assert.equal(
calls.filter((args) => args[0] !== 'show').length,
mutationsBeforeReplay,
);
},
);
test(
'root outcome survives Owner outcome preoccupation without overwriting it',
{ skip: process.getuid?.() !== 0 },
() => {
const { root, controllerRoot } = fixture('systemd');
const prepared = prepare(
root,
'systemd',
'123e4567-e89b-42d3-a456-426614174043',
'install-enable-start',
);
fs.writeFileSync(prepared.outcomePath, '{}\n', { mode: 0o600 });
const state = { active: false, enabled: false, pid: 0 };
let mutations = 0;
const runner = ({ args }) => {
if (args[0] === 'show') {
return result(
0,
[
'LoadState=loaded',
`ActiveState=${state.active ? 'active' : 'inactive'}`,
`SubState=${state.active ? 'running' : 'dead'}`,
'FragmentPath=/etc/systemd/system/qinglong3.service',
`MainPID=${state.pid}`,
`UnitFileState=${state.enabled ? 'enabled' : 'disabled'}`,
'',
].join('\n'),
);
}
mutations += 1;
if (args[0] === 'enable') state.enabled = true;
if (args[0] === 'start') {
state.active = true;
state.pid = 6101;
}
return result(0);
};
assert.throws(
() =>
runLocalServiceBridge(
bridgeCommand(prepared, controllerRoot, 'systemd'),
{ runManager: runner, now: () => 1786416400000 },
),
/service bridge Owner outcome drifted/,
);
assert.equal(fs.readFileSync(prepared.outcomePath, 'utf8'), '{}\n');
assert.equal(
fs.existsSync(
path.join(controllerRoot, prepared.actionId, 'outcome.json'),
),
true,
);
const mutationsAfterFirst = mutations;
assert.throws(
() =>
runLocalServiceBridge(
bridgeCommand(prepared, controllerRoot, 'systemd'),
{ runManager: runner, now: () => 1786416400100 },
),
/service bridge Owner outcome drifted/,
);
assert.equal(mutations, mutationsAfterFirst);
assert.equal(fs.readFileSync(prepared.outcomePath, 'utf8'), '{}\n');
},
);
test(
'root OpenRC bridge uses fixed update/service argv for install and stop',
{ skip: process.getuid?.() !== 0 },
() => {
fs.mkdirSync('/etc/init.d', { mode: 0o755, recursive: true });
const { root, controllerRoot } = fixture('openrc');
const state = { active: false, enabled: false, loseStart: true };
const calls = [];
const runner = ({ args }) => {
calls.push([...args]);
if (args[0] === 'show') {
return result(0, state.enabled ? ' qinglong3 | default\n' : '');
}
if (args[1] === 'status') {
return state.active
? result(0, 'status: started\n')
: result(3, 'status: stopped\n');
}
if (args[0] === 'add') state.enabled = true;
if (args[1] === 'start') {
state.active = true;
if (state.loseStart) return result(null, '', true);
}
if (args[1] === 'stop') state.active = false;
return result(0);
};
let now = 1786416200000;
const dependencies = { runManager: runner, now: () => now++ };
const install = prepare(
root,
'openrc',
'123e4567-e89b-42d3-a456-426614174031',
'install-enable-start',
);
const installed = runLocalServiceBridge(
bridgeCommand(install, controllerRoot, 'openrc'),
dependencies,
);
assert.equal(installed.state, 'active');
const installedOutcome = JSON.parse(
fs.readFileSync(install.outcomePath, 'utf8'),
);
assert.equal(
installedOutcome.mutationDisposition,
'response-loss-inspected',
);
const callCount = calls.length;
assert.equal(
runLocalServiceBridge(
bridgeCommand(install, controllerRoot, 'openrc'),
dependencies,
).status,
'existing',
);
assert.equal(calls.length, callCount);
state.loseStart = false;
const stop = prepare(
root,
'openrc',
'123e4567-e89b-42d3-a456-426614174032',
'stop',
);
assert.equal(
runLocalServiceBridge(
bridgeCommand(stop, controllerRoot, 'openrc'),
dependencies,
).state,
'stopped',
);
assert.ok(calls.some((args) => args.join(' ') === 'add qinglong3 default'));
assert.ok(calls.some((args) => args.join(' ') === 'qinglong3 start'));
assert.ok(calls.some((args) => args.join(' ') === 'qinglong3 stop'));
},
);
@@ -0,0 +1,572 @@
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
consumeLocalServiceManagerCutoverOutcome,
} = require('../dist/deployment/service-manager/serviceCutoverConsumer.js');
const {
prepareLocalServiceManagerIntent,
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
const {
localServiceManagerObservationDigest,
localServiceManagerOutcomeDigest,
} = require('../dist/deployment/service-manager/serviceOutcomeContract.js');
const {
advanceLocalCutoverInstanceHead,
claimLocalCutoverInstance,
readLocalCutoverInstanceHead,
} = require('../dist/deployment/cutover/instanceLineage.js');
const {
cutoverDigest,
} = require('../dist/deployment/cutover/targetEvidence.js');
function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}
function writePrivate(filePath, value) {
fs.writeFileSync(
filePath,
typeof value === 'string' ? value : `${JSON.stringify(value)}\n`,
{ mode: 0o600 },
);
}
function fixture(t) {
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-service-cutover-')),
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.chmodSync(root, 0o700);
const service = path.join(root, 'service');
const cutoverId = 'cutover-edge-router-1';
const journal = path.join(service, 'cutovers', cutoverId);
fs.mkdirSync(journal, { recursive: true, mode: 0o700 });
fs.chmodSync(service, 0o700);
fs.chmodSync(path.join(service, 'cutovers'), 0o700);
fs.chmodSync(journal, 0o700);
const sourcePath = path.join(root, 'legacy.sqlite');
const targetPath = path.join(root, 'target.sqlite');
const recoveryPath = path.join(root, 'recovery.sqlite');
const manifestPath = path.join(root, 'manifest.json');
const activationPath = path.join(root, 'activation.json');
for (const [filePath, contents] of [
[sourcePath, 'legacy\n'],
[targetPath, 'target\n'],
[recoveryPath, 'legacy\n'],
]) {
writePrivate(filePath, contents);
}
const manifestPayload = {
schemaVersion: 1,
kind: 'qinglong3-local-adoption-manifest-fixture',
};
const manifestDigest = cutoverDigest(manifestPayload);
writePrivate(manifestPath, { ...manifestPayload, manifestDigest });
const target = fs.statSync(targetPath, { bigint: true });
const activationPayload = {
schemaVersion: 1,
kind: 'qinglong3-local-sqlite-activation',
state: 'prepared',
profile: 'edge',
createdAtMs: 1786416000000,
adoptionManifestDigest: manifestDigest,
planDigest: '2'.repeat(64),
sourcePathDigest: sha256(sourcePath),
recoverySha256: sha256(fs.readFileSync(recoveryPath)),
targetSha256: sha256(fs.readFileSync(targetPath)),
targetPathDigest: sha256(targetPath),
targetDevice: target.dev.toString(),
targetInode: target.ino.toString(),
};
const activationDigest = cutoverDigest(activationPayload);
writePrivate(activationPath, { ...activationPayload, activationDigest });
const commitmentPayload = {
schemaVersion: 1,
kind: 'qinglong3-local-legacy-silence-commitment',
state: 'legacy_stopped',
cutoverId,
profile: 'edge',
instanceId: 'edge-router-1',
activationDigest,
requestedAtMs: 1786416000010,
observedAtMs: 1786416000020,
previousRecordDigest: '3'.repeat(64),
controller: {
kind: 'docker',
endpointDigest: '4'.repeat(64),
legacyContainerId: '5'.repeat(64),
legacyContainerIdentityDigest: '6'.repeat(64),
legacySourceBindingDigest: '7'.repeat(64),
},
};
const commitmentDigest = cutoverDigest(commitmentPayload);
const commitmentPath = path.join(journal, '0002-legacy-stopped.json');
writePrivate(commitmentPath, {
...commitmentPayload,
commitmentDigest,
});
const applicationPath = path.join(root, 'local-application.json');
writePrivate(applicationPath, {
schema: 'qinglong/local-application-process@v3',
instanceId: 'edge-router-1',
profile: 'edge',
storage: {
mode: 'adopted',
sourcePath,
targetPath,
recoveryPath,
manifestPath,
activationPath,
expectedActivationDigest: activationDigest,
},
runtime: {},
pluginPackages: {},
ai: { deployment: 'excluded' },
cutover: {
cutoverId,
commitmentPath,
expectedCommitmentDigest: commitmentDigest,
},
});
writePrivate(
path.join(service, 'qinglong3.service'),
'[Service]\nExecStart=/usr/bin/node /opt/qinglong3/app.js\n',
);
const identity = {
options: { deploymentRoot: root },
request: {
cutoverId,
profile: 'edge',
instanceId: 'edge-router-1',
expectedActivationDigest: activationDigest,
requestedAtMs: 1786416000030,
},
};
claimLocalCutoverInstance(identity, process.getuid(), '8'.repeat(64));
advanceLocalCutoverInstanceHead(
identity,
process.getuid(),
'legacy_stopped',
0,
commitmentDigest,
);
const procRoot = path.join(root, 'proc');
fs.mkdirSync(procRoot, { mode: 0o700 });
return {
root,
service,
cutoverId,
activationDigest,
commitmentDigest,
applicationPath,
sourcePath,
identity,
procRoot,
};
}
function prepare(state, generation, action, previousRecordDigest, actionId) {
return prepareLocalServiceManagerIntent({
schemaVersion: 1,
operation: 'local.deployment.service-manager.intent.prepare',
options: {
deploymentRoot: state.root,
allowRootService: process.getuid() === 0,
},
request: {
actionId,
action,
serviceKind: 'systemd',
lineage: {
mode: 'adopted',
cutoverId: state.cutoverId,
generation,
expectedActivationDigest: state.activationDigest,
previousRecordDigest,
},
requestedAtMs: 1786416000100 + generation,
},
});
}
function publishOutcome(prepared, action, state, mainPid, completedAtMs) {
const intent = JSON.parse(fs.readFileSync(prepared.intentPath, 'utf8'));
const observationPayload = {
managerKind: 'systemd',
serviceName: 'qinglong3',
fragmentPath: '/etc/systemd/system/qinglong3.service',
loadState: 'loaded',
activeState: state === 'active' ? 'active' : 'inactive',
subState: state === 'active' ? 'running' : 'dead',
enabledState: 'enabled',
mainPid,
observedAtMs: completedAtMs - 1,
};
const observation = {
...observationPayload,
observationDigest: localServiceManagerObservationDigest(observationPayload),
};
const payload = {
schemaVersion: 1,
kind: 'qinglong3-local-service-manager-outcome',
actionId: prepared.actionId,
action,
intentDigest: prepared.intentDigest,
descriptorDigest: intent.descriptor.sha256,
state,
mutationDisposition: 'executed',
manualReason: null,
observation,
completedAtMs,
};
writePrivate(prepared.outcomePath, {
...payload,
outcomeDigest: localServiceManagerOutcomeDigest(payload),
});
}
function publishReceipt(state, processId, startTicks) {
const executable = fs.realpathSync(process.execPath);
const payload = {
schemaVersion: 1,
schema: 'qinglong/local-application-startup-receipt@v1',
instanceId: 'edge-router-1',
profile: 'edge',
aiStatus: 'deployment_excluded',
bootId: '00000000-0000-4000-8000-000000000001',
activeBootAgeMs: 1000,
processId,
processStartTicks: startTicks,
nodeExecutable: executable,
nodeVersion: 'v24.18.0',
};
const digest = crypto
.createHash('sha256')
.update('qinglong.local-application-startup-receipt.v1\0', 'utf8')
.update(JSON.stringify(payload), 'utf8')
.digest('hex');
writePrivate(`${state.applicationPath}.active.json`, {
...payload,
sha256: digest,
});
const processRoot = path.join(state.procRoot, String(processId));
fs.mkdirSync(processRoot, { mode: 0o700 });
const fields = ['S', ...Array(18).fill('0'), startTicks, '0'];
fs.writeFileSync(
path.join(processRoot, 'stat'),
`${processId} (node) ${fields.join(' ')}\n`,
);
fs.symlinkSync(executable, path.join(processRoot, 'exe'));
return digest;
}
function publishShutdownReceipt(
state,
processId,
startTicks,
startupReceiptDigest,
) {
const payload = {
schemaVersion: 1,
schema: 'qinglong/local-application-shutdown-receipt@v1',
instanceId: 'edge-router-1',
profile: 'edge',
signal: 'SIGTERM',
stopResult: 'stopped',
startupReceiptDigest,
bootId: '00000000-0000-4000-8000-000000000001',
stoppedBootAgeMs: 2000,
processId,
processStartTicks: startTicks,
nodeExecutable: fs.realpathSync(process.execPath),
nodeVersion: 'v24.18.0',
};
const digest = crypto
.createHash('sha256')
.update('qinglong.local-application-shutdown-receipt.v1\0', 'utf8')
.update(JSON.stringify(payload), 'utf8')
.digest('hex');
writePrivate(`${state.applicationPath}.stopped.json`, {
...payload,
sha256: digest,
});
return digest;
}
function consumeCommand(state, prepared) {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.cutover.consume',
options: {
deploymentRoot: state.root,
allowRootService: process.getuid() === 0,
startupTimeoutMs: 100,
startupPollMs: 10,
},
request: {
actionId: prepared.actionId,
expectedIntentDigest: prepared.intentDigest,
},
};
}
test('commits adopted service active evidence and replays from the instance head', async (t) => {
const state = fixture(t);
const prepared = prepare(
state,
1,
'install-enable-start',
state.commitmentDigest,
'123e4567-e89b-42d3-a456-426614174021',
);
publishOutcome(
prepared,
'install-enable-start',
'active',
4123,
1786416000200,
);
const receiptDigest = publishReceipt(state, 4123, '100001');
const command = consumeCommand(state, prepared);
const result = await consumeLocalServiceManagerCutoverOutcome(command, {
procRoot: state.procRoot,
});
assert.equal(result.status, 'prepared');
assert.equal(result.state, 'target_active');
const head = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(head.state, 'target_active');
assert.equal(head.sourceRecordDigest, result.recordDigest);
const recordPath = path.join(
state.root,
'service',
'cutovers',
state.cutoverId,
'service-manager-g01-active.json',
);
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
assert.equal(record.evidence.startupReceiptDigest, receiptDigest);
assert.match(record.evidence.processIdentityDigest, /^[0-9a-f]{64}$/);
assert.equal(
(
await consumeLocalServiceManagerCutoverOutcome(command, {
procRoot: state.procRoot,
})
).status,
'existing',
);
});
test('restart cannot reuse the previous generation startup receipt', async (t) => {
const state = fixture(t);
const first = prepare(
state,
1,
'install-enable-start',
state.commitmentDigest,
'123e4567-e89b-42d3-a456-426614174022',
);
publishOutcome(first, 'install-enable-start', 'active', 4223, 1786416000200);
publishReceipt(state, 4223, '100002');
const firstResult = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, first),
{ procRoot: state.procRoot },
);
const restart = prepare(
state,
2,
'restart',
firstResult.recordDigest,
'123e4567-e89b-42d3-a456-426614174023',
);
publishOutcome(restart, 'restart', 'active', 5223, 1786416000300);
let clock = 0;
const result = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, restart),
{
procRoot: state.procRoot,
now: () => clock,
wait: async (milliseconds) => {
clock += milliseconds;
},
},
);
assert.equal(result.state, 'manual_required');
const head = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(head.state, 'manual_required');
});
test('stop advances only after the exact receipted process identity disappears', async (t) => {
const state = fixture(t);
const first = prepare(
state,
1,
'install-enable-start',
state.commitmentDigest,
'123e4567-e89b-42d3-a456-426614174024',
);
publishOutcome(first, 'install-enable-start', 'active', 4323, 1786416000200);
const startupReceiptDigest = publishReceipt(state, 4323, '100003');
const firstResult = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, first),
{ procRoot: state.procRoot },
);
const stopped = prepare(
state,
1,
'stop',
firstResult.recordDigest,
'123e4567-e89b-42d3-a456-426614174025',
);
publishOutcome(stopped, 'stop', 'stopped', 0, 1786416000300);
const shutdownReceiptDigest = publishShutdownReceipt(
state,
4323,
'100003',
startupReceiptDigest,
);
fs.rmSync(path.join(state.procRoot, '4323'), {
recursive: true,
force: true,
});
const result = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, stopped),
{ procRoot: state.procRoot },
);
assert.equal(result.state, 'target_stopped');
const head = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(head.state, 'target_stopped');
assert.equal(head.sourceRecordDigest, result.recordDigest);
const record = JSON.parse(
fs.readFileSync(
path.join(
state.root,
'service',
'cutovers',
state.cutoverId,
'service-manager-g01-stopped.json',
),
'utf8',
),
);
assert.equal(record.evidence.shutdownReceiptDigest, shutdownReceiptDigest);
});
test('stop without an exact shutdown receipt requires manual resolution', async (t) => {
const state = fixture(t);
const first = prepare(
state,
1,
'install-enable-start',
state.commitmentDigest,
'123e4567-e89b-42d3-a456-426614174028',
);
publishOutcome(first, 'install-enable-start', 'active', 4623, 1786416000200);
publishReceipt(state, 4623, '100006');
const firstResult = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, first),
{ procRoot: state.procRoot },
);
const stopped = prepare(
state,
1,
'stop',
firstResult.recordDigest,
'123e4567-e89b-42d3-a456-426614174029',
);
publishOutcome(stopped, 'stop', 'stopped', 0, 1786416000300);
fs.rmSync(path.join(state.procRoot, '4623'), {
recursive: true,
force: true,
});
const result = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, stopped),
{ procRoot: state.procRoot },
);
assert.equal(result.state, 'manual_required');
});
test('rejects legacy source content drift before committing service active', async (t) => {
const state = fixture(t);
const prepared = prepare(
state,
1,
'install-enable-start',
state.commitmentDigest,
'123e4567-e89b-42d3-a456-426614174026',
);
publishOutcome(
prepared,
'install-enable-start',
'active',
4423,
1786416000200,
);
publishReceipt(state, 4423, '100004');
fs.writeFileSync(state.sourcePath, 'legacy-drifted\n', { mode: 0o600 });
await assert.rejects(
consumeLocalServiceManagerCutoverOutcome(consumeCommand(state, prepared), {
procRoot: state.procRoot,
}),
/adopted data evidence drifted/,
);
const head = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(head.state, 'legacy_stopped');
});
test('terminalizes a manager PID replaced before Owner receipt verification', async (t) => {
const state = fixture(t);
const prepared = prepare(
state,
1,
'install-enable-start',
state.commitmentDigest,
'123e4567-e89b-42d3-a456-426614174027',
);
publishOutcome(
prepared,
'install-enable-start',
'active',
4523,
1786416000200,
);
publishReceipt(state, 4524, '100005');
let clock = 0;
const result = await consumeLocalServiceManagerCutoverOutcome(
consumeCommand(state, prepared),
{
procRoot: state.procRoot,
now: () => clock,
wait: async (milliseconds) => {
clock += milliseconds;
},
},
);
assert.equal(result.state, 'manual_required');
const head = readLocalCutoverInstanceHead(
state.root,
'edge-router-1',
process.getuid(),
);
assert.equal(head.state, 'manual_required');
});
@@ -0,0 +1,274 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
consumeLocalServiceManagerOutcome,
prepareLocalServiceManagerIntent,
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
const {
localServiceManagerObservationDigest,
localServiceManagerOutcomeDigest,
} = require('../dist/deployment/service-manager/serviceOutcomeContract.js');
const {
LocalDeploymentConfigurationError,
} = require('../dist/deployment/foundation/contract.js');
const {
advanceLocalCutoverInstanceHead,
claimLocalCutoverInstance,
} = require('../dist/deployment/cutover/instanceLineage.js');
const roots = [];
afterEach(() => {
for (const root of roots.splice(0)) {
fs.rmSync(root, { force: true, recursive: true });
}
});
function fixture() {
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-service-intent-')),
);
roots.push(root);
fs.chmodSync(root, 0o700);
const service = path.join(root, 'service');
fs.mkdirSync(service, { mode: 0o700 });
const application = `${JSON.stringify(
{
schema: 'qinglong/local-application-process@v2',
instanceId: 'edge-router-1',
profile: 'edge',
storage: { mode: 'fresh' },
},
null,
2,
)}\n`;
fs.writeFileSync(path.join(root, 'local-application.json'), application, {
mode: 0o600,
});
fs.writeFileSync(
path.join(service, 'qinglong3.service'),
'[Service]\nExecStart=/usr/bin/node /opt/qinglong3/app.js\n',
{ mode: 0o600 },
);
return { root, service };
}
function prepareCommand(root) {
return {
schemaVersion: 1,
operation: 'local.deployment.service-manager.intent.prepare',
options: {
deploymentRoot: root,
allowRootService: process.getuid() === 0,
},
request: {
actionId: '123e4567-e89b-42d3-a456-426614174011',
action: 'install-enable-start',
serviceKind: 'systemd',
lineage: { mode: 'fresh' },
requestedAtMs: 1786416000100,
},
};
}
function adoptedHead(root) {
const previousRecordDigest = 'b'.repeat(64);
const identity = {
options: { deploymentRoot: root },
request: {
cutoverId: 'cutover-edge-router-1',
profile: 'edge',
instanceId: 'edge-router-1',
expectedActivationDigest: 'a'.repeat(64),
requestedAtMs: 1786416000000,
},
};
claimLocalCutoverInstance(identity, process.getuid(), 'c'.repeat(64));
advanceLocalCutoverInstanceHead(
identity,
process.getuid(),
'legacy_stopped',
0,
previousRecordDigest,
);
return { identity, previousRecordDigest };
}
function adoptedApplication(root, identity, previousRecordDigest) {
const material = {
schema: 'qinglong/local-application-process@v3',
instanceId: identity.request.instanceId,
profile: identity.request.profile,
storage: {
mode: 'adopted',
sourcePath: path.join(root, 'legacy.sqlite'),
targetPath: path.join(root, 'target.sqlite'),
recoveryPath: path.join(root, 'recovery.sqlite'),
manifestPath: path.join(root, 'manifest.json'),
activationPath: path.join(root, 'activation.json'),
expectedActivationDigest: identity.request.expectedActivationDigest,
},
runtime: {},
pluginPackages: {},
ai: { deployment: 'excluded' },
cutover: {
cutoverId: identity.request.cutoverId,
commitmentPath: path.join(root, 'legacy-stopped.json'),
expectedCommitmentDigest: previousRecordDigest,
},
};
fs.writeFileSync(
path.join(root, 'local-application.json'),
`${JSON.stringify(material)}\n`,
{ mode: 0o600 },
);
}
test('publishes an exact Owner intent and verifies a bound bridge outcome', () => {
const { root } = fixture();
const command = prepareCommand(root);
const prepared = prepareLocalServiceManagerIntent(command);
assert.equal(prepared.status, 'prepared');
assert.equal(fs.statSync(prepared.intentPath).mode & 0o777, 0o600);
assert.equal(prepareLocalServiceManagerIntent(command).status, 'existing');
const intent = JSON.parse(fs.readFileSync(prepared.intentPath, 'utf8'));
assert.equal(intent.profile, 'edge');
assert.equal(intent.instanceId, 'edge-router-1');
assert.equal(intent.service.uid, process.getuid());
assert.equal(intent.service.gid, process.getgid());
assert.match(intent.deployment.applicationConfigSha256, /^[0-9a-f]{64}$/);
assert.match(intent.descriptor.sha256, /^[0-9a-f]{64}$/);
const observationPayload = {
managerKind: 'systemd',
serviceName: 'qinglong3',
fragmentPath: '/etc/systemd/system/qinglong3.service',
loadState: 'loaded',
activeState: 'active',
subState: 'running',
enabledState: 'enabled',
mainPid: 4123,
observedAtMs: 1786416000200,
};
const observation = {
...observationPayload,
observationDigest: localServiceManagerObservationDigest(observationPayload),
};
const outcomePayload = {
schemaVersion: 1,
kind: 'qinglong3-local-service-manager-outcome',
actionId: intent.actionId,
action: intent.action,
intentDigest: intent.intentDigest,
descriptorDigest: intent.descriptor.sha256,
state: 'active',
mutationDisposition: 'executed',
manualReason: null,
observation,
completedAtMs: 1786416000300,
};
const outcome = {
...outcomePayload,
outcomeDigest: localServiceManagerOutcomeDigest(outcomePayload),
};
fs.writeFileSync(prepared.outcomePath, `${JSON.stringify(outcome)}\n`, {
mode: 0o600,
});
assert.deepEqual(
consumeLocalServiceManagerOutcome({
schemaVersion: 1,
operation: 'local.deployment.service-manager.outcome.consume',
options: command.options,
request: {
actionId: prepared.actionId,
expectedIntentDigest: prepared.intentDigest,
},
}),
{
schemaVersion: 1,
operation: 'local.deployment.service-manager.outcome.consume',
status: 'verified',
actionId: prepared.actionId,
state: 'active',
outcomeDigest: outcome.outcomeDigest,
observationDigest: observation.observationDigest,
},
);
});
test('fails closed when current descriptor material drifts after intent publication', () => {
const { root, service } = fixture();
const command = prepareCommand(root);
const prepared = prepareLocalServiceManagerIntent(command);
fs.writeFileSync(
path.join(service, 'qinglong3.service'),
'[Service]\nExecStart=/usr/bin/false\n',
{ mode: 0o600 },
);
assert.throws(
() => prepareLocalServiceManagerIntent(command),
LocalDeploymentConfigurationError,
);
assert.equal(fs.existsSync(prepared.intentPath), true);
});
test('binds an adopted first start to the current legacy-stopped instance head', () => {
const { root } = fixture();
const { identity, previousRecordDigest } = adoptedHead(root);
adoptedApplication(root, identity, previousRecordDigest);
const command = prepareCommand(root);
command.request.lineage = {
mode: 'adopted',
cutoverId: identity.request.cutoverId,
generation: 1,
expectedActivationDigest: identity.request.expectedActivationDigest,
previousRecordDigest,
};
assert.equal(prepareLocalServiceManagerIntent(command).status, 'prepared');
advanceLocalCutoverInstanceHead(
identity,
process.getuid(),
'target_active',
1,
'e'.repeat(64),
);
const stale = structuredClone(command);
stale.request.actionId = '123e4567-e89b-42d3-a456-426614174012';
assert.throws(
() => prepareLocalServiceManagerIntent(stale),
/lost the instance lineage compare-and-swap/,
);
});
test('does not allow fresh service intent to bypass an existing cutover head', () => {
const { root } = fixture();
adoptedHead(root);
assert.throws(
() => prepareLocalServiceManagerIntent(prepareCommand(root)),
/fresh service intent cannot bypass an instance lineage head/,
);
});
test('rejects adopted lineage when the application remains a fresh v2 deployment', () => {
const { root } = fixture();
const { identity, previousRecordDigest } = adoptedHead(root);
const command = prepareCommand(root);
command.request.lineage = {
mode: 'adopted',
cutoverId: identity.request.cutoverId,
generation: 1,
expectedActivationDigest: identity.request.expectedActivationDigest,
previousRecordDigest,
};
assert.throws(
() => prepareLocalServiceManagerIntent(command),
/service intent does not match adopted application binding/,
);
});
@@ -0,0 +1,534 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createLocalTaskDefinitionCommandRunner,
runLocalTaskDefinitionCommandFile,
} = require('@qinglong/local-owner-cli/task-definition-command');
const {
provisionLocalOwnerPepperKey,
} = require('@qinglong/local-owner-console');
const {
createLocalTaskDefinitionAdministrationService,
} = require('@qinglong/local-admin/task-definition-administration');
const {
openLocalSqliteTaskDefinitionAdministrationDatabase,
} = require('@qinglong/local-sqlite/task-definition-administration');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
apiCredentialSecretDigest,
formatApiCredentialToken,
} = require('@qinglong/runtime-core/api-credential-token');
const CREDENTIAL_ID = 'task-owner';
const PEPPER_KEY_ID = 'task-owner-v1';
const PEPPER = Buffer.alloc(32, 121).toString('base64url');
const CREDENTIAL_SECRET = Buffer.alloc(32, 122).toString('base64url');
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, CREDENTIAL_SECRET);
async function fixture(t, { role = 'owner' } = {}) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-task-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandsDirectory = path.join(deploymentRoot, 'commands');
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const pepperSummary = provisionLocalOwnerPepperKey({
keyringDirectory: ownerPepperKeyringDirectory,
pepperKeyId: PEPPER_KEY_ID,
randomBytes: () => Buffer.alloc(32, 121),
});
const now = Date.now();
const secretDigest = apiCredentialSecretDigest(
PEPPER,
CREDENTIAL_ID,
CREDENTIAL_SECRET,
);
const notBeforeAtMs = now - 1_000;
const expiresAtMs = now + 10 * 60 * 1_000;
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(
PEPPER_KEY_ID,
pepperSummary.digest,
'f'.repeat(64),
'81000000-0000-4000-8000-000000000001',
'81000000-0000-4000-8000-000000000002',
now - 2_000,
now - 1_500,
);
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(
'81000000-0000-4000-8000-000000000002',
PEPPER_KEY_ID,
pepperSummary.digest,
'f'.repeat(64),
now - 1_500,
);
database
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'task-user', 'active', 1, ?, ?)`,
)
.run(now - 1_000, now - 1_000);
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', 'task-user', ?, ?, ?, ?)`,
)
.run(
CREDENTIAL_ID,
secretDigest,
now - 1_000,
notBeforeAtMs,
expiresAtMs,
);
database
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, 1, ?)`,
)
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
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', 'task-user', 1, 'active', ?,
'task-owner-binding', 'user', 'task-user', ?
)`,
)
.run(role, now - 500);
} finally {
database.close();
}
fs.chmodSync(databasePath, 0o600);
fs.writeFileSync(
credentialFilePath,
`${JSON.stringify({
schemaVersion: 1,
kind: 'qinglong3-local-identity-credential-presentation',
token: TOKEN,
})}\n`,
{ mode: 0o600 },
);
return {
deploymentRoot,
commandsDirectory,
databasePath,
now,
options: {
deploymentRoot,
databasePath,
profile: 'edge',
ownerPepperKeyringDirectory,
credentialFilePath,
},
};
}
function writeCommand(value, operation, request, name) {
const filePath = path.join(value.commandsDirectory, `${name}.json`);
fs.writeFileSync(
filePath,
`${JSON.stringify({
schemaVersion: 1,
operation,
options: value.options,
request,
})}\n`,
{ mode: 0o600 },
);
return filePath;
}
function putRequest(value, suffix, overrides = {}) {
return {
projectId: 'default',
taskId: 'task-product-entry',
expectedRevision: null,
mutationId: `82000000-0000-4000-8000-00000000000${suffix}`,
requestId: `task-put-${suffix}`,
failureAuditEventId: `83000000-0000-4000-8000-00000000000${suffix}`,
name: 'Product task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/echo',
args: ['private-argument-not-output'],
},
},
},
labels: { owner: 'product' },
enabled: true,
occurredAtMs: value.now,
...overrides,
};
}
function auditRows(databasePath) {
const database = new DatabaseSync(databasePath, { readOnly: true });
try {
return database
.prepare(
`SELECT event_id AS "eventId", operation_id AS "operationId",
outcome, reasons_json AS "reasonsJson"
FROM "QingLong3SecurityAuditEvents" ORDER BY occurred_at_ms, event_id`,
)
.all();
} finally {
database.close();
}
}
test('creates, exactly replays, disables, inspects and lists a TaskDefinition', async (t) => {
const value = await fixture(t);
const createPath = writeCommand(
value,
'task.put',
putRequest(value, '1'),
'create',
);
const created = await runLocalTaskDefinitionCommandFile(createPath);
assert.equal(created.status, 'created');
assert.equal(created.task.revision, 1);
assert.equal(created.task.enabled, true);
assert.equal(created.task.schema, 'qinglong/command@v1');
assert.doesNotMatch(JSON.stringify(created), /private-argument-not-output/);
assert.equal(
(await runLocalTaskDefinitionCommandFile(createPath)).status,
'existing',
);
const updatePath = writeCommand(
value,
'task.put',
putRequest(value, '2', {
expectedRevision: 1,
mutationId: '82000000-0000-4000-8000-000000000002',
failureAuditEventId: '83000000-0000-4000-8000-000000000002',
requestId: 'task-put-2',
name: 'Product task disabled',
enabled: false,
occurredAtMs: value.now + 1,
}),
'disable',
);
const updated = await runLocalTaskDefinitionCommandFile(updatePath);
assert.equal(updated.status, 'updated');
assert.equal(updated.task.revision, 2);
assert.equal(updated.task.enabled, false);
const inspected = await runLocalTaskDefinitionCommandFile(
writeCommand(
value,
'task.inspect',
{
projectId: 'default',
taskId: 'task-product-entry',
requestId: 'task-inspect-1',
auditEventId: '84000000-0000-4000-8000-000000000001',
failureAuditEventId: '85000000-0000-4000-8000-000000000001',
},
'inspect',
),
);
assert.equal(inspected.found, true);
assert.equal(inspected.task.revision, 2);
assert.equal(Object.hasOwn(inspected.task, 'spec'), false);
const listed = await runLocalTaskDefinitionCommandFile(
writeCommand(
value,
'task.list',
{
projectId: 'default',
requestId: 'task-list-1',
auditEventId: '84000000-0000-4000-8000-000000000002',
failureAuditEventId: '85000000-0000-4000-8000-000000000002',
limit: 1,
},
'list',
),
);
assert.equal(listed.tasks.length, 1);
assert.equal(listed.tasks[0].taskId, 'task-product-entry');
assert.equal(listed.nextCursor, null);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitionRevisions"',
)
.get().count,
2,
);
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalTaskExecutionRevisions"',
)
.get().count,
1,
);
} finally {
database.close();
}
assert.deepEqual(
auditRows(value.databasePath).map((row) => [row.operationId, row.outcome]),
[
['task.create', 'allowed'],
['task.update', 'allowed'],
['task.read', 'allowed'],
['task.read', 'allowed'],
],
);
});
test('allows an operator to manage tasks but rejects a viewer atomically', async (t) => {
const operator = await fixture(t, { role: 'operator' });
assert.equal(
(
await runLocalTaskDefinitionCommandFile(
writeCommand(
operator,
'task.put',
putRequest(operator, '3'),
'operator-create',
),
)
).status,
'created',
);
const viewer = await fixture(t, { role: 'viewer' });
await assert.rejects(
runLocalTaskDefinitionCommandFile(
writeCommand(
viewer,
'task.put',
putRequest(viewer, '4'),
'viewer-create',
),
),
{ code: 'LOCAL_TASK_DEFINITION_ADMINISTRATION_FORBIDDEN' },
);
const database = new DatabaseSync(viewer.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare('SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitions"')
.get().count,
0,
);
} finally {
database.close();
}
assert.deepEqual(
auditRows(viewer.databasePath).map((row) => [row.operationId, row.outcome]),
[['task.create', 'denied']],
);
});
test('rechecks the credential and Policy fence inside the Task transaction', async (t) => {
const value = await fixture(t);
const runner = createLocalTaskDefinitionCommandRunner({
openDatabase: openLocalSqliteTaskDefinitionAdministrationDatabase,
authenticate: require('@qinglong/local-owner-console/authenticated-command')
.establishAuthenticatedLocalCommand,
now: Date.now,
createService(projectPolicy, mutations, source, audit, options) {
const fencedMutations = {
async appendAuthorizedTaskDefinitionRevision(mutation) {
const competing = new DatabaseSync(value.databasePath);
try {
competing
.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', 'task-user', 2, 'revoked', NULL,
'task-race-revoke', 'user', 'task-user', ?
)`,
)
.run(value.now + 5);
} finally {
competing.close();
}
return mutations.appendAuthorizedTaskDefinitionRevision(mutation);
},
};
return createLocalTaskDefinitionAdministrationService(
projectPolicy,
fencedMutations,
source,
audit,
options,
);
},
});
const request = putRequest(value, '5');
await assert.rejects(
runner.run(writeCommand(value, 'task.put', request, 'fenced-create')),
{ code: 'TASK_DEFINITION_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare('SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitions"')
.get().count,
0,
);
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents" WHERE event_id = ?',
)
.get(request.mutationId).count,
0,
);
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents" WHERE event_id = ?',
)
.get(request.failureAuditEventId).count,
1,
);
} finally {
database.close();
}
});
test('rejects mutation drift without adding a revision or changing its allowed audit', async (t) => {
const value = await fixture(t);
const original = putRequest(value, '6');
await runLocalTaskDefinitionCommandFile(
writeCommand(value, 'task.put', original, 'original'),
);
await assert.rejects(
runLocalTaskDefinitionCommandFile(
writeCommand(
value,
'task.put',
{
...original,
name: 'drifted replay',
failureAuditEventId: '83000000-0000-4000-8000-000000000007',
},
'drifted',
),
),
{ code: 'TASK_DEFINITION_CONFLICT' },
);
await assert.rejects(
runLocalTaskDefinitionCommandFile(
writeCommand(
value,
'task.put',
{
...original,
requestId: 'task-put-audit-drift',
failureAuditEventId: '83000000-0000-4000-8000-000000000009',
},
'audit-drifted',
),
),
{ code: 'TASK_DEFINITION_ADMINISTRATION_MUTATION_CONFLICT' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitionRevisions"',
)
.get().count,
1,
);
} finally {
database.close();
}
assert.deepEqual(
auditRows(value.databasePath).map((row) => [row.eventId, row.outcome]),
[
[original.mutationId, 'allowed'],
['83000000-0000-4000-8000-000000000007', 'denied'],
['83000000-0000-4000-8000-000000000009', 'denied'],
],
);
});
test('requires a private command file and exposes one exact CLI surface', async (t) => {
const value = await fixture(t);
const commandPath = writeCommand(
value,
'task.put',
putRequest(value, '8'),
'broad',
);
fs.chmodSync(commandPath, 0o644);
await assert.rejects(runLocalTaskDefinitionCommandFile(commandPath), {
code: 'LOCAL_TASK_DEFINITION_COMMAND_CONFIGURATION_INVALID',
});
const cliPath = path.resolve(
__dirname,
'../dist/automation-management/taskDefinitionCli.js',
);
const help = spawnSync(process.execPath, [cliPath, '--help'], {
encoding: 'utf8',
});
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3-task run --command-file /);
const invalid = spawnSync(process.execPath, [cliPath, 'list'], {
encoding: 'utf8',
});
assert.equal(invalid.status, 64);
assert.equal(
JSON.parse(invalid.stderr).code,
'LOCAL_TASK_DEFINITION_CLI_USAGE_INVALID',
);
});
@@ -0,0 +1,407 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createLocalTriggerCommandRunner,
runLocalTriggerCommandFile,
} = require('@qinglong/local-owner-cli/trigger-command');
const {
runLocalTaskDefinitionCommandFile,
} = require('@qinglong/local-owner-cli/task-definition-command');
const {
createLocalTriggerAdministrationService,
} = require('@qinglong/local-admin/trigger-administration');
const {
openLocalSqliteTriggerAdministrationDatabase,
} = require('@qinglong/local-sqlite/trigger-administration');
const {
auditRows,
localManagementFixture,
taskPutRequest,
writeCommand,
} = require('./localManagementFixture.cjs');
async function createTask(value, suffix = '1') {
const result = await runLocalTaskDefinitionCommandFile(
writeCommand(
value,
'task.put',
taskPutRequest(value, suffix),
`task-create-${suffix}`,
),
);
return result.task;
}
function triggerPutRequest(value, task, suffix, overrides = {}) {
return {
projectId: 'default',
triggerId: 'trigger-product-entry',
expectedRevision: null,
mutationId: `94000000-0000-4000-8000-00000000000${suffix}`,
requestId: `trigger-put-${suffix}`,
failureAuditEventId: `95000000-0000-4000-8000-00000000000${suffix}`,
taskId: task.taskId,
taskRevision: task.revision,
taskContentDigest: task.contentDigest,
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '*/5 * * * *',
timezone: 'Etc/UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
occurredAtMs: value.now + 1,
...overrides,
};
}
test('creates, exactly replays, disables, inspects and lists a Trigger', async (t) => {
const value = await localManagementFixture(t);
const task = await createTask(value);
const createRequest = triggerPutRequest(value, task, '1');
const createPath = writeCommand(
value,
'trigger.put',
createRequest,
'trigger-create',
);
const created = await runLocalTriggerCommandFile(createPath);
assert.equal(created.status, 'created');
assert.equal(created.trigger.revision, 1);
assert.equal(created.trigger.enabled, true);
assert.equal(created.trigger.schema, 'qinglong/cron@v1');
assert.equal(created.trigger.taskContentDigest, task.contentDigest);
assert.doesNotMatch(JSON.stringify(created), /expression|timezone/);
assert.equal(
(await runLocalTriggerCommandFile(createPath)).status,
'existing',
);
const disabled = await runLocalTriggerCommandFile(
writeCommand(
value,
'trigger.put',
triggerPutRequest(value, task, '2', {
expectedRevision: 1,
mutationId: '94000000-0000-4000-8000-000000000002',
failureAuditEventId: '95000000-0000-4000-8000-000000000002',
requestId: 'trigger-put-2',
enabled: false,
occurredAtMs: value.now + 2,
}),
'trigger-disable',
),
);
assert.equal(disabled.status, 'updated');
assert.equal(disabled.trigger.revision, 2);
assert.equal(disabled.trigger.enabled, false);
const inspected = await runLocalTriggerCommandFile(
writeCommand(
value,
'trigger.inspect',
{
projectId: 'default',
triggerId: 'trigger-product-entry',
requestId: 'trigger-inspect-1',
auditEventId: '96000000-0000-4000-8000-000000000001',
failureAuditEventId: '97000000-0000-4000-8000-000000000001',
},
'trigger-inspect',
),
);
assert.equal(inspected.found, true);
assert.equal(inspected.trigger.revision, 2);
assert.equal(Object.hasOwn(inspected.trigger, 'spec'), false);
const listed = await runLocalTriggerCommandFile(
writeCommand(
value,
'trigger.list',
{
projectId: 'default',
requestId: 'trigger-list-1',
auditEventId: '96000000-0000-4000-8000-000000000002',
failureAuditEventId: '97000000-0000-4000-8000-000000000002',
limit: 1,
},
'trigger-list',
),
);
assert.equal(listed.triggers.length, 1);
assert.equal(listed.triggers[0].triggerId, 'trigger-product-entry');
assert.equal(listed.nextCursor, null);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare('SELECT COUNT(*) AS count FROM "QingLong3TriggerRevisions"')
.get().count,
2,
);
assert.equal(
database
.prepare(
'SELECT trigger_revision AS revision FROM "QingLong3LocalTriggerSchedules"',
)
.get().revision,
2,
);
} finally {
database.close();
}
assert.deepEqual(
auditRows(value.databasePath)
.filter((row) => row.operationId.startsWith('trigger.'))
.map((row) => [row.operationId, row.outcome]),
[
['trigger.create', 'allowed'],
['trigger.update', 'allowed'],
['trigger.read', 'allowed'],
['trigger.read', 'allowed'],
],
);
});
test('allows an operator to manage Triggers but rejects a viewer atomically', async (t) => {
const operator = await localManagementFixture(t, { role: 'operator' });
const operatorTask = await createTask(operator, '3');
assert.equal(
(
await runLocalTriggerCommandFile(
writeCommand(
operator,
'trigger.put',
triggerPutRequest(operator, operatorTask, '3'),
'operator-trigger-create',
),
)
).status,
'created',
);
const viewer = await localManagementFixture(t, { role: 'viewer' });
const database = new DatabaseSync(viewer.databasePath);
try {
const {
requestId: _requestId,
failureAuditEventId: _failureAuditEventId,
...task
} = taskPutRequest(viewer, '4');
database
.prepare(
`INSERT INTO "QingLong3TaskDefinitions" (
"project_id", "task_id", "current_revision", "created_at_ms", "updated_at_ms"
) VALUES ('default', ?, 1, ?, ?)`,
)
.run(task.taskId, viewer.now, viewer.now);
const {
createTaskDefinitionRecord,
} = require('@qinglong/runtime-core/task-definition');
const record = createTaskDefinitionRecord(task, viewer.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 (?, ?, 1, ?, ?, NULL, ?, ?, ?, 1, ?, ?)`,
)
.run(
record.projectId,
record.taskId,
record.mutationId,
record.name,
record.kind,
JSON.stringify(record.spec),
JSON.stringify(record.labels),
record.contentDigest,
record.updatedAtMs,
);
await assert.rejects(
runLocalTriggerCommandFile(
writeCommand(
viewer,
'trigger.put',
triggerPutRequest(viewer, record, '4'),
'viewer-trigger-create',
),
),
{ code: 'LOCAL_TRIGGER_ADMINISTRATION_FORBIDDEN' },
);
} finally {
database.close();
}
const reader = new DatabaseSync(viewer.databasePath, { readOnly: true });
try {
assert.equal(
reader.prepare('SELECT COUNT(*) AS count FROM "QingLong3Triggers"').get()
.count,
0,
);
} finally {
reader.close();
}
});
test('rechecks the credential and RoleBinding fence inside the Trigger transaction', async (t) => {
const value = await localManagementFixture(t);
const task = await createTask(value, '5');
const runner = createLocalTriggerCommandRunner({
openDatabase: openLocalSqliteTriggerAdministrationDatabase,
authenticate: require('@qinglong/local-owner-console/authenticated-command')
.establishAuthenticatedLocalCommand,
now: Date.now,
createService(projectPolicy, mutations, source, audit, options) {
const fencedMutations = {
async appendAuthorizedTriggerRevision(mutation) {
const competing = new DatabaseSync(value.databasePath);
try {
competing
.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', 'automation-user', 2, 'revoked', NULL,
'trigger-race-revoke', 'user', 'automation-user', ?
)`,
)
.run(value.now + 5);
} finally {
competing.close();
}
return mutations.appendAuthorizedTriggerRevision(mutation);
},
};
return createLocalTriggerAdministrationService(
projectPolicy,
fencedMutations,
source,
audit,
options,
);
},
});
const request = triggerPutRequest(value, task, '5');
await assert.rejects(
runner.run(
writeCommand(value, 'trigger.put', request, 'trigger-fenced-create'),
),
{ code: 'TRIGGER_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare('SELECT COUNT(*) AS count FROM "QingLong3Triggers"')
.get().count,
0,
);
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents" WHERE event_id = ?',
)
.get(request.mutationId).count,
0,
);
} finally {
database.close();
}
});
test('rejects Trigger and audit replay drift without adding revisions', async (t) => {
const value = await localManagementFixture(t);
const task = await createTask(value, '6');
const original = triggerPutRequest(value, task, '6');
await runLocalTriggerCommandFile(
writeCommand(value, 'trigger.put', original, 'trigger-original'),
);
await assert.rejects(
runLocalTriggerCommandFile(
writeCommand(
value,
'trigger.put',
{
...original,
spec: {
...original.spec,
config: { ...original.spec.config, expression: '*/10 * * * *' },
},
failureAuditEventId: '95000000-0000-4000-8000-000000000007',
},
'trigger-drifted',
),
),
{ code: 'TRIGGER_CONFLICT' },
);
await assert.rejects(
runLocalTriggerCommandFile(
writeCommand(
value,
'trigger.put',
{
...original,
requestId: 'trigger-audit-drift',
failureAuditEventId: '95000000-0000-4000-8000-000000000008',
},
'trigger-audit-drifted',
),
),
{ code: 'TRIGGER_ADMINISTRATION_MUTATION_CONFLICT' },
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare('SELECT COUNT(*) AS count FROM "QingLong3TriggerRevisions"')
.get().count,
1,
);
} finally {
database.close();
}
});
test('requires a private Trigger command file and exposes one exact CLI', async (t) => {
const value = await localManagementFixture(t);
const task = await createTask(value, '8');
const commandPath = writeCommand(
value,
'trigger.put',
triggerPutRequest(value, task, '8'),
'trigger-broad',
);
fs.chmodSync(commandPath, 0o644);
await assert.rejects(runLocalTriggerCommandFile(commandPath), {
code: 'LOCAL_TRIGGER_COMMAND_CONFIGURATION_INVALID',
});
const cliPath = path.resolve(
__dirname,
'../dist/automation-management/triggerCli.js',
);
const help = spawnSync(process.execPath, [cliPath, '--help'], {
encoding: 'utf8',
});
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3-trigger run --command-file /);
const invalid = spawnSync(process.execPath, [cliPath, 'list'], {
encoding: 'utf8',
});
assert.equal(invalid.status, 64);
assert.equal(
JSON.parse(invalid.stderr).code,
'LOCAL_TRIGGER_CLI_USAGE_INVALID',
);
});