feat(ql3): add secret-backed console automation

This commit is contained in:
whyour
2026-08-29 19:44:04 +08:00
parent 53d65abd8d
commit f46fb44ac9
26 changed files with 2272 additions and 39 deletions
@@ -86,6 +86,51 @@ test('defers Trigger put Policy, presence and mutation to the route', async () =
]);
});
test('uses secret.manage for bounded Secret metadata and never widens the route input', async () => {
const { admission, events } = fixture();
assert.deepEqual(
await execute(
admission,
request({
operation: {
operationId: 'secret.list',
projectId: 'prj_default',
limit: 16,
after: { name: 'alpha' },
},
}),
),
{ statusCode: 200, body: { secrets: [], truncated: false } },
);
assert.deepEqual(events, [
'authenticate',
'authorize:secret.manage:prj_default',
'audit:allowed:secret.list',
'confirm',
'secrets:prj_default:16',
]);
});
test('defers Secret put Policy, presence and plaintext body to the fenced route', async () => {
const { admission, events } = fixture();
const prepared = await admission.prepare(
request({
localPresence: 'ql3p_bound',
operation: {
operationId: 'secret.put',
projectId: 'prj_default',
},
}),
);
assert.equal(prepared.bodyMode, 'json');
assert.equal(prepared.maximumBodyBytes, 20 * 1024);
assert.deepEqual(await prepared.handle({ plaintext: 'ephemeral' }), {
statusCode: 201,
body: { status: 'inserted' },
});
assert.deepEqual(events, ['authenticate', 'secret-put:prj_default']);
});
function request(overrides = {}) {
return Object.freeze({
requestId: 'local:019f70c0-0000-7000-8000-000000000001',
@@ -246,6 +291,18 @@ function fixture(overrides = {}) {
return { statusCode: 201, body: { status: 'created' } };
},
},
secretListRoute: {
async handle(value) {
events.push(`secrets:${value.projectId}:${value.limit}`);
return { statusCode: 200, body: { secrets: [], truncated: false } };
},
},
secretPutRoute: {
async handle(value) {
events.push(`secret-put:${value.projectId}`);
return { statusCode: 201, body: { status: 'inserted' } };
},
},
now: () => 10_000,
randomUuid: () => '019f70c0-0000-4000-8000-000000000002',
...overrides,
@@ -90,6 +90,15 @@ test('loads one bounded offline Console asset closure', () => {
assert.match(text, /triggers\/\$\{mutation\.triggerId\}/u);
assert.match(text, /state\.view === 'triggers'/u);
assert.match(text, /trigger_fence_rejected/u);
assert.match(text, /state\.view === 'secrets'/u);
assert.match(text, /secret-mutation/u);
assert.match(text, /createSecretRef/u);
assert.match(text, /kind: 'secret'/u);
assert.match(text, /secret_query_unavailable/u);
assert.equal(
/localStorage.*plaintext|sessionStorage.*plaintext/u.test(text),
false,
);
}
if (requestPath === '/') {
assert.match(text, /id="task-editor-dialog"/u);
@@ -99,6 +108,10 @@ test('loads one bounded offline Console asset closure', () => {
assert.match(text, /id="presence-copy"/u);
assert.match(text, /id="trigger-editor-dialog"/u);
assert.match(text, /data-view="triggers"/u);
assert.match(text, /data-view="secrets"/u);
assert.match(text, /id="secret-editor-dialog"/u);
assert.match(text, /id="task-secret-bindings-input"/u);
assert.match(text, /AES-256-GCM/u);
}
}
assert.ok(totalBytes <= 192 * 1024);
@@ -52,14 +52,19 @@ function request(port, path, options = {}) {
function preparedAdmission(handler) {
return {
async prepare(value) {
const json = ['run.cancel', 'task.start', 'task.put'].includes(
value.operation.operationId,
);
const json = [
'run.cancel',
'task.start',
'task.put',
'secret.put',
].includes(value.operation.operationId);
return {
bodyMode: json ? 'json' : 'none',
maximumBodyBytes:
value.operation.operationId === 'task.put'
? 72 * 1024
: value.operation.operationId === 'secret.put'
? 20 * 1024
: json
? 512
: 0,
@@ -83,7 +88,8 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
if (
value.operation.operationId === 'run.cancel' ||
value.operation.operationId === 'task.start' ||
value.operation.operationId === 'task.put'
value.operation.operationId === 'task.put' ||
value.operation.operationId === 'secret.put'
) {
return { statusCode: 202, body: { accepted: body } };
}
@@ -140,6 +146,12 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
body: { task: { taskId: value.operation.taskId } },
};
}
if (value.operation.operationId === 'secret.list') {
return {
statusCode: 200,
body: { secrets: [], truncated: false },
};
}
return {
statusCode: 200,
body: { runs: [], hasMore: false, input: value.operation.input },
@@ -356,6 +368,41 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
});
assert.equal(observed[11].localPresence, 'ql3p_authoring_read_proof');
const secrets = await request(
port,
'/api/v3/projects/prj_default/secrets?limit=8&after=YWxwaGE',
);
assert.deepEqual(secrets.body, { secrets: [], truncated: false });
assert.deepEqual(observed[12].operation, {
operationId: 'secret.list',
projectId: 'prj_default',
limit: 8,
after: { name: 'alpha' },
});
const secretPutBody = JSON.stringify({ name: 'github-token' });
const secretPut = await request(
port,
'/api/v3/projects/prj_default/secrets',
{
method: 'PUT',
headers: {
authorization: 'Bearer opaque',
'x-qinglong-local-presence': 'ql3p_secret_bound_proof',
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(secretPutBody)),
},
body: secretPutBody,
},
);
assert.equal(secretPut.statusCode, 202);
assert.deepEqual(secretPut.body.accepted, JSON.parse(secretPutBody));
assert.deepEqual(observed[13].operation, {
operationId: 'secret.put',
projectId: 'prj_default',
});
assert.equal(observed[13].localPresence, 'ql3p_secret_bound_proof');
for (const invalidPath of [
'/api/v3/projects/prj_default/runs/run_123?expanded=true',
'/api/v3/projects/prj_default/runs/run%5f123',
@@ -402,6 +449,17 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
assert.equal(invalid.statusCode, 400);
assert.deepEqual(invalid.body, { code: 'invalid_task_list_query' });
}
for (const invalidQuery of [
'/api/v3/projects/prj_default/secrets?',
'/api/v3/projects/prj_default/secrets?limit=08',
'/api/v3/projects/prj_default/secrets?limit=65',
'/api/v3/projects/prj_default/secrets?after=Y',
'/api/v3/projects/prj_default/secrets?unknown=value',
]) {
const invalid = await request(port, invalidQuery);
assert.equal(invalid.statusCode, 400);
assert.deepEqual(invalid.body, { code: 'invalid_secret_list_query' });
}
for (const invalidQuery of [
'/api/v3/projects/prj_default/runs/run_123/events?',
'/api/v3/projects/prj_default/runs/run_123/events?after_sequence=07',
@@ -425,7 +483,7 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
assert.equal(invalid.statusCode, 400);
assert.deepEqual(invalid.body, { code: 'invalid_run_step_list_query' });
}
assert.equal(observed.length, 12);
assert.equal(observed.length, 14);
assert.deepEqual(
await Promise.all([surface.stopAndDrain(), surface.stopAndDrain()]),
['stopped', 'stopped'],
@@ -0,0 +1,313 @@
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 {
createLocalPresenceProofManager,
} = require('../dist/authentication/localPresenceProof.js');
const {
createLocalApiSecretListRoute,
createLocalApiSecretPutRoute,
} = require('../dist/secret/secretRoutes.js');
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner' }),
authenticationId: 'local_credential:owner-console:1',
authenticatedAtMs: 9_000,
expiresAtMs: 60_000,
assurance: 'single_factor',
});
const FENCE = Object.freeze({
credentialId: 'owner-console',
credentialVersion: 1,
pepperKeyId: 'owner-v1',
materialDigest: 'a'.repeat(64),
subjectType: 'user',
subjectId: 'owner',
secretDigest: 'b'.repeat(64),
notBeforeAtMs: 1,
expiresAtMs: 60_000,
});
function uuidFactory() {
let sequence = 400;
return () => {
sequence += 1;
return `019f9200-0000-4000-8000-${String(sequence).padStart(12, '0')}`;
};
}
function body(overrides = {}) {
return Object.freeze({
name: 'github-token',
plaintext: 'never-return-this-value',
mutationId: '019f9200-0000-4000-8000-000000000101',
expectedCurrentVersion: 0,
...overrides,
});
}
function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-secret-put-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const calls = [];
const presenceProof = createLocalPresenceProofManager({
deploymentRoot,
profile: 'edge',
now: () => 10_000,
randomUuid: uuidFactory(),
randomSecret: () => Buffer.alloc(32, 17),
});
t.after(() => presenceProof.close());
const projectPolicy = {
async resolve(projectId, subject) {
calls.push(['policy', projectId, subject]);
return {
project: {
id: projectId,
name: 'Default',
slug: 'default',
status: 'active',
version: 3,
createdAtMs: 1,
updatedAtMs: 2,
},
binding: {
projectId,
subject,
version: 5,
state: 'active',
role: 'owner',
mutationId: 'owner-binding',
changedBy: { type: 'user', id: 'bootstrap-owner' },
createdAtMs: 2,
},
};
},
async append() {
throw new Error('not used');
},
};
const route = createLocalApiSecretPutRoute({
projectPolicy,
async secretAdministrationForCredential(fence) {
calls.push(['credential-fence', fence]);
return {
async resolveLocalSecretAdministrationMutation() {
return null;
},
async appendAuthorizedLocalSecretEnvelope(command) {
calls.push(['mutation', command]);
return {
status: 'inserted',
envelope: command.envelope,
audit: command.audit,
};
},
async record() {
throw new Error('not used');
},
};
},
securityAudit: {
async record(record) {
calls.push(['audit', record]);
},
},
secretKeys: {
async active() {
calls.push(['active-key']);
return { keyId: 'active-key', key: Buffer.alloc(32, 23) };
},
async resolve() {
return null;
},
},
presenceProof,
now: () => 10_000,
randomUuid: uuidFactory(),
});
const authenticated = Object.freeze({
principal: PRINCIPAL,
credentialFence: FENCE,
async confirm() {
calls.push(['confirm']);
},
});
return { route, calls, deploymentRoot, authenticated };
}
function request(state, requestBody, overrides = {}) {
return Object.freeze({
requestId: 'local:019f9200-0000-4000-8000-000000000301',
projectId: 'default',
body: requestBody,
presence: null,
authenticated: state.authenticated,
signal: new AbortController().signal,
...overrides,
});
}
function readProof(state, challenge) {
return JSON.parse(
fs.readFileSync(
path.join(
state.deploymentRoot,
'console-presence',
challenge.body.proofFileName,
),
'utf8',
),
).proof;
}
test('lists bounded Secret metadata without storage or mutation material', async () => {
const route = createLocalApiSecretListRoute({
async listLocalSecretMetadata(options) {
assert.deepEqual(options, { projectId: 'default', limit: 1 });
return {
secrets: [
{
projectId: 'default',
name: 'github-token',
currentVersion: 2,
createdAtMs: 10_000,
},
],
truncated: true,
next: { name: 'github-token' },
};
},
});
const result = await route.handle({ projectId: 'default', limit: 1 });
assert.equal(result.statusCode, 200);
assert.deepEqual(Object.keys(result.body.secrets[0]).sort(), [
'createdAtMs',
'currentVersion',
'name',
'secretRef',
]);
assert.equal(
result.body.secrets[0].secretRef.startsWith('qlsecret:v1:'),
true,
);
assert.equal(result.body.next.after, 'Z2l0aHViLXRva2Vu');
assert.doesNotMatch(
JSON.stringify(result.body),
/cipher|plaintext|mutation|keyId/u,
);
});
test('fails closed on widened, cross-Project and over-budget metadata', async () => {
for (const page of [
{
secrets: [
{
projectId: 'other',
name: 'github-token',
currentVersion: 2,
createdAtMs: 10_000,
},
],
truncated: false,
},
{
secrets: [
{
projectId: 'default',
name: 'github-token',
currentVersion: 2,
createdAtMs: 10_000,
ciphertext: 'forbidden',
},
],
truncated: false,
},
{
secrets: [
{
projectId: 'default',
name: 'first',
currentVersion: 1,
createdAtMs: 10_000,
},
{
projectId: 'default',
name: 'second',
currentVersion: 1,
createdAtMs: 10_001,
},
],
truncated: true,
next: { name: 'second' },
},
]) {
const route = createLocalApiSecretListRoute({
async listLocalSecretMetadata() {
return page;
},
});
assert.deepEqual(await route.handle({ projectId: 'default', limit: 1 }), {
statusCode: 503,
body: { code: 'secret_query_unavailable' },
});
}
});
test('requires exact local presence and returns no Secret plaintext', async (t) => {
const state = fixture(t);
const command = body();
const challenge = await state.route.handle(request(state, command));
assert.equal(challenge.statusCode, 428);
const result = await state.route.handle(
request(state, command, { presence: readProof(state, challenge) }),
);
assert.equal(result.statusCode, 201);
assert.deepEqual(result.body, {
status: 'inserted',
secret: {
name: 'github-token',
currentVersion: 1,
secretRef: result.body.secret.secretRef,
},
});
assert.equal(JSON.stringify(result).includes(command.plaintext), false);
const mutation = state.calls.find(([kind]) => kind === 'mutation')[1];
assert.equal(
Buffer.from(mutation.envelope.ciphertext, 'base64url').includes(
Buffer.from(command.plaintext),
),
false,
);
assert.deepEqual(
state.calls
.filter(([kind]) => kind === 'audit')
.map(([, audit]) => [audit.operationId, audit.outcome, audit.reasons[0]]),
[['secret.create', 'approval_required', 'local_presence_required']],
);
});
test('binds proof to exact plaintext digest and rejects widened bodies', async (t) => {
const state = fixture(t);
assert.deepEqual(
await state.route.handle(request(state, { ...body(), extra: true })),
{ statusCode: 400, body: { code: 'invalid_secret' } },
);
const command = body();
const challenge = await state.route.handle(request(state, command));
const proof = readProof(state, challenge);
assert.deepEqual(
await state.route.handle(
request(state, body({ plaintext: 'changed-value' }), { presence: proof }),
),
{ statusCode: 401, body: { code: 'local_presence_rejected' } },
);
assert.equal(state.calls.filter(([kind]) => kind === 'mutation').length, 0);
});
@@ -187,8 +187,8 @@ function seed(databasePath, materialDigest) {
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES (
'default', 'user', 'local-api-user', 1, 'active', 'operator',
'grant-local-api-operator', 'user', 'local-api-user', ?
'default', 'user', 'local-api-user', 1, 'active', 'owner',
'grant-local-api-owner', 'user', 'local-api-user', ?
)`,
)
.run(NOW - 500);
@@ -442,6 +442,19 @@ test('serves an authenticated Run through one real SQLite authority and durable
triggers: runtime.triggers,
triggerAdministrationForCredential:
runtime.triggerAdministrationForCredential,
localSecretMetadata: runtime.localSecretMetadata,
localSecretAdministrationForCredential:
runtime.localSecretAdministrationForCredential,
localSecretKeys: {
async active() {
return { keyId: 'integration-key', key: Buffer.alloc(32, 83) };
},
async resolve(keyId) {
return keyId === 'integration-key'
? { keyId, key: Buffer.alloc(32, 83) }
: null;
},
},
apiCredentials: runtime.apiCredentials,
ownerPepper: runtime.ownerPepper,
projectPolicy: runtime.projectPolicy,
@@ -531,6 +544,63 @@ test('serves an authenticated Run through one real SQLite authority and durable
{ statusCode: 404, body: { code: 'task_not_found' } },
);
const secretPlaintext = 'local-api-secret-value';
const secretBody = JSON.stringify({
name: 'github-token',
plaintext: secretPlaintext,
mutationId: '019f7300-0000-4000-8000-000000000700',
expectedCurrentVersion: 0,
});
const secretPath = '/api/v3/projects/default/secrets';
const secretOptions = {
method: 'PUT',
headers: {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(secretBody)),
},
body: secretBody,
};
const secretChallenge = await request(
port,
`Bearer ${TOKEN}`,
secretPath,
secretOptions,
);
assert.equal(secretChallenge.statusCode, 428);
assert.equal(secretChallenge.body.code, 'local_presence_required');
const secretProof = JSON.parse(
fs.readFileSync(
path.join(root, 'console-presence', secretChallenge.body.proofFileName),
'utf8',
),
);
const secretCreated = await request(port, `Bearer ${TOKEN}`, secretPath, {
...secretOptions,
headers: {
...secretOptions.headers,
'x-qinglong-local-presence': secretProof.proof,
},
});
assert.equal(secretCreated.statusCode, 201);
assert.equal(secretCreated.body.secret.currentVersion, 1);
assert.match(secretCreated.body.secret.secretRef, /^qlsecret:v1:/u);
assert.equal(JSON.stringify(secretCreated).includes(secretPlaintext), false);
const secretList = await request(
port,
`Bearer ${TOKEN}`,
`${secretPath}?limit=64`,
);
assert.equal(secretList.statusCode, 200);
assert.deepEqual(secretList.body.secrets, [
{ ...secretCreated.body.secret, createdAtMs: NOW },
]);
assert.equal(secretList.body.truncated, false);
assert.doesNotMatch(
JSON.stringify(secretList),
new RegExp(`${secretPlaintext}|ciphertext|keyId|mutationId`, 'u'),
);
const taskCreateBody = JSON.stringify({
expectedRevision: null,
mutationId: '019f7300-0000-4000-8000-000000000701',
@@ -545,6 +615,13 @@ test('serves an authenticated Run through one real SQLite authority and durable
file: '/bin/echo',
args: ['console-created'],
},
environment: [
{
name: 'API_TOKEN',
kind: 'secret',
secretRef: secretCreated.body.secret.secretRef,
},
],
},
},
labels: { source: 'local-console' },
@@ -973,6 +1050,40 @@ test('serves an authenticated Run through one real SQLite authority and durable
);
const auditReader = new DatabaseSync(databasePath, { readOnly: true });
try {
const encryptedSecret = auditReader
.prepare(
`SELECT ciphertext FROM "QingLong3LocalSecretEnvelopes"
WHERE project_id = 'default' AND secret_name = 'github-token'
AND version = 1`,
)
.get();
assert.equal(
Buffer.from(encryptedSecret.ciphertext).includes(
Buffer.from(secretPlaintext),
),
false,
);
const createdSpec = JSON.parse(
auditReader
.prepare(
`SELECT revision.spec_json AS specJson
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = 'default'
AND head.task_id = 'task-console-created'`,
)
.get().specJson,
);
assert.deepEqual(createdSpec.config.environment, [
{
name: 'API_TOKEN',
kind: 'secret',
secretRef: secretCreated.body.secret.secretRef,
},
]);
assert.deepEqual(
auditReader
.prepare(
@@ -981,7 +1092,8 @@ test('serves an authenticated Run through one real SQLite authority and durable
'run.get', 'run.list', 'run.events.list', 'run.steps.list',
'run.cancel', 'task.authoring.read', 'task.create', 'task.get',
'task.list', 'task.start', 'task.update', 'run.log.read',
'trigger.create', 'trigger.get', 'trigger.list', 'trigger.update'
'trigger.create', 'trigger.get', 'trigger.list', 'trigger.update',
'secret.create', 'secret.list'
)
ORDER BY operation_id, outcome`,
)
@@ -996,6 +1108,9 @@ test('serves an authenticated Run through one real SQLite authority and durable
'run.list:allowed',
'run.log.read:allowed',
'run.steps.list:allowed',
'secret.create:allowed',
'secret.create:approval_required',
'secret.list:allowed',
'task.authoring.read:allowed',
'task.authoring.read:approval_required',
'task.create:allowed',