Files
qinglong/packages/ql3-runtime-core/test/toolExecutionStartBarrier.test.cjs
T

481 lines
14 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { join } = require('node:path');
const { test } = require('node:test');
const {
consumeApprovalRequest,
createApprovalRequest,
decideApprovalRequest,
} = require('../dist/approved-action/approvedAction');
const {
createPluginPackageResourceGenerationFromReferences,
} = require('../dist/plugin-package/pluginPackageResourceGeneration');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionRegistry,
} = require('../dist/tool-execution/tool-registry/projectToolDefinitionSnapshot');
const {
createStepRunRecord,
transitionStepRunMutation,
transitionStepRunRecord,
} = require('../dist/run/stepRun');
const {
InvalidToolExecutionStartBarrierError,
TOOL_EXECUTION_START_BARRIER_SCHEMA,
TOOL_EXECUTION_START_COMMAND_SCHEMA,
createToolExecutionStartCommand,
normalizeToolExecutionStartBarrierRecord,
normalizeToolExecutionStartCommand,
toolExecutionStartBarrierRecord,
} = require('../dist/tool-execution/toolExecutionStartBarrier');
const {
TOOL_EXECUTION_START_AUDIT_OPERATION,
createToolExecutionEvidenceBundle,
toolExecutionAdmissionEvidence,
} = require('../dist/tool-execution/toolExecutionEvidence');
const {
TrustedToolHandlerBindingRegistry,
admitTrustedToolExecution,
createTrustedToolHandlerBinding,
createTrustedToolInvocationPlan,
trustedToolContractIdentityDigest,
trustedToolInvocationApprovalBinding,
} = require('../dist/tool-execution/trustedToolInvocation');
const {
prepareToolInvocation,
} = require('../dist/tool-execution/tool-registry/toolRegistry');
const DIGEST_A = 'a'.repeat(64);
const DIGEST_B = 'b'.repeat(64);
const DIGEST_C = 'c'.repeat(64);
const REQUESTER = Object.freeze({ type: 'user', id: 'usr-tool-owner' });
const SYSTEM = Object.freeze({ type: 'system', id: 'tool-dispatcher' });
const FENCE = Object.freeze({ projectVersion: 3, bindingVersion: 7 });
const NOW_MS = 1_400;
function definition() {
return {
name: 'demo.compare',
version: '1.0.0',
description: 'Compare one bounded Run projection',
inputSchema: {
type: 'object',
properties: {
runId: { type: 'string', minLength: 1, maxLength: 64 },
},
required: ['runId'],
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
summary: { type: 'string', maxLength: 1024 },
},
required: ['summary'],
additionalProperties: false,
},
effect: 'read',
risk: 'low',
requiredPermissions: ['run.read'],
timeoutSeconds: 30,
};
}
function snapshot() {
const generation = createPluginPackageResourceGenerationFromReferences({
installationId: 'install-demo',
projectId: 'project-001',
packageName: 'demo',
lockDigest: DIGEST_A,
generation: 1,
previousActiveLockDigest: null,
contentDigest: DIGEST_B,
resources: [],
});
return createProjectToolDefinitionSnapshot({
projectId: 'project-001',
contributions: [
{
generation,
revisionDigest: DIGEST_C,
definitions: [definition()],
},
],
});
}
function principal() {
return {
subject: REQUESTER,
authenticationId: 'auth-tool-1',
authenticatedAtMs: 800,
expiresAtMs: 10_000,
assurance: 'local_console',
};
}
function authorizer(effect = 'allow') {
return {
async authorize() {
return {
effect,
reasons:
effect === 'allow'
? ['role_grant']
: ['agent_action_requires_approval'],
fence: FENCE,
};
},
};
}
async function approvedDispatch(currentPlan) {
const action = trustedToolInvocationApprovalBinding(
currentPlan.plan,
currentPlan.bindings,
);
const request = createApprovalRequest({
id: 'approval-tool-001',
projectId: currentPlan.plan.projectId,
action,
risk: currentPlan.plan.risk,
decisionMode: 'human_confirmation',
requestedBy: REQUESTER,
requestedAtMs: 1_050,
expiresAtMs: 9_000,
requestFence: FENCE,
});
const approved = decideApprovalRequest(request, {
expectedVersion: 1,
decisionId: 'decision-tool-001',
decision: 'approved',
reasonCode: 'reviewed',
principal: principal(),
decidedAtMs: 1_100,
authorizationFence: FENCE,
});
return consumeApprovalRequest(approved, {
expectedVersion: 2,
consumptionId: 'consume-tool-001',
dispatchId: 'dispatch-tool-001',
action,
requestedBy: REQUESTER,
consumedBy: SYSTEM,
consumedAtMs: 1_200,
authorizationFence: FENCE,
}).dispatch;
}
async function fixture(options = {}) {
const currentSnapshot = snapshot();
const binding = createTrustedToolHandlerBinding(currentSnapshot, {
tool: { name: 'demo.compare', version: '1.0.0' },
adapter: { id: 'builtin.demo-compare', version: '1.0.0' },
executionClass: 'builtin_in_process',
profiles: ['edge', 'standalone'],
authorities: ['database.read'],
timeoutSeconds: 20,
redactionContract: {
id: 'redaction.demo-compare',
version: '1.0.0',
},
auditContract: { id: 'audit.tool-call', version: '1.0.0' },
});
const bindings = new TrustedToolHandlerBindingRegistry(currentSnapshot, [
binding,
]);
const approvalRequired = options.approvalRequired === true;
const invocation = await prepareToolInvocation(
projectToolDefinitionRegistry(currentSnapshot),
{
projectId: 'project-001',
principal: principal(),
nowMs: 900,
tool: { name: 'demo.compare', version: '1.0.0' },
input: { runId: 'run-001' },
},
authorizer(approvalRequired ? 'require_approval' : 'allow'),
);
const plan = (
await createTrustedToolInvocationPlan(bindings, invocation, {
actionRef: 'tool-plan:run-001',
inputArtifactId: 'artifact-input-001',
previewArtifactId: 'artifact-preview-001',
artifactKeyId: 'tool-key-test',
artifactKey: Buffer.alloc(32, 7),
artifactNonce: Buffer.alloc(12, 9),
profile: 'edge',
preview: {
title: 'Compare Run',
summary: 'Reads one Run projection',
fields: [{ kind: 'identifier', label: 'Run', value: 'run-001' }],
warnings: [],
},
sealedAtMs: 1_000,
})
).plan;
const ready = createStepRunRecord({
id: 'step-run-001',
runId: 'run-001',
stepKey: 'workflow.compare',
kind: 'tool',
definitionRef: 'tool:demo.compare@1.0.0',
definitionDigest: binding.definitionDigest,
required: true,
initialStatus: 'ready',
inputRef: 'artifact:step-input-001',
mutationId: 'step-create-001',
createdAtMs: 1_000,
});
const planWithBindings = { plan, bindings };
const dispatch = approvalRequired
? await approvedDispatch(planWithBindings)
: undefined;
const previous = approvalRequired
? transitionStepRunRecord(ready, {
expectedVersion: ready.version,
expectedDigest: ready.stepRunDigest,
mutationId: 'step-waiting-002',
to: 'waiting_approval',
atMs: 1_200,
approvalRequestId: dispatch.approvalRequestId,
})
: ready;
const evidence = createToolExecutionEvidenceBundle({
traceId: '1'.repeat(32),
spanId: '2'.repeat(16),
projectId: 'project-001',
runId: 'run-001',
stepRunId: previous.id,
invocationPlanDigest: plan.planDigest,
bindingDigest: binding.bindingDigest,
adapterDigest:
options.adapterDigest ??
trustedToolContractIdentityDigest(binding.adapter),
redactionContractDigest: trustedToolContractIdentityDigest(
binding.redactionContract,
),
auditContractDigest: trustedToolContractIdentityDigest(
binding.auditContract,
),
audit: {
eventId: '40000000-0000-4000-8000-000000000001',
requestId: 'tool-request-001',
operationId: TOOL_EXECUTION_START_AUDIT_OPERATION,
projectId: 'project-001',
subject: REQUESTER,
authenticationId: 'auth-tool-1',
outcome: 'allowed',
reasons: ['tool_execution_start'],
fence: FENCE,
occurredAtMs: NOW_MS,
},
createdAtMs: NOW_MS,
});
const admission = await admitTrustedToolExecution(bindings, plan, {
principal: principal(),
profile: 'edge',
nowMs: NOW_MS,
authorizer: authorizer(),
evidence: {
stepRun: {
id: previous.id,
version: previous.version,
digest: previous.stepRunDigest,
},
...toolExecutionAdmissionEvidence(evidence),
},
...(dispatch ? { dispatch } : {}),
});
const stepRunMutation = transitionStepRunMutation(
previous,
{
expectedVersion: previous.version,
expectedDigest: previous.stepRunDigest,
mutationId: 'step-running-003',
to: 'running',
atMs: NOW_MS,
...(dispatch ? { approvalRequestId: dispatch.approvalRequestId } : {}),
},
{
expectedRunVersion: 5,
expectedRunEventSequence: 8,
eventId: 'event-step-running-001',
dedupeKey: 'step-running:step-run-001',
actor: REQUESTER,
},
);
return { admission, binding, evidence, stepRunMutation };
}
function copy(value) {
return structuredClone(value);
}
test('creates one immutable same-transaction Tool start command and barrier', async () => {
const current = await fixture();
const command = createToolExecutionStartCommand({
startId: 'tool-start-001',
admission: current.admission,
evidence: current.evidence,
stepRunMutation: current.stepRunMutation,
});
const barrier = toolExecutionStartBarrierRecord(command);
assert.equal(command.schema, TOOL_EXECUTION_START_COMMAND_SCHEMA);
assert.equal(barrier.schema, TOOL_EXECUTION_START_BARRIER_SCHEMA);
assert.equal(barrier.projectId, 'project-001');
assert.equal(barrier.stepRunId, 'step-run-001');
assert.equal(barrier.previousStepRunVersion, 1);
assert.equal(barrier.startedStepRunVersion, 2);
assert.equal(barrier.approvalRequestId, null);
assert.equal(barrier.adapterDigest, current.evidence.trace.adapterDigest);
assert.match(command.commandDigest, /^[0-9a-f]{64}$/);
assert.match(barrier.barrierDigest, /^[0-9a-f]{64}$/);
assert.equal(Object.isFrozen(command), true);
assert.deepEqual(normalizeToolExecutionStartCommand(command), command);
assert.deepEqual(normalizeToolExecutionStartBarrierRecord(barrier), barrier);
for (const sensitive of ['input', 'handler', 'execute', 'token', 'secret']) {
assert.equal(sensitive in barrier, false);
}
});
test('binds an approved start to the exact waiting Approval request', async () => {
const current = await fixture({ approvalRequired: true });
const command = createToolExecutionStartCommand({
startId: 'tool-start-approved-001',
admission: current.admission,
evidence: current.evidence,
stepRunMutation: current.stepRunMutation,
});
const barrier = toolExecutionStartBarrierRecord(command);
assert.equal(barrier.approvalRequestId, 'approval-tool-001');
assert.equal(barrier.approvalDispatchId, 'dispatch-tool-001');
assert.equal(command.stepRunMutation.previousStatus, 'waiting_approval');
assert.equal(
command.stepRunMutation.stepRun.approvalRequestId,
barrier.approvalRequestId,
);
});
test('rejects detached contract, StepRun, audit and approval bindings', async () => {
const current = await fixture();
const wrongContract = await fixture({ adapterDigest: DIGEST_A });
assert.notEqual(
DIGEST_A,
trustedToolContractIdentityDigest(wrongContract.binding.adapter),
);
assert.throws(
() =>
createToolExecutionStartCommand({
startId: 'tool-start-wrong-contract',
admission: wrongContract.admission,
evidence: wrongContract.evidence,
stepRunMutation: wrongContract.stepRunMutation,
}),
InvalidToolExecutionStartBarrierError,
);
assert.throws(
() =>
createToolExecutionStartCommand({
startId: 'tool-start-wrong-step',
admission: current.admission,
evidence: current.evidence,
stepRunMutation: {
...copy(current.stepRunMutation),
runId: 'run-other',
},
}),
InvalidToolExecutionStartBarrierError,
);
const approved = await fixture({ approvalRequired: true });
assert.throws(
() =>
createToolExecutionStartCommand({
startId: 'tool-start-wrong-approval',
admission: approved.admission,
evidence: approved.evidence,
stepRunMutation: current.stepRunMutation,
}),
InvalidToolExecutionStartBarrierError,
);
});
test('rejects unknown fields, accessors and digest tampering', async () => {
const current = await fixture();
const command = createToolExecutionStartCommand({
startId: 'tool-start-001',
admission: current.admission,
evidence: current.evidence,
stepRunMutation: current.stepRunMutation,
});
const barrier = toolExecutionStartBarrierRecord(command);
assert.throws(
() =>
normalizeToolExecutionStartCommand({
...copy(command),
commandDigest: DIGEST_A,
}),
InvalidToolExecutionStartBarrierError,
);
assert.throws(
() =>
normalizeToolExecutionStartBarrierRecord({
...copy(barrier),
adapterDigest: DIGEST_A,
}),
InvalidToolExecutionStartBarrierError,
);
assert.throws(
() => normalizeToolExecutionStartCommand({ ...copy(command), extra: 1 }),
InvalidToolExecutionStartBarrierError,
);
const accessor = copy(command);
Object.defineProperty(accessor, 'startId', {
enumerable: true,
get() {
return 'tool-start-accessor';
},
});
assert.throws(
() => normalizeToolExecutionStartCommand(accessor),
InvalidToolExecutionStartBarrierError,
);
});
test('publishes the contract through root and explicit subpath without authority', () => {
const root = require('../dist');
const subpath = require('@qinglong/runtime-core/tool-execution-start-barrier');
assert.equal(
root.createToolExecutionStartCommand,
createToolExecutionStartCommand,
);
assert.equal(
subpath.toolExecutionStartBarrierRecord,
toolExecutionStartBarrierRecord,
);
const source = readFileSync(
join(
__dirname,
'..',
'src',
'tool-execution',
'toolExecutionStartBarrier.ts',
),
'utf8',
);
for (const authority of [
'node:child_process',
'node:fs',
'node:http',
'node:https',
'node:net',
'node:worker_threads',
]) {
assert.equal(source.includes(`from '${authority}'`), false);
}
});