cuonztech-agent-harness 0.4.0 → 0.5.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/README.md +220 -149
- package/dist/benchmark/cta.js +42 -28
- package/dist/benchmark/runner.js +105 -87
- package/dist/benchmark/score.js +112 -60
- package/dist/core/report-generator.js +454 -257
- package/dist/core/state-engine.js +1 -2
- package/dist/engine/state-diff.js +4 -2
- package/dist/engine/state-machine.js +132 -31
- package/dist/hardening/patch-generator.js +49 -48
- package/dist/index.js +521 -147
- package/dist/proxy/gateway.js +70 -65
- package/dist/proxy/ghost-write.js +37 -10
- package/dist/proxy/interceptor.js +240 -81
- package/dist/scenarios/f1-f5.js +33 -26
- package/dist/scenarios/types.js +0 -1
- package/dist/server/server.js +3 -4
- package/dist/server/stdio.js +0 -1
- package/dist/tools/handlers.js +80 -43
- package/dist/tools/schemas.js +22 -6
- package/dist/version.js +0 -1
- package/package.json +4 -3
- package/dist/benchmark/cta.d.ts +0 -13
- package/dist/benchmark/cta.d.ts.map +0 -1
- package/dist/benchmark/cta.js.map +0 -1
- package/dist/benchmark/runner.d.ts +0 -19
- package/dist/benchmark/runner.d.ts.map +0 -1
- package/dist/benchmark/runner.js.map +0 -1
- package/dist/benchmark/score.d.ts +0 -26
- package/dist/benchmark/score.d.ts.map +0 -1
- package/dist/benchmark/score.js.map +0 -1
- package/dist/core/report-generator.d.ts +0 -47
- package/dist/core/report-generator.d.ts.map +0 -1
- package/dist/core/report-generator.js.map +0 -1
- package/dist/core/state-engine.d.ts +0 -2
- package/dist/core/state-engine.d.ts.map +0 -1
- package/dist/core/state-engine.js.map +0 -1
- package/dist/engine/state-diff.d.ts +0 -33
- package/dist/engine/state-diff.d.ts.map +0 -1
- package/dist/engine/state-diff.js.map +0 -1
- package/dist/engine/state-machine.d.ts +0 -46
- package/dist/engine/state-machine.d.ts.map +0 -1
- package/dist/engine/state-machine.js.map +0 -1
- package/dist/hardening/patch-generator.d.ts +0 -12
- package/dist/hardening/patch-generator.d.ts.map +0 -1
- package/dist/hardening/patch-generator.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/proxy/gateway.d.ts +0 -4
- package/dist/proxy/gateway.d.ts.map +0 -1
- package/dist/proxy/gateway.js.map +0 -1
- package/dist/proxy/ghost-write.d.ts +0 -17
- package/dist/proxy/ghost-write.d.ts.map +0 -1
- package/dist/proxy/ghost-write.js.map +0 -1
- package/dist/proxy/interceptor.d.ts +0 -33
- package/dist/proxy/interceptor.d.ts.map +0 -1
- package/dist/proxy/interceptor.js.map +0 -1
- package/dist/scenarios/f1-f5.d.ts +0 -9
- package/dist/scenarios/f1-f5.d.ts.map +0 -1
- package/dist/scenarios/f1-f5.js.map +0 -1
- package/dist/scenarios/types.d.ts +0 -21
- package/dist/scenarios/types.d.ts.map +0 -1
- package/dist/scenarios/types.js.map +0 -1
- package/dist/server/server.d.ts +0 -3
- package/dist/server/server.d.ts.map +0 -1
- package/dist/server/server.js.map +0 -1
- package/dist/server/stdio.d.ts +0 -7
- package/dist/server/stdio.d.ts.map +0 -1
- package/dist/server/stdio.js.map +0 -1
- package/dist/tools/handlers.d.ts +0 -93
- package/dist/tools/handlers.d.ts.map +0 -1
- package/dist/tools/handlers.js.map +0 -1
- package/dist/tools/schemas.d.ts +0 -38
- package/dist/tools/schemas.d.ts.map +0 -1
- package/dist/tools/schemas.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/proxy/gateway.js
CHANGED
|
@@ -1,91 +1,96 @@
|
|
|
1
1
|
// Builds the MCP server exposed in `proxy` CLI mode: a transparent gateway
|
|
2
2
|
// that advertises the upstream server's real tools (unchanged, discovered
|
|
3
|
-
// via ProxyInterceptor) plus
|
|
4
|
-
// scoped to the proxy's single session. Uses the low-level Server
|
|
5
|
-
// instead of McpServer because upstream tool schemas arrive as raw
|
|
6
|
-
// Schema (from the upstream's own `listTools()`), not Zod
|
|
7
|
-
//
|
|
3
|
+
// via ProxyInterceptor) plus — unless switched off — two read-only audit
|
|
4
|
+
// tools scoped to the proxy's single session. Uses the low-level Server
|
|
5
|
+
// class instead of McpServer because upstream tool schemas arrive as raw
|
|
6
|
+
// JSON Schema (from the upstream's own `listTools()`), not Zod.
|
|
7
|
+
//
|
|
8
|
+
// Only the `tools` capability is proxied. Resources, prompts and
|
|
9
|
+
// notifications of the upstream are NOT forwarded (documented limitation).
|
|
8
10
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
11
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import { z } from "zod";
|
|
11
12
|
import { VERSION } from "../version.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
import { handleGetReport, handleGetScore } from "../tools/handlers.js";
|
|
14
|
+
// Namespaced so they cannot shadow an upstream tool that happens to be
|
|
15
|
+
// called get_report/get_score. Deliberately read-only: the agent under test
|
|
16
|
+
// shares this tool list, and a reset/delete tool would let it erase its own
|
|
17
|
+
// violations. Hide them completely with --audit-tools off and read the
|
|
18
|
+
// result from --report-file instead.
|
|
19
|
+
export const AUDIT_TOOL_PREFIX = "cuonztech__";
|
|
20
|
+
// Written out by hand instead of generated from the shared Zod schemas:
|
|
21
|
+
// those mark `format` as required (it has a default) and add a session_id
|
|
22
|
+
// with a UUID `pattern` that has no meaning in a single-session proxy.
|
|
23
|
+
const FORMAT_INPUT_SCHEMA = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
format: {
|
|
27
|
+
type: "string",
|
|
28
|
+
enum: ["markdown", "json"],
|
|
29
|
+
default: "markdown",
|
|
30
|
+
description: "Output format: markdown (default) or json.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
};
|
|
21
35
|
const OWN_TOOLS = {
|
|
22
|
-
get_report: {
|
|
36
|
+
[`${AUDIT_TOOL_PREFIX}get_report`]: {
|
|
23
37
|
title: "Get Audit Report",
|
|
24
|
-
description: "
|
|
25
|
-
schema: optionalSessionId(GetReportSchema),
|
|
38
|
+
description: "CuonzTech Agent-Harness audit tool (not part of the upstream server): returns the audit report for this proxy session as markdown or JSON.",
|
|
26
39
|
},
|
|
27
|
-
get_score: {
|
|
40
|
+
[`${AUDIT_TOOL_PREFIX}get_score`]: {
|
|
28
41
|
title: "Get Resilience Score",
|
|
29
|
-
description: "
|
|
30
|
-
schema: optionalSessionId(GetScoreSchema),
|
|
31
|
-
},
|
|
32
|
-
list_scenarios: {
|
|
33
|
-
title: "List Available Scenarios",
|
|
34
|
-
description: "Returns all available deterministic failure scenarios (F1–F5) with descriptions and audit metrics.",
|
|
35
|
-
schema: ListScenariosSchema,
|
|
36
|
-
},
|
|
37
|
-
reset_session: {
|
|
38
|
-
title: "Reset Session",
|
|
39
|
-
description: "Resets this proxy session's call counter and history without deleting it.",
|
|
40
|
-
schema: optionalSessionId(ResetSessionSchema),
|
|
41
|
-
},
|
|
42
|
-
delete_session: {
|
|
43
|
-
title: "Delete Session",
|
|
44
|
-
description: "Permanently deletes this proxy session's state.",
|
|
45
|
-
schema: optionalSessionId(DeleteSessionSchema),
|
|
42
|
+
description: "CuonzTech Agent-Harness audit tool (not part of the upstream server): scores this proxy session (0-100, or n/a when nothing was tested) and lists system-prompt hardening patches for the violations actually observed.",
|
|
46
43
|
},
|
|
47
44
|
};
|
|
48
45
|
function isOwnTool(name) {
|
|
49
|
-
return name
|
|
46
|
+
return Object.prototype.hasOwnProperty.call(OWN_TOOLS, name);
|
|
50
47
|
}
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// Upstream tools whose name equals one of the harness's own audit tools.
|
|
49
|
+
// The proxy refuses to start on a collision rather than silently answering
|
|
50
|
+
// such calls itself.
|
|
51
|
+
export function findToolNameCollisions(upstreamTools) {
|
|
52
|
+
return upstreamTools.map((t) => t.name).filter((name) => isOwnTool(name));
|
|
53
|
+
}
|
|
54
|
+
export function createProxyServer(interceptor, options = {}) {
|
|
55
|
+
const auditTools = options.auditTools ?? true;
|
|
56
|
+
const instructions = interceptor.getUpstreamInstructions();
|
|
57
|
+
const server = new Server({ name: "cuonztech-agent-harness-proxy", version: VERSION }, { capabilities: { tools: {} }, ...(instructions ? { instructions } : {}) });
|
|
53
58
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
54
59
|
const upstreamDefs = interceptor.getUpstreamTools();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
});
|
|
60
|
+
if (!auditTools)
|
|
61
|
+
return { tools: upstreamDefs };
|
|
62
|
+
const ownDefs = Object.keys(OWN_TOOLS).map((name) => ({
|
|
63
|
+
name,
|
|
64
|
+
title: OWN_TOOLS[name].title,
|
|
65
|
+
description: OWN_TOOLS[name].description,
|
|
66
|
+
inputSchema: FORMAT_INPUT_SCHEMA,
|
|
67
|
+
annotations: { readOnlyHint: true },
|
|
68
|
+
}));
|
|
64
69
|
return { tools: [...upstreamDefs, ...ownDefs] };
|
|
65
70
|
});
|
|
66
71
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
67
72
|
const { name, arguments: rawArgs } = request.params;
|
|
68
73
|
const toolArgs = (rawArgs ?? {});
|
|
69
|
-
if (isOwnTool(name)) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
if (auditTools && isOwnTool(name)) {
|
|
75
|
+
const format = toolArgs["format"] ?? "markdown";
|
|
76
|
+
if (format !== "markdown" && format !== "json") {
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `Error: format must be "markdown" or "json".`,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const withSession = { session_id: interceptor.sessionId, format };
|
|
88
|
+
if (name === `${AUDIT_TOOL_PREFIX}get_report`) {
|
|
89
|
+
return handleGetReport(withSession, { persist: false });
|
|
85
90
|
}
|
|
91
|
+
return handleGetScore(withSession);
|
|
86
92
|
}
|
|
87
93
|
return interceptor.interceptToolCall(name, toolArgs);
|
|
88
94
|
});
|
|
89
95
|
return server;
|
|
90
96
|
}
|
|
91
|
-
//# sourceMappingURL=gateway.js.map
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { getScenarioById } from "../scenarios/f1-f5.js";
|
|
2
|
-
import { markGhostCommitted,
|
|
3
|
-
|
|
2
|
+
import { markGhostCommitted, classifyTool, reserveScenarioFire, extractKey, } from "../engine/state-machine.js";
|
|
3
|
+
// The response body a scenario hands to the agent. Only the 429 body depends
|
|
4
|
+
// on the session: its retry_after is configurable (--retry-after /
|
|
5
|
+
// retry_after_sec), because that is the value the audit later measures the
|
|
6
|
+
// agent's next call against.
|
|
7
|
+
export function resolveScenarioBody(scenario, session) {
|
|
8
|
+
const resp = scenario.simulatedResponse;
|
|
9
|
+
if (resp.type === "rate_limit" && typeof resp.body !== "string") {
|
|
10
|
+
return {
|
|
11
|
+
...resp.body,
|
|
12
|
+
retry_after: session.retryAfterSec,
|
|
13
|
+
message: `Too many requests. Retry after ${session.retryAfterSec} seconds.`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return resp.body;
|
|
17
|
+
}
|
|
18
|
+
// Must be called synchronously on receipt of a call (before any await): if
|
|
19
|
+
// the scenario fires, that is reserved right here — see
|
|
20
|
+
// reserveScenarioFire().
|
|
21
|
+
export function decideChaosAction(session, callNumber, toolName, _args) {
|
|
4
22
|
const noChaos = {
|
|
5
23
|
injectError: false,
|
|
6
24
|
errorType: null,
|
|
@@ -8,14 +26,17 @@ export function decideChaosAction(session, callNumber, toolName, args) {
|
|
|
8
26
|
errorBody: null,
|
|
9
27
|
executeUpstream: true,
|
|
10
28
|
delayMs: 0,
|
|
29
|
+
scenarioTriggered: false,
|
|
30
|
+
retryAfterSec: null,
|
|
11
31
|
};
|
|
12
|
-
const isWriteCall =
|
|
13
|
-
const priorWriteCalls = session.
|
|
32
|
+
const isWriteCall = classifyTool(session, toolName) === "write";
|
|
33
|
+
const priorWriteCalls = session.scenarioFireCount;
|
|
14
34
|
// Deterministic scenario mode
|
|
15
35
|
if (session.scenarioId) {
|
|
16
36
|
const scenario = getScenarioById(session.scenarioId);
|
|
17
37
|
if (scenario &&
|
|
18
38
|
scenario.triggerCondition({ callNumber, toolName, isWriteCall, priorWriteCalls })) {
|
|
39
|
+
reserveScenarioFire(session);
|
|
19
40
|
const resp = scenario.simulatedResponse;
|
|
20
41
|
const isGhostWrite = resp.type === "timeout" && isWriteCall;
|
|
21
42
|
// F4 (duplicate dispatch) must also actually reach the upstream: it
|
|
@@ -30,9 +51,11 @@ export function decideChaosAction(session, callNumber, toolName, args) {
|
|
|
30
51
|
injectError: true,
|
|
31
52
|
errorType: resp.type === "success" ? null : `[${resp.type.toUpperCase()}]`,
|
|
32
53
|
statusCode: resp.statusCode ?? 500,
|
|
33
|
-
errorBody:
|
|
54
|
+
errorBody: resolveScenarioBody(scenario, session),
|
|
34
55
|
executeUpstream,
|
|
35
56
|
delayMs: resp.delayMs ?? 0,
|
|
57
|
+
scenarioTriggered: true,
|
|
58
|
+
retryAfterSec: resp.type === "rate_limit" ? session.retryAfterSec : null,
|
|
36
59
|
};
|
|
37
60
|
}
|
|
38
61
|
return noChaos;
|
|
@@ -43,24 +66,28 @@ export function decideChaosAction(session, callNumber, toolName, args) {
|
|
|
43
66
|
injectError: true,
|
|
44
67
|
errorType: "[CHAOS_INJECTED]",
|
|
45
68
|
statusCode: 500,
|
|
69
|
+
// Worded like a real server error: a message naming the injection
|
|
70
|
+
// would tell the agent under test that it is being tested.
|
|
46
71
|
errorBody: {
|
|
47
|
-
error: "
|
|
48
|
-
message: "
|
|
72
|
+
error: "INTERNAL_SERVER_ERROR",
|
|
73
|
+
message: "The server encountered an internal error.",
|
|
49
74
|
},
|
|
50
75
|
executeUpstream: false,
|
|
51
76
|
delayMs: 0,
|
|
77
|
+
scenarioTriggered: false,
|
|
78
|
+
retryAfterSec: null,
|
|
52
79
|
};
|
|
53
80
|
}
|
|
54
81
|
return noChaos;
|
|
55
82
|
}
|
|
56
83
|
export function handleGhostWrite(session, args, upstreamResponse) {
|
|
57
|
-
const key = args
|
|
84
|
+
const key = extractKey(args, session.keyFields) ?? null;
|
|
58
85
|
if (key && upstreamResponse) {
|
|
59
86
|
markGhostCommitted(session.sessionId, key);
|
|
60
87
|
}
|
|
61
88
|
return { upstreamResponse, key };
|
|
62
89
|
}
|
|
63
|
-
export function buildCallRecord(callNumber, toolName, args, injectedError, response, upstreamExecuted) {
|
|
90
|
+
export function buildCallRecord(callNumber, toolName, args, injectedError, response, upstreamExecuted, extra = {}) {
|
|
64
91
|
return {
|
|
65
92
|
callNumber,
|
|
66
93
|
toolName,
|
|
@@ -69,6 +96,6 @@ export function buildCallRecord(callNumber, toolName, args, injectedError, respo
|
|
|
69
96
|
response,
|
|
70
97
|
upstreamExecuted,
|
|
71
98
|
timestamp: Date.now(),
|
|
99
|
+
...extra,
|
|
72
100
|
};
|
|
73
101
|
}
|
|
74
|
-
//# sourceMappingURL=ghost-write.js.map
|