mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
262 lines
7.7 KiB
JavaScript
262 lines
7.7 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { readFileSync } = require('node:fs');
|
|
const { join } = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
InvalidToolExecutionEvidenceError,
|
|
MAX_TOOL_EXECUTION_EVIDENCE_PAGE_SIZE,
|
|
TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA,
|
|
TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA,
|
|
TOOL_EXECUTION_START_AUDIT_OPERATION,
|
|
TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA,
|
|
createToolExecutionEvidenceBundle,
|
|
normalizeListToolExecutionEvidenceQuery,
|
|
normalizeListToolExecutionEvidenceResult,
|
|
normalizeToolExecutionAuditReceipt,
|
|
normalizeToolExecutionEvidenceBundle,
|
|
normalizeToolExecutionTraceAnchor,
|
|
toolExecutionAdmissionEvidence,
|
|
toolExecutionAuditRecordDigest,
|
|
} = require('../dist/tool-execution/toolExecutionEvidence');
|
|
|
|
const DIGEST_A = 'a'.repeat(64);
|
|
const DIGEST_B = 'b'.repeat(64);
|
|
const DIGEST_C = 'c'.repeat(64);
|
|
const DIGEST_D = 'd'.repeat(64);
|
|
const DIGEST_E = 'e'.repeat(64);
|
|
|
|
function audit(overrides = {}) {
|
|
return {
|
|
eventId: '40000000-0000-4000-8000-000000000001',
|
|
requestId: 'tool-request-001',
|
|
operationId: TOOL_EXECUTION_START_AUDIT_OPERATION,
|
|
projectId: 'project-001',
|
|
subject: { type: 'agent', id: 'agent-001' },
|
|
authenticationId: 'auth-agent-001',
|
|
outcome: 'allowed',
|
|
reasons: ['tool_execution_start'],
|
|
fence: { projectVersion: 3, bindingVersion: 4 },
|
|
occurredAtMs: 1_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function input(overrides = {}) {
|
|
return {
|
|
traceId: '1'.repeat(32),
|
|
spanId: '2'.repeat(16),
|
|
projectId: 'project-001',
|
|
runId: 'run-001',
|
|
stepRunId: 'step-run-001',
|
|
invocationPlanDigest: DIGEST_A,
|
|
bindingDigest: DIGEST_B,
|
|
adapterDigest: DIGEST_C,
|
|
redactionContractDigest: DIGEST_D,
|
|
auditContractDigest: DIGEST_E,
|
|
audit: audit(),
|
|
createdAtMs: 1_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function copy(value) {
|
|
return structuredClone(value);
|
|
}
|
|
|
|
test('creates one immutable low-sensitive Trace and Audit evidence bundle', () => {
|
|
const bundle = createToolExecutionEvidenceBundle(input());
|
|
assert.equal(bundle.schema, TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA);
|
|
assert.equal(bundle.trace.schema, TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA);
|
|
assert.equal(bundle.receipt.schema, TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA);
|
|
assert.equal(bundle.trace.parentSpanId, null);
|
|
assert.match(bundle.trace.traceDigest, /^[0-9a-f]{64}$/);
|
|
assert.match(bundle.receipt.auditRecordDigest, /^[0-9a-f]{64}$/);
|
|
assert.match(bundle.receipt.receiptDigest, /^[0-9a-f]{64}$/);
|
|
assert.equal(Object.isFrozen(bundle), true);
|
|
assert.equal(Object.isFrozen(bundle.trace), true);
|
|
assert.equal(Object.isFrozen(bundle.audit), true);
|
|
assert.equal(Object.isFrozen(bundle.receipt), true);
|
|
assert.deepEqual(normalizeToolExecutionEvidenceBundle(bundle), bundle);
|
|
assert.deepEqual(toolExecutionAdmissionEvidence(bundle), {
|
|
trace: {
|
|
traceId: bundle.trace.traceId,
|
|
spanId: bundle.trace.spanId,
|
|
digest: bundle.trace.traceDigest,
|
|
},
|
|
audit: {
|
|
eventId: bundle.audit.eventId,
|
|
digest: bundle.receipt.receiptDigest,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('uses canonical domain-separated digests and binds every authority fact', () => {
|
|
const first = createToolExecutionEvidenceBundle(input());
|
|
const replay = createToolExecutionEvidenceBundle(input());
|
|
assert.deepEqual(replay, first);
|
|
assert.equal(
|
|
toolExecutionAuditRecordDigest(first.audit),
|
|
first.receipt.auditRecordDigest,
|
|
);
|
|
|
|
const changed = createToolExecutionEvidenceBundle(
|
|
input({
|
|
adapterDigest: 'f'.repeat(64),
|
|
audit: audit({
|
|
eventId: '40000000-0000-4000-8000-000000000002',
|
|
}),
|
|
}),
|
|
);
|
|
assert.notEqual(changed.trace.traceDigest, first.trace.traceDigest);
|
|
assert.notEqual(changed.receipt.receiptDigest, first.receipt.receiptDigest);
|
|
});
|
|
|
|
test('rejects non-start, denied, unfenced and cross-Project audit records', () => {
|
|
for (const invalidAudit of [
|
|
audit({ operationId: 'tool.invoke.finish' }),
|
|
audit({ outcome: 'denied' }),
|
|
audit({ fence: null }),
|
|
audit({ projectId: 'project-other' }),
|
|
audit({ occurredAtMs: 999 }),
|
|
]) {
|
|
assert.throws(
|
|
() => createToolExecutionEvidenceBundle(input({ audit: invalidAudit })),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('rejects trace identity, parent loops, unknown fields and digest tampering', () => {
|
|
assert.throws(
|
|
() => createToolExecutionEvidenceBundle(input({ traceId: '1'.repeat(31) })),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createToolExecutionEvidenceBundle(
|
|
input({ parentSpanId: '2'.repeat(16) }),
|
|
),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
assert.throws(
|
|
() => createToolExecutionEvidenceBundle({ ...input(), secret: 'value' }),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
|
|
const bundle = createToolExecutionEvidenceBundle(input());
|
|
assert.throws(
|
|
() =>
|
|
normalizeToolExecutionTraceAnchor({
|
|
...copy(bundle.trace),
|
|
traceDigest: '0'.repeat(64),
|
|
}),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeToolExecutionAuditReceipt({
|
|
...copy(bundle.receipt),
|
|
receiptDigest: '0'.repeat(64),
|
|
}),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
});
|
|
|
|
test('rejects detached Trace, Audit and receipt relationships', () => {
|
|
const bundle = createToolExecutionEvidenceBundle(input());
|
|
const other = createToolExecutionEvidenceBundle(
|
|
input({
|
|
traceId: '3'.repeat(32),
|
|
spanId: '4'.repeat(16),
|
|
stepRunId: 'step-run-002',
|
|
audit: audit({
|
|
eventId: '40000000-0000-4000-8000-000000000002',
|
|
}),
|
|
}),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeToolExecutionEvidenceBundle({
|
|
...copy(bundle),
|
|
receipt: other.receipt,
|
|
}),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeToolExecutionEvidenceBundle({
|
|
...copy(bundle),
|
|
audit: other.audit,
|
|
}),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
});
|
|
|
|
test('normalizes bounded stable evidence pagination', () => {
|
|
const first = createToolExecutionEvidenceBundle(input());
|
|
const second = createToolExecutionEvidenceBundle(
|
|
input({
|
|
traceId: '3'.repeat(32),
|
|
spanId: '4'.repeat(16),
|
|
audit: audit({
|
|
eventId: '40000000-0000-4000-8000-000000000002',
|
|
occurredAtMs: 1_001,
|
|
}),
|
|
createdAtMs: 1_001,
|
|
}),
|
|
);
|
|
const query = normalizeListToolExecutionEvidenceQuery({
|
|
runId: 'run-001',
|
|
limit: 2,
|
|
});
|
|
assert.deepEqual(
|
|
normalizeListToolExecutionEvidenceResult(
|
|
{
|
|
bundles: [first, second],
|
|
truncated: true,
|
|
next: {
|
|
createdAtMs: second.trace.createdAtMs,
|
|
traceId: second.trace.traceId,
|
|
spanId: second.trace.spanId,
|
|
},
|
|
},
|
|
query,
|
|
).bundles,
|
|
[first, second],
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeListToolExecutionEvidenceQuery({
|
|
runId: 'run-001',
|
|
limit: MAX_TOOL_EXECUTION_EVIDENCE_PAGE_SIZE + 1,
|
|
}),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeListToolExecutionEvidenceResult(
|
|
{ bundles: [second, first], truncated: false },
|
|
query,
|
|
),
|
|
InvalidToolExecutionEvidenceError,
|
|
);
|
|
});
|
|
|
|
test('publishes the same pure contract through root and explicit subpath', () => {
|
|
const root = require('../dist');
|
|
const subpath = require('../dist/tool-execution/toolExecutionEvidence');
|
|
assert.equal(
|
|
root.createToolExecutionEvidenceBundle,
|
|
subpath.createToolExecutionEvidenceBundle,
|
|
);
|
|
const source = readFileSync(
|
|
join(__dirname, '../src/tool-execution/toolExecutionEvidence.ts'),
|
|
'utf8',
|
|
);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/node:(?:fs|child_process|net|http|https)|setInterval|setTimeout|execute|handler/i,
|
|
);
|
|
});
|