cuonztech-agent-harness 0.4.0 → 0.5.1

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.
Files changed (78) hide show
  1. package/README.md +260 -151
  2. package/dist/benchmark/cta.js +45 -31
  3. package/dist/benchmark/runner.js +105 -87
  4. package/dist/benchmark/score.js +115 -60
  5. package/dist/core/report-generator.js +704 -258
  6. package/dist/core/state-engine.js +1 -2
  7. package/dist/engine/state-diff.js +4 -2
  8. package/dist/engine/state-machine.js +164 -35
  9. package/dist/hardening/patch-generator.js +58 -48
  10. package/dist/index.js +737 -148
  11. package/dist/proxy/gateway.js +71 -65
  12. package/dist/proxy/ghost-write.js +37 -10
  13. package/dist/proxy/interceptor.js +367 -84
  14. package/dist/scenarios/f1-f5.js +34 -27
  15. package/dist/scenarios/types.js +0 -1
  16. package/dist/server/server.js +70 -18
  17. package/dist/server/stdio.js +0 -1
  18. package/dist/tools/handlers.js +80 -43
  19. package/dist/tools/schemas.js +27 -11
  20. package/dist/version.js +0 -1
  21. package/package.json +4 -3
  22. package/dist/benchmark/cta.d.ts +0 -13
  23. package/dist/benchmark/cta.d.ts.map +0 -1
  24. package/dist/benchmark/cta.js.map +0 -1
  25. package/dist/benchmark/runner.d.ts +0 -19
  26. package/dist/benchmark/runner.d.ts.map +0 -1
  27. package/dist/benchmark/runner.js.map +0 -1
  28. package/dist/benchmark/score.d.ts +0 -26
  29. package/dist/benchmark/score.d.ts.map +0 -1
  30. package/dist/benchmark/score.js.map +0 -1
  31. package/dist/core/report-generator.d.ts +0 -47
  32. package/dist/core/report-generator.d.ts.map +0 -1
  33. package/dist/core/report-generator.js.map +0 -1
  34. package/dist/core/state-engine.d.ts +0 -2
  35. package/dist/core/state-engine.d.ts.map +0 -1
  36. package/dist/core/state-engine.js.map +0 -1
  37. package/dist/engine/state-diff.d.ts +0 -33
  38. package/dist/engine/state-diff.d.ts.map +0 -1
  39. package/dist/engine/state-diff.js.map +0 -1
  40. package/dist/engine/state-machine.d.ts +0 -46
  41. package/dist/engine/state-machine.d.ts.map +0 -1
  42. package/dist/engine/state-machine.js.map +0 -1
  43. package/dist/hardening/patch-generator.d.ts +0 -12
  44. package/dist/hardening/patch-generator.d.ts.map +0 -1
  45. package/dist/hardening/patch-generator.js.map +0 -1
  46. package/dist/index.d.ts +0 -3
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/index.js.map +0 -1
  49. package/dist/proxy/gateway.d.ts +0 -4
  50. package/dist/proxy/gateway.d.ts.map +0 -1
  51. package/dist/proxy/gateway.js.map +0 -1
  52. package/dist/proxy/ghost-write.d.ts +0 -17
  53. package/dist/proxy/ghost-write.d.ts.map +0 -1
  54. package/dist/proxy/ghost-write.js.map +0 -1
  55. package/dist/proxy/interceptor.d.ts +0 -33
  56. package/dist/proxy/interceptor.d.ts.map +0 -1
  57. package/dist/proxy/interceptor.js.map +0 -1
  58. package/dist/scenarios/f1-f5.d.ts +0 -9
  59. package/dist/scenarios/f1-f5.d.ts.map +0 -1
  60. package/dist/scenarios/f1-f5.js.map +0 -1
  61. package/dist/scenarios/types.d.ts +0 -21
  62. package/dist/scenarios/types.d.ts.map +0 -1
  63. package/dist/scenarios/types.js.map +0 -1
  64. package/dist/server/server.d.ts +0 -3
  65. package/dist/server/server.d.ts.map +0 -1
  66. package/dist/server/server.js.map +0 -1
  67. package/dist/server/stdio.d.ts +0 -7
  68. package/dist/server/stdio.d.ts.map +0 -1
  69. package/dist/server/stdio.js.map +0 -1
  70. package/dist/tools/handlers.d.ts +0 -93
  71. package/dist/tools/handlers.d.ts.map +0 -1
  72. package/dist/tools/handlers.js.map +0 -1
  73. package/dist/tools/schemas.d.ts +0 -38
  74. package/dist/tools/schemas.d.ts.map +0 -1
  75. package/dist/tools/schemas.js.map +0 -1
  76. package/dist/version.d.ts +0 -2
  77. package/dist/version.d.ts.map +0 -1
  78. package/dist/version.js.map +0 -1
@@ -1,91 +1,97 @@
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 a handful of the harness's own audit tools
4
- // scoped to the proxy's single session. Uses the low-level Server class
5
- // instead of McpServer because upstream tool schemas arrive as raw JSON
6
- // Schema (from the upstream's own `listTools()`), not Zod — McpServer's
7
- // `registerTool` only accepts a Zod raw shape.
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 { GetReportSchema, GetScoreSchema, ListScenariosSchema, ResetSessionSchema, DeleteSessionSchema, } from "../tools/schemas.js";
13
- import { handleGetReport, handleGetScore, handleListScenarios, handleResetSession, handleDeleteSession, } from "../tools/handlers.js";
14
- // session_id is required in the shared schemas (used by the multi-session
15
- // stdio server), but a proxy only ever has one session — its own. This
16
- // override makes it optional so the proxy's CallTool handler can default it
17
- // to `interceptor.sessionId` without touching the shared schemas.
18
- function optionalSessionId(schema) {
19
- return { ...schema, session_id: schema.session_id.optional() };
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: "Generates the audit report for this proxy session. Returns markdown (chat-friendly) or JSON.",
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: "Scores this proxy session's own audit report (0-100) and generates system-prompt hardening patches for whatever violations the connected agent actually produced.",
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 in OWN_TOOLS;
46
+ return Object.prototype.hasOwnProperty.call(OWN_TOOLS, name);
50
47
  }
51
- export function createProxyServer(interceptor) {
52
- const server = new Server({ name: "cuonztech-agent-harness-proxy", version: VERSION }, { capabilities: { tools: {} } });
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
- const ownDefs = Object.keys(OWN_TOOLS).map((name) => {
56
- const tool = OWN_TOOLS[name];
57
- return {
58
- name,
59
- title: tool.title,
60
- description: tool.description,
61
- inputSchema: z.toJSONSchema(z.object(tool.schema)),
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 withSession = {
71
- session_id: interceptor.sessionId,
72
- ...toolArgs,
73
- };
74
- switch (name) {
75
- case "get_report":
76
- return handleGetReport(withSession);
77
- case "get_score":
78
- return handleGetScore(withSession);
79
- case "list_scenarios":
80
- return handleListScenarios();
81
- case "reset_session":
82
- return handleResetSession(withSession);
83
- case "delete_session":
84
- return handleDeleteSession(withSession);
74
+ if (auditTools && isOwnTool(name)) {
75
+ const rawFormat = toolArgs["format"] ?? "markdown";
76
+ const format = typeof rawFormat === "string" ? rawFormat.toLowerCase() : rawFormat;
77
+ if (format !== "markdown" && format !== "json") {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: `Error: format must be "markdown" or "json".`,
83
+ },
84
+ ],
85
+ isError: true,
86
+ };
87
+ }
88
+ const withSession = { session_id: interceptor.sessionId, format };
89
+ if (name === `${AUDIT_TOOL_PREFIX}get_report`) {
90
+ return handleGetReport(withSession, { persist: false });
85
91
  }
92
+ return handleGetScore(withSession);
86
93
  }
87
94
  return interceptor.interceptToolCall(name, toolArgs);
88
95
  });
89
96
  return server;
90
97
  }
91
- //# sourceMappingURL=gateway.js.map
@@ -1,6 +1,24 @@
1
1
  import { getScenarioById } from "../scenarios/f1-f5.js";
2
- import { markGhostCommitted, isWriteTool } from "../engine/state-machine.js";
3
- export function decideChaosAction(session, callNumber, toolName, args) {
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 = isWriteTool(toolName, session.writeToolPatterns);
13
- const priorWriteCalls = session.history.filter((r) => isWriteTool(r.toolName, session.writeToolPatterns)).length;
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: resp.body,
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: "CHAOS_ERROR",
48
- message: "Stochastically injected failure.",
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["idempotency_key"] ?? null;
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