@mcpose/audit 2.0.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +440 -28
- package/dist/chain.d.ts +49 -6
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js +227 -28
- package/dist/chain.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +3 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +499 -85
- package/dist/middleware.js.map +1 -1
- package/dist/sensitivity.d.ts +11 -2
- package/dist/sensitivity.d.ts.map +1 -1
- package/dist/sensitivity.js +18 -2
- package/dist/sensitivity.js.map +1 -1
- package/dist/signingKey.d.ts +8 -0
- package/dist/signingKey.d.ts.map +1 -1
- package/dist/signingKey.js +21 -4
- package/dist/signingKey.js.map +1 -1
- package/dist/subjectKeyStore.d.ts +17 -0
- package/dist/subjectKeyStore.d.ts.map +1 -0
- package/dist/subjectKeyStore.js +37 -0
- package/dist/subjectKeyStore.js.map +1 -0
- package/dist/types.d.ts +170 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +24 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +66 -0
- package/dist/verify.js.map +1 -0
- package/package.json +43 -15
- package/src/__tests__/chain.batch.test.ts +30 -0
- package/src/__tests__/chain.test.ts +433 -0
- package/src/__tests__/erasable.test.ts +499 -0
- package/src/__tests__/merkle-batch.middleware.test.ts +96 -0
- package/src/__tests__/middleware.http.test.ts +172 -0
- package/src/__tests__/middleware.rejection.test.ts +183 -0
- package/src/__tests__/middleware.robustness.test.ts +809 -0
- package/src/__tests__/middleware.test.ts +1121 -0
- package/src/__tests__/middleware.unusableResult.test.ts +141 -0
- package/src/__tests__/properties.test.ts +764 -0
- package/src/__tests__/sensitivity.test.ts +72 -0
- package/src/__tests__/signingKey.test.ts +76 -0
- package/src/chain.ts +317 -0
- package/src/index.ts +31 -0
- package/src/middleware.ts +781 -0
- package/src/sensitivity.ts +33 -0
- package/src/signingKey.ts +35 -0
- package/src/subjectKeyStore.ts +38 -0
- package/src/types.ts +287 -0
- package/src/verify.ts +92 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createAuditMiddleware } from '../middleware.js';
|
|
3
|
+
import { createDefaultSigningKeyProvider } from '../signingKey.js';
|
|
4
|
+
import { createSensitivityResolver } from '../sensitivity.js';
|
|
5
|
+
import { verifyAuditChain, verifyManifestSignature } from '../verify.js';
|
|
6
|
+
import type { AuditEvent, AuditOptions } from '../types.js';
|
|
7
|
+
import { createProxyContext } from 'mcpose';
|
|
8
|
+
import type { Identity } from 'mcpose';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A call that neither throws nor produces a tool result is a failed call, and
|
|
12
|
+
* the trail has to say so.
|
|
13
|
+
*
|
|
14
|
+
* `hasToolContent` narrows the pipeline's result and reads `.content` on the
|
|
15
|
+
* way, so a nullish one used to throw inside the post-call section — whose
|
|
16
|
+
* only handler reports and moves on. `buildEvent` never ran, so a governed
|
|
17
|
+
* call that failed reached the client and never reached the trail, while the
|
|
18
|
+
* manifest that counted it still verified: a valid chain under a valid
|
|
19
|
+
* signature, short one event of the calls it claims to cover.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const identity: Identity = {
|
|
23
|
+
sub: 'user-1',
|
|
24
|
+
type: 'human',
|
|
25
|
+
roles: ['analyst'],
|
|
26
|
+
claims: {},
|
|
27
|
+
resolvedAt: '2026-06-01T00:00:00.000Z',
|
|
28
|
+
source: 'jwt',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const signingKey = createDefaultSigningKeyProvider('test-secret');
|
|
32
|
+
|
|
33
|
+
function makeCtx(sessionId: string) {
|
|
34
|
+
return createProxyContext({
|
|
35
|
+
transport: 'http',
|
|
36
|
+
identity,
|
|
37
|
+
sessionId,
|
|
38
|
+
proxy: { name: 'test-proxy', version: '0.0.0' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeReq(tool: string) {
|
|
43
|
+
return {
|
|
44
|
+
method: 'tools/call' as const,
|
|
45
|
+
params: { name: tool, arguments: {} },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeOptions(events: AuditEvent[]): AuditOptions {
|
|
50
|
+
return {
|
|
51
|
+
signingKey,
|
|
52
|
+
sensitivityResolver: createSensitivityResolver({ search: 'low' }),
|
|
53
|
+
onEvent: (e) => void events.push(e),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('a pipeline that resolves without a tool result', () => {
|
|
58
|
+
for (const [label, value] of [
|
|
59
|
+
['undefined', undefined],
|
|
60
|
+
['null', null],
|
|
61
|
+
['a string', 'all good'],
|
|
62
|
+
['the number 0', 0],
|
|
63
|
+
] as const) {
|
|
64
|
+
it(`records the call as an error when the pipeline resolves to ${label}`, async () => {
|
|
65
|
+
const events: AuditEvent[] = [];
|
|
66
|
+
const handle = createAuditMiddleware(makeOptions(events));
|
|
67
|
+
|
|
68
|
+
await handle.middleware(
|
|
69
|
+
makeReq('search'),
|
|
70
|
+
async () => value as never,
|
|
71
|
+
makeCtx(`sess-${label}`),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(events).toHaveLength(1);
|
|
75
|
+
const event = events[0]!;
|
|
76
|
+
expect(event.outcome).toBe('error');
|
|
77
|
+
expect(event.error).toEqual({
|
|
78
|
+
name: 'InvalidToolResult',
|
|
79
|
+
message: 'Tool call resolved to a value that is not a CallToolResult',
|
|
80
|
+
});
|
|
81
|
+
expect(event.replayManifestPosition).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
it('names the shape a prompt fetch was expected to return', async () => {
|
|
86
|
+
const events: AuditEvent[] = [];
|
|
87
|
+
const { promptMiddleware } = createAuditMiddleware(makeOptions(events));
|
|
88
|
+
|
|
89
|
+
await promptMiddleware(
|
|
90
|
+
{ method: 'prompts/get' as const, params: { name: 'greet' } },
|
|
91
|
+
async () => undefined as never,
|
|
92
|
+
makeCtx('sess-prompt'),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(events).toHaveLength(1);
|
|
96
|
+
const event = events[0]!;
|
|
97
|
+
expect(event.kind).toBe('prompt');
|
|
98
|
+
expect(event.outcome).toBe('error');
|
|
99
|
+
expect(event.error).toEqual({
|
|
100
|
+
name: 'InvalidToolResult',
|
|
101
|
+
message: 'Prompt fetch resolved to a value that is not a GetPromptResult',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps the chain and the manifest honest across a dropped result', async () => {
|
|
106
|
+
const events: AuditEvent[] = [];
|
|
107
|
+
const onAuditError = vi.fn();
|
|
108
|
+
const handle = createAuditMiddleware({
|
|
109
|
+
...makeOptions(events),
|
|
110
|
+
onAuditError,
|
|
111
|
+
});
|
|
112
|
+
const ctx = makeCtx('sess-mixed');
|
|
113
|
+
|
|
114
|
+
// First call fails the way a local tool handler with a missing `return`
|
|
115
|
+
// does; the second is an ordinary success.
|
|
116
|
+
await handle.middleware(
|
|
117
|
+
makeReq('search'),
|
|
118
|
+
async () => undefined as never,
|
|
119
|
+
ctx,
|
|
120
|
+
);
|
|
121
|
+
await handle.middleware(
|
|
122
|
+
makeReq('search'),
|
|
123
|
+
async () => ({ content: [{ type: 'text', text: 'ok' }] }),
|
|
124
|
+
ctx,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const manifest = await handle.closeSession('sess-mixed');
|
|
128
|
+
|
|
129
|
+
// Two governed calls, two records, in order.
|
|
130
|
+
expect(events).toHaveLength(2);
|
|
131
|
+
expect(events.map((e) => e.replayManifestPosition)).toEqual([0, 1]);
|
|
132
|
+
expect(events.map((e) => e.outcome)).toEqual(['error', 'success']);
|
|
133
|
+
// Nothing needed the containment report any more: the unusable result is
|
|
134
|
+
// in the trail, not in the operator's log.
|
|
135
|
+
expect(onAuditError).not.toHaveBeenCalled();
|
|
136
|
+
|
|
137
|
+
expect(await verifyAuditChain(events, signingKey)).toEqual({ valid: true });
|
|
138
|
+
expect(manifest?.eventCount).toBe(2);
|
|
139
|
+
expect(await verifyManifestSignature(manifest!, signingKey)).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|