nexus-agents 2.95.0 → 2.97.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 +4 -4
- package/dist/{child-mcp-config-NW7JMSDK.js → child-mcp-config-WVPJIK2J.js} +2 -2
- package/dist/chunk-5LGAPYX7.js +2133 -0
- package/dist/chunk-5LGAPYX7.js.map +1 -0
- package/dist/{chunk-LXROEQAO.js → chunk-65LE457J.js} +2 -2
- package/dist/{chunk-47Z3YPOK.js → chunk-674NONPG.js} +2 -2
- package/dist/{chunk-2X24T4ZD.js → chunk-6IN4MKDA.js} +7 -7
- package/dist/{chunk-FYGZ4MEQ.js → chunk-76FTBRUS.js} +415 -1410
- package/dist/chunk-76FTBRUS.js.map +1 -0
- package/dist/{chunk-YYXJ6RTO.js → chunk-7BLBXITB.js} +3 -3
- package/dist/{chunk-GC5FH2IA.js → chunk-APLZRTYC.js} +7 -7
- package/dist/{chunk-TEKWQNQ2.js → chunk-D3OT7TOA.js} +3 -3
- package/dist/{chunk-3HXWI34V.js → chunk-E43RUPLP.js} +3 -3
- package/dist/{chunk-LZOTH2GI.js → chunk-EY7LL4F4.js} +2 -2
- package/dist/{chunk-5HBDXW44.js → chunk-FDTIRCSL.js} +2 -2
- package/dist/{chunk-VTMQND6B.js → chunk-GAIUXYF4.js} +366 -2470
- package/dist/chunk-GAIUXYF4.js.map +1 -0
- package/dist/{chunk-D3JSXVX3.js → chunk-GERVGPTF.js} +2 -2
- package/dist/{chunk-6IWPN5UB.js → chunk-HIWO2WWP.js} +2 -2
- package/dist/{chunk-WGA4VGIR.js → chunk-HWCMNFIS.js} +3 -3
- package/dist/{chunk-G42N55ZR.js → chunk-HZGHPBTV.js} +2 -2
- package/dist/{chunk-KHZGMF2W.js → chunk-IHKPGTF3.js} +4 -4
- package/dist/{chunk-ECBH5INZ.js → chunk-JTQLV374.js} +71 -6
- package/dist/chunk-JTQLV374.js.map +1 -0
- package/dist/{chunk-PFOY5ML6.js → chunk-JX6VL7JF.js} +3 -3
- package/dist/{chunk-4NVYN2MB.js → chunk-KKVH3PDK.js} +4 -4
- package/dist/{chunk-3H6Q3RZM.js → chunk-ML4IF3PS.js} +4 -4
- package/dist/{chunk-QUPPV6UO.js → chunk-N5656LE3.js} +2 -2
- package/dist/{chunk-FXVJLJUF.js → chunk-NTBVUWU4.js} +2 -2
- package/dist/{chunk-ZICVVBDW.js → chunk-OKRTOMKX.js} +2 -2
- package/dist/{chunk-Y3OOHHKQ.js → chunk-OSHCOBDK.js} +2 -2
- package/dist/chunk-P2BKWVKF.js +1095 -0
- package/dist/chunk-P2BKWVKF.js.map +1 -0
- package/dist/{chunk-FTDVA33Z.js → chunk-SAUBPHQ7.js} +4 -4
- package/dist/{chunk-55G7KQ7F.js → chunk-SWNOQW7Y.js} +5 -5
- package/dist/{chunk-O4LVC4MQ.js → chunk-SZNNT2DH.js} +2 -2
- package/dist/{chunk-ECZW5FXN.js → chunk-T5TYWM5S.js} +2 -2
- package/dist/{chunk-7GUP6KQQ.js → chunk-UNOOL6EN.js} +2 -2
- package/dist/{chunk-BSHDI4BH.js → chunk-WXDWDGVL.js} +2 -2
- package/dist/{cli-circuit-breaker-LNU5DQSB.js → cli-circuit-breaker-STKW4DTL.js} +4 -4
- package/dist/cli.js +149 -52
- package/dist/cli.js.map +1 -1
- package/dist/{composite-router-DJ54ND5X.js → composite-router-POFAM7T4.js} +2 -2
- package/dist/{consensus-vote-XLEQFVE7.js → consensus-vote-EQAYX53Z.js} +13 -12
- package/dist/{context-retriever-6O6TREXH.js → context-retriever-H22L3WFV.js} +6 -6
- package/dist/{doctor-deep-PSNJPELP.js → doctor-deep-HGAC6N5Y.js} +3 -3
- package/dist/{expert-bridge-Q3OWPD4L.js → expert-bridge-IAQPOJAQ.js} +4 -4
- package/dist/{factory-PSMTMC7V.js → factory-P2SQS7UM.js} +5 -5
- package/dist/{factory-43RVPRT5.js → factory-ZIZYUDKH.js} +5 -5
- package/dist/improvement-review-G2HAMGZP.js +27 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +52 -49
- package/dist/index.js.map +1 -1
- package/dist/{init-opencode-25AUARJH.js → init-opencode-RHCYZDZW.js} +5 -5
- package/dist/{issue-triage-KZTENUDH.js → issue-triage-VVNQIQW5.js} +5 -5
- package/dist/{pr-reviewer-helpers-XENJ4E2T.js → pr-reviewer-helpers-UN7RDPDN.js} +4 -4
- package/dist/{registry-command-X3O34FAK.js → registry-command-OVRRKYHO.js} +2 -2
- package/dist/{repo-security-plan-XCX7EFJR.js → repo-security-plan-X74TBYOV.js} +3 -3
- package/dist/{research-helpers-synthesize-CGVR6M4S.js → research-helpers-synthesize-OSP6FGNV.js} +3 -3
- package/dist/{routing-memory-YZRYGXQC.js → routing-memory-5T5QJF47.js} +2 -2
- package/dist/{session-memory-SXQAA2EN.js → session-memory-X7CJ6X4A.js} +3 -3
- package/dist/{setup-command-VG7JZI6O.js → setup-command-ZWJCGPJ5.js} +11 -11
- package/dist/{setup-config-VJAK6AA2.js → setup-config-YVIKUNOB.js} +3 -3
- package/dist/{setup-custom-api-KSF2JMTS.js → setup-custom-api-FLV4SELI.js} +4 -4
- package/dist/{tool-memory-L7SDFSF5.js → tool-memory-QNEAKRKV.js} +5 -5
- package/dist/{weather-report-D6HFOK7S.js → weather-report-SUVIXWKI.js} +2 -2
- package/dist/weather-report-SUVIXWKI.js.map +1 -0
- package/package.json +12 -2
- package/dist/chunk-ECBH5INZ.js.map +0 -1
- package/dist/chunk-FYGZ4MEQ.js.map +0 -1
- package/dist/chunk-VTMQND6B.js.map +0 -1
- /package/dist/{child-mcp-config-NW7JMSDK.js.map → child-mcp-config-WVPJIK2J.js.map} +0 -0
- /package/dist/{chunk-LXROEQAO.js.map → chunk-65LE457J.js.map} +0 -0
- /package/dist/{chunk-47Z3YPOK.js.map → chunk-674NONPG.js.map} +0 -0
- /package/dist/{chunk-2X24T4ZD.js.map → chunk-6IN4MKDA.js.map} +0 -0
- /package/dist/{chunk-YYXJ6RTO.js.map → chunk-7BLBXITB.js.map} +0 -0
- /package/dist/{chunk-GC5FH2IA.js.map → chunk-APLZRTYC.js.map} +0 -0
- /package/dist/{chunk-TEKWQNQ2.js.map → chunk-D3OT7TOA.js.map} +0 -0
- /package/dist/{chunk-3HXWI34V.js.map → chunk-E43RUPLP.js.map} +0 -0
- /package/dist/{chunk-LZOTH2GI.js.map → chunk-EY7LL4F4.js.map} +0 -0
- /package/dist/{chunk-5HBDXW44.js.map → chunk-FDTIRCSL.js.map} +0 -0
- /package/dist/{chunk-D3JSXVX3.js.map → chunk-GERVGPTF.js.map} +0 -0
- /package/dist/{chunk-6IWPN5UB.js.map → chunk-HIWO2WWP.js.map} +0 -0
- /package/dist/{chunk-WGA4VGIR.js.map → chunk-HWCMNFIS.js.map} +0 -0
- /package/dist/{chunk-G42N55ZR.js.map → chunk-HZGHPBTV.js.map} +0 -0
- /package/dist/{chunk-KHZGMF2W.js.map → chunk-IHKPGTF3.js.map} +0 -0
- /package/dist/{chunk-PFOY5ML6.js.map → chunk-JX6VL7JF.js.map} +0 -0
- /package/dist/{chunk-4NVYN2MB.js.map → chunk-KKVH3PDK.js.map} +0 -0
- /package/dist/{chunk-3H6Q3RZM.js.map → chunk-ML4IF3PS.js.map} +0 -0
- /package/dist/{chunk-QUPPV6UO.js.map → chunk-N5656LE3.js.map} +0 -0
- /package/dist/{chunk-FXVJLJUF.js.map → chunk-NTBVUWU4.js.map} +0 -0
- /package/dist/{chunk-ZICVVBDW.js.map → chunk-OKRTOMKX.js.map} +0 -0
- /package/dist/{chunk-Y3OOHHKQ.js.map → chunk-OSHCOBDK.js.map} +0 -0
- /package/dist/{chunk-FTDVA33Z.js.map → chunk-SAUBPHQ7.js.map} +0 -0
- /package/dist/{chunk-55G7KQ7F.js.map → chunk-SWNOQW7Y.js.map} +0 -0
- /package/dist/{chunk-O4LVC4MQ.js.map → chunk-SZNNT2DH.js.map} +0 -0
- /package/dist/{chunk-ECZW5FXN.js.map → chunk-T5TYWM5S.js.map} +0 -0
- /package/dist/{chunk-7GUP6KQQ.js.map → chunk-UNOOL6EN.js.map} +0 -0
- /package/dist/{chunk-BSHDI4BH.js.map → chunk-WXDWDGVL.js.map} +0 -0
- /package/dist/{cli-circuit-breaker-LNU5DQSB.js.map → cli-circuit-breaker-STKW4DTL.js.map} +0 -0
- /package/dist/{composite-router-DJ54ND5X.js.map → composite-router-POFAM7T4.js.map} +0 -0
- /package/dist/{consensus-vote-XLEQFVE7.js.map → consensus-vote-EQAYX53Z.js.map} +0 -0
- /package/dist/{context-retriever-6O6TREXH.js.map → context-retriever-H22L3WFV.js.map} +0 -0
- /package/dist/{doctor-deep-PSNJPELP.js.map → doctor-deep-HGAC6N5Y.js.map} +0 -0
- /package/dist/{expert-bridge-Q3OWPD4L.js.map → expert-bridge-IAQPOJAQ.js.map} +0 -0
- /package/dist/{factory-43RVPRT5.js.map → factory-P2SQS7UM.js.map} +0 -0
- /package/dist/{factory-PSMTMC7V.js.map → factory-ZIZYUDKH.js.map} +0 -0
- /package/dist/{issue-triage-KZTENUDH.js.map → improvement-review-G2HAMGZP.js.map} +0 -0
- /package/dist/{init-opencode-25AUARJH.js.map → init-opencode-RHCYZDZW.js.map} +0 -0
- /package/dist/{pr-reviewer-helpers-XENJ4E2T.js.map → issue-triage-VVNQIQW5.js.map} +0 -0
- /package/dist/{repo-security-plan-XCX7EFJR.js.map → pr-reviewer-helpers-UN7RDPDN.js.map} +0 -0
- /package/dist/{registry-command-X3O34FAK.js.map → registry-command-OVRRKYHO.js.map} +0 -0
- /package/dist/{research-helpers-synthesize-CGVR6M4S.js.map → repo-security-plan-X74TBYOV.js.map} +0 -0
- /package/dist/{routing-memory-YZRYGXQC.js.map → research-helpers-synthesize-OSP6FGNV.js.map} +0 -0
- /package/dist/{session-memory-SXQAA2EN.js.map → routing-memory-5T5QJF47.js.map} +0 -0
- /package/dist/{setup-command-VG7JZI6O.js.map → session-memory-X7CJ6X4A.js.map} +0 -0
- /package/dist/{setup-config-VJAK6AA2.js.map → setup-command-ZWJCGPJ5.js.map} +0 -0
- /package/dist/{tool-memory-L7SDFSF5.js.map → setup-config-YVIKUNOB.js.map} +0 -0
- /package/dist/{setup-custom-api-KSF2JMTS.js.map → setup-custom-api-FLV4SELI.js.map} +0 -0
- /package/dist/{weather-report-D6HFOK7S.js.map → tool-memory-QNEAKRKV.js.map} +0 -0
|
@@ -0,0 +1,2133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CACHE_TIMEOUTS,
|
|
3
|
+
MCP_TIMEOUTS,
|
|
4
|
+
SecurityError,
|
|
5
|
+
TIMEOUT_GUARD,
|
|
6
|
+
ValidationError,
|
|
7
|
+
createLogger,
|
|
8
|
+
createMetricsMiddleware,
|
|
9
|
+
err,
|
|
10
|
+
formatZodError,
|
|
11
|
+
getErrorMessage,
|
|
12
|
+
getTimeProvider,
|
|
13
|
+
ok
|
|
14
|
+
} from "./chunk-JTQLV374.js";
|
|
15
|
+
import {
|
|
16
|
+
getNexusDataDir
|
|
17
|
+
} from "./chunk-CH7QIDHQ.js";
|
|
18
|
+
|
|
19
|
+
// src/mcp/middleware/validation.ts
|
|
20
|
+
function validateToolInput(schema, args) {
|
|
21
|
+
const parsed = schema.safeParse(args);
|
|
22
|
+
if (parsed.success) {
|
|
23
|
+
return ok(parsed.data);
|
|
24
|
+
}
|
|
25
|
+
const message = formatZodError(parsed.error);
|
|
26
|
+
const validationError = new ValidationError(`Invalid tool input: ${message}`, {
|
|
27
|
+
context: {
|
|
28
|
+
issues: parsed.error.issues,
|
|
29
|
+
receivedType: typeof args
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return err(validationError);
|
|
33
|
+
}
|
|
34
|
+
function createValidator(schema) {
|
|
35
|
+
return (args) => validateToolInput(schema, args);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/mcp/middleware/rate-limiter.ts
|
|
39
|
+
var DEFAULT_REFILL_INTERVAL_MS = CACHE_TIMEOUTS.rateLimitRefillMs;
|
|
40
|
+
var RateLimiter = class {
|
|
41
|
+
tokens;
|
|
42
|
+
capacity;
|
|
43
|
+
refillRate;
|
|
44
|
+
refillIntervalMs;
|
|
45
|
+
lastRefillTime;
|
|
46
|
+
logger;
|
|
47
|
+
name;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.capacity = config.capacity;
|
|
50
|
+
this.refillRate = config.refillRate;
|
|
51
|
+
this.refillIntervalMs = config.refillIntervalMs ?? DEFAULT_REFILL_INTERVAL_MS;
|
|
52
|
+
this.tokens = this.capacity;
|
|
53
|
+
this.lastRefillTime = getTimeProvider().now();
|
|
54
|
+
this.name = config.name ?? "rate-limiter";
|
|
55
|
+
this.logger = config.logger ?? createLogger({ component: this.name });
|
|
56
|
+
this.logger.debug("Rate limiter initialized", {
|
|
57
|
+
capacity: this.capacity,
|
|
58
|
+
refillRate: this.refillRate,
|
|
59
|
+
refillIntervalMs: this.refillIntervalMs
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Refills tokens based on elapsed time.
|
|
64
|
+
* Called automatically before each acquire attempt.
|
|
65
|
+
*/
|
|
66
|
+
refill() {
|
|
67
|
+
const now = getTimeProvider().now();
|
|
68
|
+
const elapsed = now - this.lastRefillTime;
|
|
69
|
+
const intervals = Math.floor(elapsed / this.refillIntervalMs);
|
|
70
|
+
if (intervals > 0) {
|
|
71
|
+
const tokensToAdd = intervals * this.refillRate;
|
|
72
|
+
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
|
|
73
|
+
this.lastRefillTime = now - elapsed % this.refillIntervalMs;
|
|
74
|
+
if (tokensToAdd > 0) {
|
|
75
|
+
this.logger.debug("Tokens refilled", {
|
|
76
|
+
added: tokensToAdd,
|
|
77
|
+
current: this.tokens
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Attempts to acquire a token.
|
|
84
|
+
*
|
|
85
|
+
* @param count - Number of tokens to acquire (default: 1)
|
|
86
|
+
* @returns True if tokens were acquired, false if rate limited
|
|
87
|
+
*/
|
|
88
|
+
tryAcquire(count = 1) {
|
|
89
|
+
this.refill();
|
|
90
|
+
if (this.tokens >= count) {
|
|
91
|
+
this.tokens -= count;
|
|
92
|
+
this.logger.debug("Token acquired", {
|
|
93
|
+
requested: count,
|
|
94
|
+
remaining: this.tokens
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
this.logger.warn("Rate limit exceeded", {
|
|
99
|
+
requested: count,
|
|
100
|
+
available: this.tokens
|
|
101
|
+
});
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Gets the current state of the rate limiter.
|
|
106
|
+
*
|
|
107
|
+
* @returns The current rate limiter state
|
|
108
|
+
*/
|
|
109
|
+
getState() {
|
|
110
|
+
this.refill();
|
|
111
|
+
const nextTokenMs = this.tokens > 0 ? 0 : this.refillIntervalMs - (getTimeProvider().now() - this.lastRefillTime);
|
|
112
|
+
return {
|
|
113
|
+
tokens: this.tokens,
|
|
114
|
+
capacity: this.capacity,
|
|
115
|
+
nextTokenMs: Math.max(0, nextTokenMs)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resets the rate limiter to full capacity.
|
|
120
|
+
* Useful for testing or after configuration changes.
|
|
121
|
+
*/
|
|
122
|
+
reset() {
|
|
123
|
+
this.tokens = this.capacity;
|
|
124
|
+
this.lastRefillTime = getTimeProvider().now();
|
|
125
|
+
this.logger.debug("Rate limiter reset", { tokens: this.tokens });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function createDefaultRateLimiter(name, logger) {
|
|
129
|
+
const config = {
|
|
130
|
+
capacity: 100,
|
|
131
|
+
refillRate: 10,
|
|
132
|
+
refillIntervalMs: 1e3
|
|
133
|
+
};
|
|
134
|
+
if (name !== void 0) {
|
|
135
|
+
config.name = name;
|
|
136
|
+
}
|
|
137
|
+
if (logger !== void 0) {
|
|
138
|
+
config.logger = logger;
|
|
139
|
+
}
|
|
140
|
+
return new RateLimiter(config);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/mcp/middleware/policy-types.ts
|
|
144
|
+
import { z } from "zod";
|
|
145
|
+
var PolicyError = class extends SecurityError {
|
|
146
|
+
decision;
|
|
147
|
+
constructor(message, decision) {
|
|
148
|
+
super(message, {
|
|
149
|
+
context: {
|
|
150
|
+
allowed: decision.allowed,
|
|
151
|
+
reason: decision.reason,
|
|
152
|
+
ruleName: decision.ruleName,
|
|
153
|
+
requiredArtifact: decision.requiredArtifact
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
this.name = "PolicyError";
|
|
157
|
+
this.decision = decision;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var PolicyConfigSchema = z.object({
|
|
161
|
+
defaultMode: z.enum(["read-only", "read-write"]).default("read-only"),
|
|
162
|
+
policyMode: z.enum(["enforce", "warn"]).default("enforce"),
|
|
163
|
+
allowedPaths: z.array(z.string()).default(["./"])
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// src/mcp/middleware/policy-helpers.ts
|
|
167
|
+
function isPathSafe(targetPath, allowedPaths) {
|
|
168
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
169
|
+
for (const allowed of allowedPaths) {
|
|
170
|
+
const normalizedAllowed = normalizePath(allowed);
|
|
171
|
+
if (normalizedTarget.startsWith(normalizedAllowed)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
function normalizePath(p) {
|
|
178
|
+
let normalized = p.replace(/\/{1,100}$/, "");
|
|
179
|
+
if (normalized === ".") {
|
|
180
|
+
normalized = "";
|
|
181
|
+
} else if (normalized.startsWith("./")) {
|
|
182
|
+
normalized = normalized.slice(2);
|
|
183
|
+
}
|
|
184
|
+
if (!normalized.startsWith("/")) {
|
|
185
|
+
normalized = "/" + normalized;
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
function extractPathFromArgs(args) {
|
|
190
|
+
if (args === null || typeof args !== "object") {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
const argsObj = args;
|
|
194
|
+
const pathFields = ["path", "filePath", "file_path", "directory", "dir", "target"];
|
|
195
|
+
for (const field of pathFields) {
|
|
196
|
+
const value = argsObj[field];
|
|
197
|
+
if (typeof value === "string") {
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return void 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/mcp/middleware/policy-rules.ts
|
|
205
|
+
var MUTATION_TOOLS = /* @__PURE__ */ new Set([
|
|
206
|
+
"write_file",
|
|
207
|
+
"edit_file",
|
|
208
|
+
"delete_file",
|
|
209
|
+
"create_directory",
|
|
210
|
+
"remove_directory",
|
|
211
|
+
"execute_command",
|
|
212
|
+
"run_shell",
|
|
213
|
+
"bash",
|
|
214
|
+
"create_expert",
|
|
215
|
+
"run_workflow"
|
|
216
|
+
]);
|
|
217
|
+
var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
218
|
+
"read_file",
|
|
219
|
+
"list_directory",
|
|
220
|
+
"search_files",
|
|
221
|
+
"get_status",
|
|
222
|
+
"orchestrate",
|
|
223
|
+
"delegate_to_model"
|
|
224
|
+
]);
|
|
225
|
+
function isMutationTool(toolName) {
|
|
226
|
+
if (MUTATION_TOOLS.has(toolName)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (READ_ONLY_TOOLS.has(toolName)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
var denyMutationsWithoutModeRule = {
|
|
235
|
+
name: "deny-mutations-without-mode",
|
|
236
|
+
description: "Blocks write operations unless mode is read-write",
|
|
237
|
+
check(ctx) {
|
|
238
|
+
if (ctx.mode === "read-write") {
|
|
239
|
+
return { allowed: true, reason: "Read-write mode enabled" };
|
|
240
|
+
}
|
|
241
|
+
if (isMutationTool(ctx.toolName)) {
|
|
242
|
+
return {
|
|
243
|
+
allowed: false,
|
|
244
|
+
reason: `Tool '${ctx.toolName}' is a mutation operation but mode is '${ctx.mode}'. Set mode to 'read-write' to enable.`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { allowed: true, reason: "Read-only operation allowed" };
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var safePathsRule = {
|
|
251
|
+
name: "safe-paths",
|
|
252
|
+
description: "Validates paths against allowed root directories",
|
|
253
|
+
check(ctx) {
|
|
254
|
+
const targetPath = extractPathFromArgs(ctx.args);
|
|
255
|
+
if (targetPath === void 0) {
|
|
256
|
+
return { allowed: true, reason: "No path argument found" };
|
|
257
|
+
}
|
|
258
|
+
if (targetPath.includes("..")) {
|
|
259
|
+
return {
|
|
260
|
+
allowed: false,
|
|
261
|
+
reason: `Path contains '..' which may indicate path traversal: ${targetPath}`
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const allowedPaths = ctx.allowedPaths ?? ["./"];
|
|
265
|
+
if (!isPathSafe(targetPath, allowedPaths)) {
|
|
266
|
+
return {
|
|
267
|
+
allowed: false,
|
|
268
|
+
reason: `Path '${targetPath}' is outside allowed directories: ${allowedPaths.join(", ")}`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { allowed: true, reason: "Path is within allowed directories" };
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/mcp/middleware/policy.ts
|
|
276
|
+
var PolicyFirewall = class {
|
|
277
|
+
rules = [];
|
|
278
|
+
mode;
|
|
279
|
+
logger;
|
|
280
|
+
constructor(config) {
|
|
281
|
+
this.mode = config?.mode ?? "enforce";
|
|
282
|
+
this.logger = config?.logger ?? createLogger({ component: "policy-firewall" });
|
|
283
|
+
if (config?.rules) {
|
|
284
|
+
for (const rule of config.rules) {
|
|
285
|
+
this.rules.push(rule);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.logger.debug("Policy firewall initialized", {
|
|
289
|
+
mode: this.mode,
|
|
290
|
+
ruleCount: this.rules.length
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Evaluates all policy rules against the given context.
|
|
295
|
+
*
|
|
296
|
+
* Rules are evaluated in order. The first rule that denies stops
|
|
297
|
+
* evaluation and returns the denial decision.
|
|
298
|
+
*
|
|
299
|
+
* @param ctx - The policy context to evaluate
|
|
300
|
+
* @returns The policy decision
|
|
301
|
+
*/
|
|
302
|
+
evaluate(ctx) {
|
|
303
|
+
this.logger.debug("Evaluating policy", {
|
|
304
|
+
toolName: ctx.toolName,
|
|
305
|
+
mode: ctx.mode,
|
|
306
|
+
ruleCount: this.rules.length
|
|
307
|
+
});
|
|
308
|
+
if (this.rules.length === 0) {
|
|
309
|
+
return this.allowWithReason(ctx, "No policy rules configured");
|
|
310
|
+
}
|
|
311
|
+
for (const rule of this.rules) {
|
|
312
|
+
const decision = rule.check(ctx);
|
|
313
|
+
if (!decision.allowed) {
|
|
314
|
+
return this.handleDenial(ctx, rule, decision);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return this.allowWithReason(ctx, "All policy rules passed");
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Creates an allow decision with the given reason and logs it.
|
|
321
|
+
*/
|
|
322
|
+
allowWithReason(ctx, reason) {
|
|
323
|
+
const decision = { allowed: true, reason };
|
|
324
|
+
this.logDecision(ctx, decision);
|
|
325
|
+
return decision;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Handles a rule denial, respecting warn mode if configured.
|
|
329
|
+
*/
|
|
330
|
+
handleDenial(ctx, rule, decision) {
|
|
331
|
+
const denialDecision = {
|
|
332
|
+
...decision,
|
|
333
|
+
ruleName: rule.name
|
|
334
|
+
};
|
|
335
|
+
this.logDecision(ctx, denialDecision);
|
|
336
|
+
if (this.mode === "warn") {
|
|
337
|
+
this.logger.warn("Policy denial overridden by warn mode", {
|
|
338
|
+
toolName: ctx.toolName,
|
|
339
|
+
ruleName: rule.name,
|
|
340
|
+
reason: decision.reason
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
allowed: true,
|
|
344
|
+
reason: `[WARN MODE] Would be denied: ${decision.reason}`,
|
|
345
|
+
ruleName: rule.name
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return denialDecision;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Adds a policy rule to the firewall.
|
|
352
|
+
*
|
|
353
|
+
* @param rule - The rule to add
|
|
354
|
+
*/
|
|
355
|
+
addRule(rule) {
|
|
356
|
+
const existingIndex = this.rules.findIndex((r) => r.name === rule.name);
|
|
357
|
+
if (existingIndex >= 0) {
|
|
358
|
+
this.logger.warn("Replacing existing policy rule", { ruleName: rule.name });
|
|
359
|
+
this.rules[existingIndex] = rule;
|
|
360
|
+
} else {
|
|
361
|
+
this.rules.push(rule);
|
|
362
|
+
this.logger.debug("Policy rule added", { ruleName: rule.name });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Removes a policy rule by name.
|
|
367
|
+
*
|
|
368
|
+
* @param name - The name of the rule to remove
|
|
369
|
+
* @returns True if the rule was found and removed
|
|
370
|
+
*/
|
|
371
|
+
removeRule(name) {
|
|
372
|
+
const index = this.rules.findIndex((r) => r.name === name);
|
|
373
|
+
if (index >= 0) {
|
|
374
|
+
this.rules.splice(index, 1);
|
|
375
|
+
this.logger.debug("Policy rule removed", { ruleName: name });
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Gets all registered policy rules.
|
|
382
|
+
*
|
|
383
|
+
* @returns A readonly array of policy rules
|
|
384
|
+
*/
|
|
385
|
+
getRules() {
|
|
386
|
+
return [...this.rules];
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Sets the policy enforcement mode.
|
|
390
|
+
*
|
|
391
|
+
* @param mode - The new enforcement mode
|
|
392
|
+
*/
|
|
393
|
+
setMode(mode) {
|
|
394
|
+
const previousMode = this.mode;
|
|
395
|
+
this.mode = mode;
|
|
396
|
+
this.logger.info("Policy mode changed", { from: previousMode, to: mode });
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Gets the current policy enforcement mode.
|
|
400
|
+
*
|
|
401
|
+
* @returns The current mode
|
|
402
|
+
*/
|
|
403
|
+
getMode() {
|
|
404
|
+
return this.mode;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Logs a policy decision for audit purposes.
|
|
408
|
+
*/
|
|
409
|
+
logDecision(ctx, decision) {
|
|
410
|
+
const logData = {
|
|
411
|
+
toolName: ctx.toolName,
|
|
412
|
+
mode: ctx.mode,
|
|
413
|
+
workflowId: ctx.workflowId,
|
|
414
|
+
allowed: decision.allowed,
|
|
415
|
+
reason: decision.reason,
|
|
416
|
+
ruleName: decision.ruleName
|
|
417
|
+
};
|
|
418
|
+
if (decision.allowed) {
|
|
419
|
+
this.logger.debug("Policy decision: ALLOWED", logData);
|
|
420
|
+
} else {
|
|
421
|
+
this.logger.warn("Policy decision: DENIED", logData);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
function createDefaultPolicyFirewall(config) {
|
|
426
|
+
const firewall = new PolicyFirewall(config);
|
|
427
|
+
firewall.addRule(denyMutationsWithoutModeRule);
|
|
428
|
+
firewall.addRule(safePathsRule);
|
|
429
|
+
return firewall;
|
|
430
|
+
}
|
|
431
|
+
function evaluatePolicy(firewall, ctx) {
|
|
432
|
+
const decision = firewall.evaluate(ctx);
|
|
433
|
+
if (decision.allowed) {
|
|
434
|
+
return ok(void 0);
|
|
435
|
+
}
|
|
436
|
+
return err(new PolicyError(`Policy denied: ${decision.reason}`, decision));
|
|
437
|
+
}
|
|
438
|
+
function createPolicyContext(toolName, args, options) {
|
|
439
|
+
const base = {
|
|
440
|
+
toolName,
|
|
441
|
+
args,
|
|
442
|
+
mode: options?.mode ?? "read-only"
|
|
443
|
+
};
|
|
444
|
+
const result = { ...base };
|
|
445
|
+
if (options?.artifacts !== void 0) {
|
|
446
|
+
result["artifacts"] = options.artifacts;
|
|
447
|
+
}
|
|
448
|
+
if (options?.workflowId !== void 0) {
|
|
449
|
+
result["workflowId"] = options.workflowId;
|
|
450
|
+
}
|
|
451
|
+
if (options?.allowedPaths !== void 0) {
|
|
452
|
+
result["allowedPaths"] = options.allowedPaths;
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/audit/secure-handler-audit.ts
|
|
458
|
+
function actorFromContext(ctx, fallback) {
|
|
459
|
+
const caller = ctx.caller;
|
|
460
|
+
const clientId = caller.clientId;
|
|
461
|
+
if (clientId !== void 0 && clientId.length > 0) {
|
|
462
|
+
return {
|
|
463
|
+
type: clientId.includes("api") ? "external" : clientId.includes("cli") ? "agent" : "user",
|
|
464
|
+
id: clientId,
|
|
465
|
+
name: caller.userAgent
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return fallback ?? { type: "system", id: "unknown", name: "Unknown Caller" };
|
|
469
|
+
}
|
|
470
|
+
function resultToOutcome(isError, isPolicyDenied) {
|
|
471
|
+
if (isPolicyDenied) return "denied";
|
|
472
|
+
if (isError === true) return "failure";
|
|
473
|
+
return "success";
|
|
474
|
+
}
|
|
475
|
+
function logToolInvocationAudit(opts) {
|
|
476
|
+
opts.auditLogger.logToolInvocation({
|
|
477
|
+
toolName: opts.toolName,
|
|
478
|
+
outcome: opts.outcome,
|
|
479
|
+
actor: opts.actor,
|
|
480
|
+
requestId: opts.requestId,
|
|
481
|
+
durationMs: opts.durationMs,
|
|
482
|
+
errorMessage: opts.errorMessage
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function logPolicyAudit(opts) {
|
|
486
|
+
opts.auditLogger.logPolicyDecision({
|
|
487
|
+
policyName: opts.policyName,
|
|
488
|
+
decision: opts.decision,
|
|
489
|
+
reason: opts.reason,
|
|
490
|
+
toolName: opts.toolName,
|
|
491
|
+
actor: opts.actor,
|
|
492
|
+
requestId: opts.requestId
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function logRateLimitAudit(opts) {
|
|
496
|
+
opts.auditLogger.logRateLimitViolation({
|
|
497
|
+
toolName: opts.toolName,
|
|
498
|
+
actor: opts.actor,
|
|
499
|
+
currentRate: opts.currentRate,
|
|
500
|
+
limitRate: opts.limitRate,
|
|
501
|
+
requestId: opts.requestId
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/mcp/error-envelope.ts
|
|
506
|
+
import { z as z2 } from "zod";
|
|
507
|
+
var ErrorCategorySchema = z2.enum([
|
|
508
|
+
"transient",
|
|
509
|
+
"validation",
|
|
510
|
+
"permission",
|
|
511
|
+
"business",
|
|
512
|
+
"internal"
|
|
513
|
+
]);
|
|
514
|
+
var ToolErrorEnvelopeSchema = z2.object({
|
|
515
|
+
errorCategory: ErrorCategorySchema,
|
|
516
|
+
/** Whether retrying the same call could succeed without caller changes. */
|
|
517
|
+
isRetryable: z2.boolean(),
|
|
518
|
+
/** Human-readable summary. Bounded to keep stack traces out of results. */
|
|
519
|
+
message: z2.string().min(1).max(2e3),
|
|
520
|
+
/**
|
|
521
|
+
* Optional structured context. MUST NOT carry secrets, credentials,
|
|
522
|
+
* absolute filesystem paths, or raw Error/response objects — those are
|
|
523
|
+
* an information-disclosure risk (this field is not output-sanitized).
|
|
524
|
+
*/
|
|
525
|
+
detail: z2.record(z2.string(), z2.unknown()).optional()
|
|
526
|
+
});
|
|
527
|
+
var ERROR_ENVELOPE_META_KEY = "nexus-agents/error";
|
|
528
|
+
function defaultRetryable(category) {
|
|
529
|
+
return category === "transient";
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/mcp/tools/tool-result.ts
|
|
533
|
+
function toolSuccess(text) {
|
|
534
|
+
return {
|
|
535
|
+
content: [{ type: "text", text }]
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function toolSuccessStructured(data) {
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
541
|
+
structuredContent: data
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function toolStructuredError(input) {
|
|
545
|
+
const envelope = {
|
|
546
|
+
errorCategory: input.errorCategory,
|
|
547
|
+
isRetryable: input.isRetryable ?? defaultRetryable(input.errorCategory),
|
|
548
|
+
message: input.message,
|
|
549
|
+
...input.detail !== void 0 ? { detail: input.detail } : {}
|
|
550
|
+
};
|
|
551
|
+
return {
|
|
552
|
+
isError: true,
|
|
553
|
+
content: [{ type: "text", text: envelope.message }],
|
|
554
|
+
_meta: { [ERROR_ENVELOPE_META_KEY]: envelope }
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function toolError(message) {
|
|
558
|
+
return toolStructuredError({ errorCategory: "internal", message });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/mcp/middleware/request-context.ts
|
|
562
|
+
import { randomBytes } from "crypto";
|
|
563
|
+
function generateRequestId() {
|
|
564
|
+
const bytes = randomBytes(8);
|
|
565
|
+
return `req_${bytes.toString("hex")}`;
|
|
566
|
+
}
|
|
567
|
+
function formatTimestamp() {
|
|
568
|
+
const now = new Date(getTimeProvider().now());
|
|
569
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
570
|
+
timeZone: "America/New_York",
|
|
571
|
+
timeZoneName: "shortOffset"
|
|
572
|
+
});
|
|
573
|
+
const parts = formatter.formatToParts(now);
|
|
574
|
+
const offset = parts.find((p) => p.type === "timeZoneName")?.value ?? "-05:00";
|
|
575
|
+
const base = now.toLocaleString("sv-SE", {
|
|
576
|
+
timeZone: "America/New_York",
|
|
577
|
+
hour12: false
|
|
578
|
+
});
|
|
579
|
+
return base.replace(" ", "T") + offset.replace("GMT", "");
|
|
580
|
+
}
|
|
581
|
+
function deriveTrustTier(caller) {
|
|
582
|
+
const knownClients = ["claude-cli", "gemini-cli", "codex-cli"];
|
|
583
|
+
if (caller.transport === "stdio") return "1";
|
|
584
|
+
if (caller.authenticated === true) {
|
|
585
|
+
if (caller.clientId !== void 0 && knownClients.includes(caller.clientId)) {
|
|
586
|
+
return "1";
|
|
587
|
+
}
|
|
588
|
+
return "2";
|
|
589
|
+
}
|
|
590
|
+
return "3";
|
|
591
|
+
}
|
|
592
|
+
function createRequestContext(options) {
|
|
593
|
+
const caller = options.caller ?? {};
|
|
594
|
+
const context = {
|
|
595
|
+
requestId: generateRequestId(),
|
|
596
|
+
timestamp: formatTimestamp(),
|
|
597
|
+
toolName: options.toolName,
|
|
598
|
+
caller,
|
|
599
|
+
trustTier: options.trustTier ?? deriveTrustTier(caller),
|
|
600
|
+
...options.traceId !== void 0 && { traceId: options.traceId },
|
|
601
|
+
...options.parentSpanId !== void 0 && { parentSpanId: options.parentSpanId }
|
|
602
|
+
};
|
|
603
|
+
return Object.freeze(context);
|
|
604
|
+
}
|
|
605
|
+
function contextForLogging(ctx) {
|
|
606
|
+
return {
|
|
607
|
+
requestId: ctx.requestId,
|
|
608
|
+
toolName: ctx.toolName,
|
|
609
|
+
trustTier: ctx.trustTier,
|
|
610
|
+
...ctx.caller.clientId !== void 0 && { clientId: ctx.caller.clientId },
|
|
611
|
+
...ctx.traceId !== void 0 && { traceId: ctx.traceId }
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/mcp/middleware/tool-input-sanitizer.ts
|
|
616
|
+
var XML_INJECTION_PATTERN = /<\/?(system|human|assistant|instructions|user|prompt|context|tool_use|tool_result)\b[^>]*>/gi;
|
|
617
|
+
var INJECTION_DETECTORS = [
|
|
618
|
+
{ name: "system_prompt_override", pattern: /ignore (?:all )?previous (?:instructions|rules)/i },
|
|
619
|
+
{ name: "role_impersonation", pattern: /i(?:'m| am) the (?:repo |project )?(?:owner|admin)/i },
|
|
620
|
+
{ name: "hidden_instruction", pattern: /<!--[\s\S]*?(?:execute|delete|merge|apply)[\s\S]*?-->/i }
|
|
621
|
+
];
|
|
622
|
+
function sanitizeString(value) {
|
|
623
|
+
XML_INJECTION_PATTERN.lastIndex = 0;
|
|
624
|
+
const cleaned = value.replace(XML_INJECTION_PATTERN, "");
|
|
625
|
+
return { cleaned, modified: cleaned !== value };
|
|
626
|
+
}
|
|
627
|
+
function detectPatterns(value) {
|
|
628
|
+
const detected = [];
|
|
629
|
+
for (const { name, pattern } of INJECTION_DETECTORS) {
|
|
630
|
+
pattern.lastIndex = 0;
|
|
631
|
+
if (pattern.test(value)) {
|
|
632
|
+
detected.push(name);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return detected;
|
|
636
|
+
}
|
|
637
|
+
function sanitizeValue(value, stats) {
|
|
638
|
+
if (typeof value === "string") {
|
|
639
|
+
const patterns = detectPatterns(value);
|
|
640
|
+
if (patterns.length > 0) {
|
|
641
|
+
stats.patterns.push(...patterns);
|
|
642
|
+
}
|
|
643
|
+
const { cleaned, modified } = sanitizeString(value);
|
|
644
|
+
if (modified) stats.count++;
|
|
645
|
+
return cleaned;
|
|
646
|
+
}
|
|
647
|
+
if (Array.isArray(value)) {
|
|
648
|
+
return value.map((item) => sanitizeValue(item, stats));
|
|
649
|
+
}
|
|
650
|
+
if (value !== null && typeof value === "object") {
|
|
651
|
+
const result = {};
|
|
652
|
+
for (const [key, val] of Object.entries(value)) {
|
|
653
|
+
result[key] = sanitizeValue(val, stats);
|
|
654
|
+
}
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
return value;
|
|
658
|
+
}
|
|
659
|
+
function sanitizeToolInput(args) {
|
|
660
|
+
if (args === void 0 || args === null) {
|
|
661
|
+
return { sanitized: args, wasModified: false, modifiedCount: 0, detectedPatterns: [] };
|
|
662
|
+
}
|
|
663
|
+
const stats = { count: 0, patterns: [] };
|
|
664
|
+
const sanitized = sanitizeValue(args, stats);
|
|
665
|
+
const uniquePatterns = [...new Set(stats.patterns)];
|
|
666
|
+
return {
|
|
667
|
+
sanitized,
|
|
668
|
+
wasModified: stats.count > 0,
|
|
669
|
+
modifiedCount: stats.count,
|
|
670
|
+
detectedPatterns: uniquePatterns
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function logSanitizationResult(result, logger, toolName) {
|
|
674
|
+
if (result.wasModified) {
|
|
675
|
+
logger.warn("Tool input sanitized \u2014 XML injection tags stripped", {
|
|
676
|
+
tool: toolName,
|
|
677
|
+
modifiedFields: result.modifiedCount
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (result.detectedPatterns.length > 0) {
|
|
681
|
+
logger.warn("Injection patterns detected in tool input", {
|
|
682
|
+
tool: toolName,
|
|
683
|
+
patterns: result.detectedPatterns
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/mcp/middleware/secure-handler.ts
|
|
689
|
+
function rateLimitError(nextTokenMs) {
|
|
690
|
+
return toolStructuredError({
|
|
691
|
+
errorCategory: "transient",
|
|
692
|
+
message: `Rate limit exceeded. Try again in ${String(nextTokenMs)}ms.`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function policyDeniedError(reason, requestId) {
|
|
696
|
+
return toolStructuredError({
|
|
697
|
+
errorCategory: "permission",
|
|
698
|
+
message: `Policy denied: ${reason} (request: ${requestId})`
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function internalError(message, requestId) {
|
|
702
|
+
return toolStructuredError({
|
|
703
|
+
errorCategory: "internal",
|
|
704
|
+
message: `Internal error: ${message} (request: ${requestId})`
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
var MAX_INPUT_SIZE_BYTES = 10 * 1024 * 1024;
|
|
708
|
+
var SECRET_PATTERNS = [
|
|
709
|
+
// API keys with common prefixes
|
|
710
|
+
/\b(sk-[a-zA-Z0-9]{20,})\b/g,
|
|
711
|
+
/\b(pk-[a-zA-Z0-9]{20,})\b/g,
|
|
712
|
+
// AWS-style keys
|
|
713
|
+
/\b(AKIA[A-Z0-9]{16})\b/g,
|
|
714
|
+
// Bearer tokens in output
|
|
715
|
+
/Bearer\s+[a-zA-Z0-9_\-.~+/]+=*/g,
|
|
716
|
+
// Generic long hex secrets (40+ chars)
|
|
717
|
+
/\b[0-9a-f]{40,}\b/gi,
|
|
718
|
+
// password= or token= in output
|
|
719
|
+
/(?:password|token|secret|apikey|api_key)\s*[=:]\s*\S{8,}/gi
|
|
720
|
+
];
|
|
721
|
+
function sanitizeOutput(text, logger) {
|
|
722
|
+
let sanitized = text;
|
|
723
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
724
|
+
const before = sanitized;
|
|
725
|
+
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
726
|
+
if (sanitized !== before) {
|
|
727
|
+
logger.warn("Potential secret detected in tool output, redacting", {
|
|
728
|
+
pattern: pattern.source.slice(0, 30)
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return sanitized;
|
|
733
|
+
}
|
|
734
|
+
function sanitizeToolResult(result, logger) {
|
|
735
|
+
for (const item of result.content) {
|
|
736
|
+
item.text = sanitizeOutput(item.text, logger);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function checkInputSize(args, logger, requestId) {
|
|
740
|
+
if (args === void 0) return null;
|
|
741
|
+
const inputSize = JSON.stringify(args).length;
|
|
742
|
+
if (inputSize > MAX_INPUT_SIZE_BYTES) {
|
|
743
|
+
logger.warn("Input size exceeds limit", { inputSize, limit: MAX_INPUT_SIZE_BYTES });
|
|
744
|
+
return internalError("Input too large", requestId);
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
function checkRateLimit(rateLimiter, logger) {
|
|
749
|
+
const acquired = rateLimiter.tryAcquire();
|
|
750
|
+
if (!acquired) {
|
|
751
|
+
const state = rateLimiter.getState();
|
|
752
|
+
logger.warn("Rate limit exceeded");
|
|
753
|
+
return rateLimitError(state.nextTokenMs);
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
function checkPolicy(opts) {
|
|
758
|
+
const ctxOpts = {
|
|
759
|
+
mode: opts.mode,
|
|
760
|
+
...opts.allowedPaths && { allowedPaths: opts.allowedPaths }
|
|
761
|
+
};
|
|
762
|
+
const decision = opts.firewall.evaluate(createPolicyContext(opts.toolName, opts.args, ctxOpts));
|
|
763
|
+
if (!decision.allowed) {
|
|
764
|
+
opts.logger.warn("Policy denied tool execution", {
|
|
765
|
+
reason: decision.reason,
|
|
766
|
+
ruleName: decision.ruleName
|
|
767
|
+
});
|
|
768
|
+
return policyDeniedError(decision.reason, opts.requestId);
|
|
769
|
+
}
|
|
770
|
+
opts.logger.debug("Policy check passed", { reason: decision.reason });
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
async function executeHandler(handler, args, ctx, logger) {
|
|
774
|
+
const startTime = getTimeProvider().now();
|
|
775
|
+
const result = handler.length >= 2 ? await handler(args, ctx) : await handler(args);
|
|
776
|
+
const durationMs = getTimeProvider().now() - startTime;
|
|
777
|
+
if (result.isError === true) {
|
|
778
|
+
logger.warn("Tool execution completed with error", { durationMs });
|
|
779
|
+
} else {
|
|
780
|
+
logger.info("Tool execution completed", { durationMs });
|
|
781
|
+
}
|
|
782
|
+
return result;
|
|
783
|
+
}
|
|
784
|
+
function emitToolAudit(auditLogger, toolName, ctx, result, durationMs) {
|
|
785
|
+
const actor = actorFromContext(ctx);
|
|
786
|
+
const outcome = resultToOutcome(result.isError, false);
|
|
787
|
+
auditLogger.logToolInvocation({
|
|
788
|
+
toolName,
|
|
789
|
+
outcome,
|
|
790
|
+
actor,
|
|
791
|
+
requestId: ctx.requestId,
|
|
792
|
+
durationMs
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function emitToolAuditException(auditLogger, toolName, ctx, durationMs) {
|
|
796
|
+
const actor = actorFromContext(ctx);
|
|
797
|
+
auditLogger.logToolInvocation({
|
|
798
|
+
toolName,
|
|
799
|
+
outcome: "error",
|
|
800
|
+
actor,
|
|
801
|
+
requestId: ctx.requestId,
|
|
802
|
+
durationMs
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function emitPolicyAudit(auditLogger, toolName, ctx, reason) {
|
|
806
|
+
const actor = actorFromContext(ctx);
|
|
807
|
+
auditLogger.logPolicyDecision({
|
|
808
|
+
policyName: "default",
|
|
809
|
+
decision: "deny",
|
|
810
|
+
reason,
|
|
811
|
+
toolName,
|
|
812
|
+
actor,
|
|
813
|
+
requestId: ctx.requestId
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function emitRateLimitAudit(auditLogger, toolName, ctx) {
|
|
817
|
+
const actor = actorFromContext(ctx);
|
|
818
|
+
auditLogger.logRateLimitViolation({
|
|
819
|
+
toolName,
|
|
820
|
+
actor,
|
|
821
|
+
currentRate: 0,
|
|
822
|
+
limitRate: 0,
|
|
823
|
+
requestId: ctx.requestId
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function checkSecurityTier(config, sanitizeResult, logger) {
|
|
827
|
+
const tier = config.securityTier ?? "standard";
|
|
828
|
+
if (tier === "standard" || sanitizeResult.detectedPatterns.length === 0) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
logger.warn("Input rejected by security tier validation", {
|
|
832
|
+
tier,
|
|
833
|
+
patterns: sanitizeResult.detectedPatterns
|
|
834
|
+
});
|
|
835
|
+
return toolStructuredError({
|
|
836
|
+
errorCategory: "permission",
|
|
837
|
+
message: `Input validation failed: detected patterns [${sanitizeResult.detectedPatterns.join(", ")}]. Remove prompt injection patterns and retry.`
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function runPreChecks(config, args, mode, requestContext, logger) {
|
|
841
|
+
const sizeResult = checkInputSize(args, logger, requestContext.requestId);
|
|
842
|
+
if (sizeResult) return { error: sizeResult, sanitizedArgs: args };
|
|
843
|
+
const sanitizeResult = sanitizeToolInput(args);
|
|
844
|
+
logSanitizationResult(sanitizeResult, logger, config.toolName);
|
|
845
|
+
const sanitizedArgs = sanitizeResult.wasModified ? sanitizeResult.sanitized : args;
|
|
846
|
+
const tierError = checkSecurityTier(config, sanitizeResult, logger);
|
|
847
|
+
if (tierError !== null) return { error: tierError, sanitizedArgs };
|
|
848
|
+
if (config.rateLimiter) {
|
|
849
|
+
const rlResult = checkRateLimit(config.rateLimiter, logger);
|
|
850
|
+
if (rlResult) {
|
|
851
|
+
if (config.auditLogger)
|
|
852
|
+
emitRateLimitAudit(config.auditLogger, config.toolName, requestContext);
|
|
853
|
+
return { error: rlResult, sanitizedArgs };
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (config.policyFirewall) {
|
|
857
|
+
const pResult = checkPolicy({
|
|
858
|
+
firewall: config.policyFirewall,
|
|
859
|
+
toolName: config.toolName,
|
|
860
|
+
args: sanitizedArgs,
|
|
861
|
+
mode,
|
|
862
|
+
allowedPaths: config.allowedPaths,
|
|
863
|
+
logger,
|
|
864
|
+
requestId: requestContext.requestId
|
|
865
|
+
});
|
|
866
|
+
if (pResult) {
|
|
867
|
+
if (config.auditLogger)
|
|
868
|
+
emitPolicyAudit(config.auditLogger, config.toolName, requestContext, "policy denied");
|
|
869
|
+
return { error: pResult, sanitizedArgs };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return { error: null, sanitizedArgs };
|
|
873
|
+
}
|
|
874
|
+
function createSecureHandler(handler, config) {
|
|
875
|
+
const logger = config.logger ?? createLogger({ tool: config.toolName });
|
|
876
|
+
const mode = config.executionMode ?? "read-only";
|
|
877
|
+
return async (args) => {
|
|
878
|
+
const ctxOpts = {
|
|
879
|
+
toolName: config.toolName,
|
|
880
|
+
...config.callerInfo && { caller: config.callerInfo }
|
|
881
|
+
};
|
|
882
|
+
const requestContext = createRequestContext(ctxOpts);
|
|
883
|
+
const requestLogger = logger.child(contextForLogging(requestContext));
|
|
884
|
+
requestLogger.info("Tool invocation started");
|
|
885
|
+
const { error: preCheckError, sanitizedArgs } = runPreChecks(
|
|
886
|
+
config,
|
|
887
|
+
args,
|
|
888
|
+
mode,
|
|
889
|
+
requestContext,
|
|
890
|
+
requestLogger
|
|
891
|
+
);
|
|
892
|
+
if (preCheckError) return preCheckError;
|
|
893
|
+
return executeAndAudit(handler, sanitizedArgs, requestContext, requestLogger, config);
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async function executeAndAudit(handler, sanitizedArgs, requestContext, requestLogger, config) {
|
|
897
|
+
const execStartTime = getTimeProvider().now();
|
|
898
|
+
try {
|
|
899
|
+
const result = await executeHandler(
|
|
900
|
+
handler,
|
|
901
|
+
sanitizedArgs,
|
|
902
|
+
{ requestContext, logger: requestLogger },
|
|
903
|
+
requestLogger
|
|
904
|
+
);
|
|
905
|
+
sanitizeToolResult(result, requestLogger);
|
|
906
|
+
if (config.auditLogger) {
|
|
907
|
+
emitToolAudit(
|
|
908
|
+
config.auditLogger,
|
|
909
|
+
config.toolName,
|
|
910
|
+
requestContext,
|
|
911
|
+
result,
|
|
912
|
+
getTimeProvider().now() - execStartTime
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
} catch (error) {
|
|
917
|
+
const rawMessage = error instanceof Error ? error.message : "Unknown error";
|
|
918
|
+
requestLogger.error("Tool execution failed", error instanceof Error ? error : void 0);
|
|
919
|
+
if (config.auditLogger) {
|
|
920
|
+
emitToolAuditException(
|
|
921
|
+
config.auditLogger,
|
|
922
|
+
config.toolName,
|
|
923
|
+
requestContext,
|
|
924
|
+
getTimeProvider().now() - execStartTime
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const sanitized = sanitizeOutput(rawMessage, requestLogger);
|
|
928
|
+
return internalError(sanitized, requestContext.requestId);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/mcp/mcp-notifier.ts
|
|
933
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
934
|
+
var internalLogger = createLogger({ component: "mcp-notifier" });
|
|
935
|
+
function createMcpNotifier(server) {
|
|
936
|
+
function send(level, logger, data) {
|
|
937
|
+
try {
|
|
938
|
+
server.sendLoggingMessage({ level, logger, data }).catch((error) => {
|
|
939
|
+
internalLogger.debug("Failed to send MCP notification", {
|
|
940
|
+
level,
|
|
941
|
+
logger,
|
|
942
|
+
error: getErrorMessage(error)
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
} catch (error) {
|
|
946
|
+
internalLogger.debug("Failed to send MCP notification", {
|
|
947
|
+
level,
|
|
948
|
+
logger,
|
|
949
|
+
error: getErrorMessage(error)
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
info: (logger, data) => {
|
|
955
|
+
send("info", logger, data);
|
|
956
|
+
},
|
|
957
|
+
debug: (logger, data) => {
|
|
958
|
+
send("debug", logger, data);
|
|
959
|
+
},
|
|
960
|
+
warn: (logger, data) => {
|
|
961
|
+
send("warning", logger, data);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
var NOOP_NOTIFIER = {
|
|
966
|
+
info: () => void 0,
|
|
967
|
+
debug: () => void 0,
|
|
968
|
+
warn: () => void 0
|
|
969
|
+
};
|
|
970
|
+
var progressContextStorage = new AsyncLocalStorage();
|
|
971
|
+
var abortSignalStorage = new AsyncLocalStorage();
|
|
972
|
+
async function withProgressHeartbeat(toolName, notifier, operation, intervalMs = 15e3) {
|
|
973
|
+
const startTime = Date.now();
|
|
974
|
+
let beatCount = 0;
|
|
975
|
+
const progressCtx = progressContextStorage.getStore();
|
|
976
|
+
const timer = setInterval(() => {
|
|
977
|
+
beatCount++;
|
|
978
|
+
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
979
|
+
if (progressCtx !== void 0) {
|
|
980
|
+
progressCtx.sendNotification(beatCount);
|
|
981
|
+
}
|
|
982
|
+
notifier.debug(toolName, {
|
|
983
|
+
event: "heartbeat",
|
|
984
|
+
elapsedSeconds: elapsed,
|
|
985
|
+
beatCount,
|
|
986
|
+
hasProgressToken: progressCtx !== void 0
|
|
987
|
+
});
|
|
988
|
+
}, intervalMs);
|
|
989
|
+
try {
|
|
990
|
+
return await operation();
|
|
991
|
+
} finally {
|
|
992
|
+
clearInterval(timer);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/mcp/middleware/tool-wrapper.ts
|
|
997
|
+
import { randomUUID } from "crypto";
|
|
998
|
+
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
|
999
|
+
import { dirname, join } from "path";
|
|
1000
|
+
|
|
1001
|
+
// src/mcp/middleware/timeout-guard.ts
|
|
1002
|
+
var DEFAULT_TIMEOUT_MS = TIMEOUT_GUARD.defaultMs;
|
|
1003
|
+
var MAX_TIMEOUT_MS = TIMEOUT_GUARD.maxMs;
|
|
1004
|
+
var NEAR_TIMEOUT_THRESHOLD = TIMEOUT_GUARD.nearTimeoutThreshold;
|
|
1005
|
+
function createTimeoutError(operationName, timeoutMs) {
|
|
1006
|
+
return {
|
|
1007
|
+
code: "OPERATION_TIMEOUT",
|
|
1008
|
+
message: `Operation '${operationName}' timed out after ${String(timeoutMs)}ms`,
|
|
1009
|
+
operation: operationName,
|
|
1010
|
+
timeoutMs
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function createGuardError(error, operationName) {
|
|
1014
|
+
const guardError = {
|
|
1015
|
+
code: "GUARD_ERROR",
|
|
1016
|
+
message: getErrorMessage(error),
|
|
1017
|
+
operation: operationName
|
|
1018
|
+
};
|
|
1019
|
+
if (error instanceof Error) {
|
|
1020
|
+
return { ...guardError, cause: error };
|
|
1021
|
+
}
|
|
1022
|
+
return guardError;
|
|
1023
|
+
}
|
|
1024
|
+
var TimeoutGuard = class {
|
|
1025
|
+
defaultTimeoutMs;
|
|
1026
|
+
maxTimeoutMs;
|
|
1027
|
+
enableLogging;
|
|
1028
|
+
logger;
|
|
1029
|
+
constructor(config) {
|
|
1030
|
+
this.defaultTimeoutMs = config?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1031
|
+
this.maxTimeoutMs = config?.maxTimeoutMs ?? MAX_TIMEOUT_MS;
|
|
1032
|
+
this.enableLogging = config?.enableLogging ?? true;
|
|
1033
|
+
this.logger = config?.logger ?? createLogger({ component: "timeout-guard" });
|
|
1034
|
+
}
|
|
1035
|
+
/** Resolves effective timeout and operation name from options. */
|
|
1036
|
+
resolveOptions(options) {
|
|
1037
|
+
return {
|
|
1038
|
+
timeoutMs: Math.min(options?.timeoutMs ?? this.defaultTimeoutMs, this.maxTimeoutMs),
|
|
1039
|
+
operationName: options?.operationName ?? "unknown"
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Executes an async operation with timeout protection.
|
|
1044
|
+
*/
|
|
1045
|
+
async execute(operation, options) {
|
|
1046
|
+
const { timeoutMs, operationName } = this.resolveOptions(options);
|
|
1047
|
+
const validationError = this.validateTimeout(timeoutMs, operationName);
|
|
1048
|
+
if (validationError !== null) {
|
|
1049
|
+
return err(validationError);
|
|
1050
|
+
}
|
|
1051
|
+
return this.runGuarded(operation, timeoutMs, operationName, options);
|
|
1052
|
+
}
|
|
1053
|
+
/** Runs the operation with timeout and optional abort signal. */
|
|
1054
|
+
async runGuarded(operation, timeoutMs, operationName, options) {
|
|
1055
|
+
this.logStart(operationName, timeoutMs);
|
|
1056
|
+
const startTime = getTimeProvider().now();
|
|
1057
|
+
const state = { timeoutId: void 0, timedOut: false };
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await this.runWithTimeout(
|
|
1060
|
+
operation,
|
|
1061
|
+
timeoutMs,
|
|
1062
|
+
state,
|
|
1063
|
+
options?.onTimeout,
|
|
1064
|
+
options?.signal
|
|
1065
|
+
);
|
|
1066
|
+
return this.handleSuccess(result, startTime, timeoutMs, operationName);
|
|
1067
|
+
} catch {
|
|
1068
|
+
const cancelled = options?.signal?.aborted === true && !state.timedOut;
|
|
1069
|
+
return err(
|
|
1070
|
+
this.handleFailure(state.timedOut, operationName, timeoutMs, startTime, cancelled)
|
|
1071
|
+
);
|
|
1072
|
+
} finally {
|
|
1073
|
+
if (state.timeoutId !== void 0) {
|
|
1074
|
+
clearTimeout(state.timeoutId);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
validateTimeout(timeoutMs, operationName) {
|
|
1079
|
+
if (timeoutMs <= 0) {
|
|
1080
|
+
return {
|
|
1081
|
+
code: "INVALID_TIMEOUT",
|
|
1082
|
+
message: `Invalid timeout: ${String(timeoutMs)}ms`,
|
|
1083
|
+
operation: operationName
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
logStart(operationName, timeoutMs) {
|
|
1089
|
+
if (this.enableLogging) {
|
|
1090
|
+
this.logger.debug("Starting guarded operation", { operation: operationName, timeoutMs });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async runWithTimeout(operation, timeoutMs, state, onTimeout, signal) {
|
|
1094
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1095
|
+
state.timeoutId = setTimeout(() => {
|
|
1096
|
+
state.timedOut = true;
|
|
1097
|
+
onTimeout?.();
|
|
1098
|
+
reject(new Error(`Operation timed out after ${String(timeoutMs)}ms`));
|
|
1099
|
+
}, timeoutMs);
|
|
1100
|
+
});
|
|
1101
|
+
const promises = [operation(), timeoutPromise];
|
|
1102
|
+
if (signal !== void 0 && !signal.aborted) {
|
|
1103
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
1104
|
+
signal.addEventListener(
|
|
1105
|
+
"abort",
|
|
1106
|
+
() => {
|
|
1107
|
+
reject(new Error("Operation cancelled by client"));
|
|
1108
|
+
},
|
|
1109
|
+
{ once: true }
|
|
1110
|
+
);
|
|
1111
|
+
});
|
|
1112
|
+
promises.push(abortPromise);
|
|
1113
|
+
}
|
|
1114
|
+
return Promise.race(promises);
|
|
1115
|
+
}
|
|
1116
|
+
handleSuccess(result, startTime, timeoutMs, operationName) {
|
|
1117
|
+
const durationMs = getTimeProvider().now() - startTime;
|
|
1118
|
+
const nearTimeout = durationMs > timeoutMs * NEAR_TIMEOUT_THRESHOLD;
|
|
1119
|
+
if (nearTimeout && this.enableLogging) {
|
|
1120
|
+
this.logger.warn("Operation completed near timeout threshold", {
|
|
1121
|
+
operation: operationName,
|
|
1122
|
+
durationMs,
|
|
1123
|
+
timeoutMs,
|
|
1124
|
+
thresholdPercent: Math.round(durationMs / timeoutMs * 100)
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (this.enableLogging) {
|
|
1128
|
+
this.logger.debug("Guarded operation completed", { operation: operationName, durationMs });
|
|
1129
|
+
}
|
|
1130
|
+
return ok({ value: result, durationMs, nearTimeout });
|
|
1131
|
+
}
|
|
1132
|
+
handleFailure(timedOut, operationName, timeoutMs, startTime, cancelled = false) {
|
|
1133
|
+
const durationMs = getTimeProvider().now() - startTime;
|
|
1134
|
+
if (cancelled) {
|
|
1135
|
+
this.logger.info("Operation cancelled by client", {
|
|
1136
|
+
operation: operationName,
|
|
1137
|
+
durationMs
|
|
1138
|
+
});
|
|
1139
|
+
return {
|
|
1140
|
+
code: "OPERATION_CANCELLED",
|
|
1141
|
+
message: `Operation '${operationName}' cancelled by client`,
|
|
1142
|
+
operation: operationName
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
if (timedOut) {
|
|
1146
|
+
this.logger.error("Operation timed out", void 0, {
|
|
1147
|
+
operation: operationName,
|
|
1148
|
+
timeoutMs,
|
|
1149
|
+
durationMs
|
|
1150
|
+
});
|
|
1151
|
+
return createTimeoutError(operationName, timeoutMs);
|
|
1152
|
+
}
|
|
1153
|
+
return createGuardError(new Error("Unknown error"), operationName);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Creates a guarded version of an async function.
|
|
1157
|
+
*/
|
|
1158
|
+
guard(fn, options) {
|
|
1159
|
+
return async (...args) => {
|
|
1160
|
+
return this.execute(() => fn(...args), options);
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// src/security/access-constraint-deriver/denylist.ts
|
|
1166
|
+
var UNBYPASSABLE_PATH_PATTERNS = [
|
|
1167
|
+
// Environment files
|
|
1168
|
+
".env",
|
|
1169
|
+
".env.*",
|
|
1170
|
+
"**/.env",
|
|
1171
|
+
"**/.env.*",
|
|
1172
|
+
// SSH credentials
|
|
1173
|
+
"~/.ssh/**",
|
|
1174
|
+
"**/ssh/id_*",
|
|
1175
|
+
"**/*_rsa",
|
|
1176
|
+
"**/*_ed25519",
|
|
1177
|
+
"**/*.pem",
|
|
1178
|
+
// Cloud credentials
|
|
1179
|
+
"~/.aws/**",
|
|
1180
|
+
"~/.azure/**",
|
|
1181
|
+
"~/.gcp/**",
|
|
1182
|
+
"~/.config/gcloud/**",
|
|
1183
|
+
"~/.kube/config",
|
|
1184
|
+
// Unix secret files
|
|
1185
|
+
"/etc/shadow",
|
|
1186
|
+
"/etc/sudoers",
|
|
1187
|
+
"/etc/sudoers.d/**",
|
|
1188
|
+
// Common secret file patterns
|
|
1189
|
+
"**/secrets.*",
|
|
1190
|
+
"**/credentials.*",
|
|
1191
|
+
"**/private_key.*",
|
|
1192
|
+
"**/id_rsa*"
|
|
1193
|
+
];
|
|
1194
|
+
var UNBYPASSABLE_TOOL_NAMES = [
|
|
1195
|
+
// Destructive git operations
|
|
1196
|
+
"git_push_force",
|
|
1197
|
+
"git_reset_hard",
|
|
1198
|
+
"git_branch_delete_force",
|
|
1199
|
+
"git_clean_force",
|
|
1200
|
+
// Destructive filesystem
|
|
1201
|
+
"rm_recursive_force",
|
|
1202
|
+
"chmod_recursive",
|
|
1203
|
+
// Identity / auth mutations
|
|
1204
|
+
"ssh_add_key",
|
|
1205
|
+
"gpg_add_key",
|
|
1206
|
+
"npm_publish_force",
|
|
1207
|
+
// Remote destruction
|
|
1208
|
+
"github_repo_delete",
|
|
1209
|
+
"github_org_transfer",
|
|
1210
|
+
"aws_account_close"
|
|
1211
|
+
];
|
|
1212
|
+
function compileGlobToRegex(pattern) {
|
|
1213
|
+
const pat = pattern.toLowerCase();
|
|
1214
|
+
const escaped = pat.replace(/[\\.+^$()|[\]{}]/g, "\\$&").replace(/\*\*/g, "__DOUBLESTAR__").replace(/\*/g, "[^/]*").replace(/__DOUBLESTAR__/g, ".*");
|
|
1215
|
+
const anchored = escaped.startsWith("~/") ? `(^|/)${escaped.slice(2)}$` : escaped.startsWith("/") ? `^${escaped}$` : `(^|/)${escaped}$`;
|
|
1216
|
+
return new RegExp(anchored);
|
|
1217
|
+
}
|
|
1218
|
+
var COMPILED_PATH_PATTERNS = UNBYPASSABLE_PATH_PATTERNS.map((pattern) => ({
|
|
1219
|
+
pattern,
|
|
1220
|
+
regex: compileGlobToRegex(pattern)
|
|
1221
|
+
}));
|
|
1222
|
+
function isPathDenied(path) {
|
|
1223
|
+
const normalized = path.toLowerCase();
|
|
1224
|
+
return COMPILED_PATH_PATTERNS.some((c) => c.regex.test(normalized));
|
|
1225
|
+
}
|
|
1226
|
+
function isToolDenied(toolName) {
|
|
1227
|
+
return UNBYPASSABLE_TOOL_NAMES.includes(toolName);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/security/access-constraint-deriver/tool-risk.ts
|
|
1231
|
+
var READ_ONLY_TOOLS2 = /* @__PURE__ */ new Set([
|
|
1232
|
+
// Discovery / listing
|
|
1233
|
+
"list_experts",
|
|
1234
|
+
"list_workflows",
|
|
1235
|
+
// Research reads
|
|
1236
|
+
"research_query",
|
|
1237
|
+
"research_analyze",
|
|
1238
|
+
"research_catalog_review",
|
|
1239
|
+
"research_synthesize",
|
|
1240
|
+
// Memory reads
|
|
1241
|
+
"memory_query",
|
|
1242
|
+
"memory_stats",
|
|
1243
|
+
// Observability
|
|
1244
|
+
"weather_report",
|
|
1245
|
+
"query_trace",
|
|
1246
|
+
"query_task_state",
|
|
1247
|
+
// Async-mode result polling (#3042 / epic #2631)
|
|
1248
|
+
"get_job_result",
|
|
1249
|
+
// Async-mode job discovery (#3046 / epic #2631 Stage 5)
|
|
1250
|
+
"list_jobs",
|
|
1251
|
+
// Codebase intelligence (read-only over local files)
|
|
1252
|
+
"search_codebase",
|
|
1253
|
+
"extract_symbols",
|
|
1254
|
+
// Repo analysis (read-only)
|
|
1255
|
+
"repo_analyze",
|
|
1256
|
+
"repo_security_plan",
|
|
1257
|
+
// Routing recommendation (no side effects — returns recommendation)
|
|
1258
|
+
"delegate_to_model",
|
|
1259
|
+
// Registry import (returns a draft template — does not write)
|
|
1260
|
+
"registry_import"
|
|
1261
|
+
]);
|
|
1262
|
+
function isRiskyTool(toolName) {
|
|
1263
|
+
return !READ_ONLY_TOOLS2.has(toolName);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/security/access-constraint-deriver/enforcer.ts
|
|
1267
|
+
function checkAccess(toolName, policy, args) {
|
|
1268
|
+
if (isToolDenied(toolName)) {
|
|
1269
|
+
return {
|
|
1270
|
+
decision: "deny",
|
|
1271
|
+
reason: `tool "${toolName}" is on the unbypassable deny-tool list`,
|
|
1272
|
+
matchedRule: "unbypassable:tool"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof args?.path === "string" && args.path.length > 0 && isPathDenied(args.path)) {
|
|
1276
|
+
return {
|
|
1277
|
+
decision: "deny",
|
|
1278
|
+
reason: `path "${args.path}" is on the unbypassable deny-path list`,
|
|
1279
|
+
matchedRule: "unbypassable:path"
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
if (policy.allowedTools === "*") return { decision: "allow" };
|
|
1283
|
+
if (policy.allowedTools.includes(toolName)) return { decision: "allow" };
|
|
1284
|
+
return decideOnViolation(toolName, policy.mode);
|
|
1285
|
+
}
|
|
1286
|
+
function decideOnViolation(toolName, mode) {
|
|
1287
|
+
if (mode === "audit") {
|
|
1288
|
+
return {
|
|
1289
|
+
decision: "log-and-allow",
|
|
1290
|
+
warning: `tool "${toolName}" not in derived policy (audit mode)`
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
if (mode === "confirm_risky") {
|
|
1294
|
+
if (!isRiskyTool(toolName)) {
|
|
1295
|
+
return {
|
|
1296
|
+
decision: "log-and-allow",
|
|
1297
|
+
warning: `tool "${toolName}" not in derived policy (confirm_risky mode, read-only \u2014 would have required human approval, allowed because read-only)`
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
decision: "deny",
|
|
1302
|
+
reason: `tool "${toolName}" not in derived policy (confirm_risky mode, risky \u2014 would have required human approval; denied for now, add to allowedTools or run in audit mode to allow)`,
|
|
1303
|
+
matchedRule: "allowedTools:confirm_risky"
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
decision: "deny",
|
|
1308
|
+
reason: `tool "${toolName}" not in derived policy`,
|
|
1309
|
+
matchedRule: "allowedTools"
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/security/access-constraint-deriver/mcp-guard.ts
|
|
1314
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
1315
|
+
var accessPolicyStorage = new AsyncLocalStorage2();
|
|
1316
|
+
function withAccessPolicy(policy, fn) {
|
|
1317
|
+
return accessPolicyStorage.run(policy, fn);
|
|
1318
|
+
}
|
|
1319
|
+
function getActivePolicy() {
|
|
1320
|
+
return accessPolicyStorage.getStore();
|
|
1321
|
+
}
|
|
1322
|
+
function denyToToolResult(decision, requestId) {
|
|
1323
|
+
return {
|
|
1324
|
+
isError: true,
|
|
1325
|
+
content: [
|
|
1326
|
+
{
|
|
1327
|
+
type: "text",
|
|
1328
|
+
text: `access denied: ${decision.reason} (rule: ${decision.matchedRule}, request: ${requestId})`
|
|
1329
|
+
}
|
|
1330
|
+
]
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/security/access-constraint-deriver/chain-adapter.ts
|
|
1335
|
+
function toGuardArgs(args) {
|
|
1336
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
1337
|
+
const path = args["path"];
|
|
1338
|
+
return typeof path === "string" && path.length > 0 ? { path } : void 0;
|
|
1339
|
+
}
|
|
1340
|
+
function createAccessPolicyChainMiddleware(toolName) {
|
|
1341
|
+
return async (args, ctx, next) => {
|
|
1342
|
+
const policy = getActivePolicy();
|
|
1343
|
+
if (policy === void 0 || policy.mode === "off") {
|
|
1344
|
+
return next(args, ctx);
|
|
1345
|
+
}
|
|
1346
|
+
const decision = checkAccess(toolName, policy, toGuardArgs(args));
|
|
1347
|
+
if (decision.decision === "allow") {
|
|
1348
|
+
return next(args, ctx);
|
|
1349
|
+
}
|
|
1350
|
+
if (decision.decision === "log-and-allow") {
|
|
1351
|
+
ctx.logger.warn("access-policy: audit violation", {
|
|
1352
|
+
tool: toolName,
|
|
1353
|
+
warning: decision.warning,
|
|
1354
|
+
policySource: policy.source,
|
|
1355
|
+
requestId: ctx.requestContext.requestId
|
|
1356
|
+
});
|
|
1357
|
+
return next(args, ctx);
|
|
1358
|
+
}
|
|
1359
|
+
ctx.logger.info("access-policy: tool call denied", {
|
|
1360
|
+
tool: toolName,
|
|
1361
|
+
reason: decision.reason,
|
|
1362
|
+
matchedRule: decision.matchedRule,
|
|
1363
|
+
policySource: policy.source,
|
|
1364
|
+
mode: policy.mode,
|
|
1365
|
+
requestId: ctx.requestContext.requestId
|
|
1366
|
+
});
|
|
1367
|
+
return denyToToolResult(decision, ctx.requestContext.requestId);
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/mcp/middleware/middleware-chain.ts
|
|
1372
|
+
function errorResult(category, message, requestId) {
|
|
1373
|
+
return toolStructuredError({
|
|
1374
|
+
errorCategory: category,
|
|
1375
|
+
message: `${message} (request: ${requestId})`
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
function createValidationMiddleware(schema) {
|
|
1379
|
+
return async (args, ctx, next) => {
|
|
1380
|
+
const result = validateToolInput(schema, args);
|
|
1381
|
+
if (!result.ok) {
|
|
1382
|
+
ctx.logger.warn("Validation failed", {
|
|
1383
|
+
error: result.error.message
|
|
1384
|
+
});
|
|
1385
|
+
return errorResult(
|
|
1386
|
+
"validation",
|
|
1387
|
+
`Validation error: ${result.error.message}`,
|
|
1388
|
+
ctx.requestContext.requestId
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
ctx.validatedArgs = result.value;
|
|
1392
|
+
return next(result.value, ctx);
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function createPolicyMiddleware(firewall, toolName, mode, allowedPaths) {
|
|
1396
|
+
return async (args, ctx, next) => {
|
|
1397
|
+
const policyCtx = createPolicyContext(toolName, args, {
|
|
1398
|
+
mode,
|
|
1399
|
+
...allowedPaths !== void 0 && { allowedPaths }
|
|
1400
|
+
});
|
|
1401
|
+
const decision = firewall.evaluate(policyCtx);
|
|
1402
|
+
if (!decision.allowed) {
|
|
1403
|
+
ctx.logger.warn("Policy denied", {
|
|
1404
|
+
reason: decision.reason,
|
|
1405
|
+
ruleName: decision.ruleName
|
|
1406
|
+
});
|
|
1407
|
+
return errorResult(
|
|
1408
|
+
"permission",
|
|
1409
|
+
`Policy denied: ${decision.reason}`,
|
|
1410
|
+
ctx.requestContext.requestId
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
ctx.logger.debug("Policy check passed", { reason: decision.reason });
|
|
1414
|
+
return next(args, ctx);
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
function createRateLimitMiddleware(limiter) {
|
|
1418
|
+
return async (args, ctx, next) => {
|
|
1419
|
+
const acquired = limiter.tryAcquire();
|
|
1420
|
+
if (!acquired) {
|
|
1421
|
+
const state = limiter.getState();
|
|
1422
|
+
ctx.logger.warn("Rate limit exceeded", {
|
|
1423
|
+
nextTokenMs: state.nextTokenMs
|
|
1424
|
+
});
|
|
1425
|
+
return errorResult(
|
|
1426
|
+
"transient",
|
|
1427
|
+
`Rate limit exceeded. Try again in ${String(state.nextTokenMs)}ms`,
|
|
1428
|
+
ctx.requestContext.requestId
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
return next(args, ctx);
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
function createTimeoutMiddleware(guard, toolName) {
|
|
1435
|
+
return async (args, ctx, next) => {
|
|
1436
|
+
const signal = abortSignalStorage.getStore();
|
|
1437
|
+
const result = await guard.execute(() => next(args, ctx), {
|
|
1438
|
+
operationName: toolName,
|
|
1439
|
+
...signal !== void 0 ? { signal } : {}
|
|
1440
|
+
});
|
|
1441
|
+
if (!result.ok) {
|
|
1442
|
+
ctx.logger.error("Operation timed out", void 0, {
|
|
1443
|
+
code: result.error.code,
|
|
1444
|
+
timeoutMs: result.error.timeoutMs
|
|
1445
|
+
});
|
|
1446
|
+
return errorResult("transient", result.error.message, ctx.requestContext.requestId);
|
|
1447
|
+
}
|
|
1448
|
+
if (result.value.nearTimeout) {
|
|
1449
|
+
ctx.logger.warn("Operation completed near timeout threshold", {
|
|
1450
|
+
durationMs: result.value.durationMs
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
return result.value.value;
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function createAuditMiddleware() {
|
|
1457
|
+
return async (args, ctx, next) => {
|
|
1458
|
+
const startTime = getTimeProvider().now();
|
|
1459
|
+
ctx.logger.info("Tool invocation started");
|
|
1460
|
+
try {
|
|
1461
|
+
const result = await next(args, ctx);
|
|
1462
|
+
const durationMs = getTimeProvider().now() - startTime;
|
|
1463
|
+
if (result.isError === true) {
|
|
1464
|
+
ctx.logger.warn("Tool execution completed with error", { durationMs });
|
|
1465
|
+
} else {
|
|
1466
|
+
ctx.logger.info("Tool execution completed", { durationMs });
|
|
1467
|
+
}
|
|
1468
|
+
return result;
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
const durationMs = getTimeProvider().now() - startTime;
|
|
1471
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1472
|
+
ctx.logger.error("Tool execution failed", error instanceof Error ? error : void 0, {
|
|
1473
|
+
durationMs
|
|
1474
|
+
});
|
|
1475
|
+
return errorResult("internal", `Internal error: ${message}`, ctx.requestContext.requestId);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function composeMiddleware(middlewares) {
|
|
1480
|
+
return (args, ctx, finalHandler) => {
|
|
1481
|
+
const dispatch = (index, currentArgs) => {
|
|
1482
|
+
if (index >= middlewares.length) {
|
|
1483
|
+
return finalHandler(currentArgs, ctx);
|
|
1484
|
+
}
|
|
1485
|
+
const middleware = middlewares[index];
|
|
1486
|
+
if (middleware === void 0) {
|
|
1487
|
+
return finalHandler(currentArgs, ctx);
|
|
1488
|
+
}
|
|
1489
|
+
return middleware(currentArgs, ctx, (nextArgs) => dispatch(index + 1, nextArgs));
|
|
1490
|
+
};
|
|
1491
|
+
return dispatch(0, args);
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function addAuditMiddleware(middlewares, skip) {
|
|
1495
|
+
if (skip.audit !== true) {
|
|
1496
|
+
middlewares.push(createAuditMiddleware());
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function addRateLimitMiddleware(middlewares, config, skip) {
|
|
1500
|
+
if (skip.rateLimit !== true && config.rateLimiter !== void 0) {
|
|
1501
|
+
const limiter = config.rateLimiter instanceof RateLimiter ? config.rateLimiter : new RateLimiter(config.rateLimiter);
|
|
1502
|
+
middlewares.push(createRateLimitMiddleware(limiter));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function addValidationMiddleware(middlewares, config, skip) {
|
|
1506
|
+
if (skip.validation !== true && config.schema !== void 0) {
|
|
1507
|
+
middlewares.push(createValidationMiddleware(config.schema));
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function addPolicyMiddleware(middlewares, config, skip) {
|
|
1511
|
+
if (skip.policy !== true && config.policyFirewall !== void 0) {
|
|
1512
|
+
const mode = config.executionMode ?? "read-only";
|
|
1513
|
+
middlewares.push(
|
|
1514
|
+
createPolicyMiddleware(config.policyFirewall, config.toolName, mode, config.allowedPaths)
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function addTimeoutMiddleware(middlewares, config, skip) {
|
|
1519
|
+
if (skip.timeout !== true && config.timeout !== void 0) {
|
|
1520
|
+
const guard = new TimeoutGuard(config.timeout);
|
|
1521
|
+
middlewares.push(createTimeoutMiddleware(guard, config.toolName));
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function addAccessPolicyMiddleware(middlewares, config, skip) {
|
|
1525
|
+
if (skip.accessPolicy !== true) {
|
|
1526
|
+
middlewares.push(createAccessPolicyChainMiddleware(config.toolName));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function buildMiddlewareStack(config) {
|
|
1530
|
+
const skip = config.skip ?? {};
|
|
1531
|
+
const middlewares = [];
|
|
1532
|
+
middlewares.push(createMetricsMiddleware());
|
|
1533
|
+
addAuditMiddleware(middlewares, skip);
|
|
1534
|
+
addRateLimitMiddleware(middlewares, config, skip);
|
|
1535
|
+
addValidationMiddleware(middlewares, config, skip);
|
|
1536
|
+
addPolicyMiddleware(middlewares, config, skip);
|
|
1537
|
+
addAccessPolicyMiddleware(middlewares, config, skip);
|
|
1538
|
+
addTimeoutMiddleware(middlewares, config, skip);
|
|
1539
|
+
return middlewares;
|
|
1540
|
+
}
|
|
1541
|
+
function createMiddlewareChain(config) {
|
|
1542
|
+
const logger = config.logger ?? createLogger({ tool: config.toolName });
|
|
1543
|
+
const middlewares = buildMiddlewareStack(config);
|
|
1544
|
+
const composed = composeMiddleware(middlewares);
|
|
1545
|
+
return (handler) => {
|
|
1546
|
+
return async (args) => {
|
|
1547
|
+
const requestContext = createRequestContext({ toolName: config.toolName });
|
|
1548
|
+
const requestLogger = logger.child(contextForLogging(requestContext));
|
|
1549
|
+
const ctx = { requestContext, logger: requestLogger };
|
|
1550
|
+
return composed(args, ctx, (finalArgs, finalCtx) => handler(finalArgs, finalCtx));
|
|
1551
|
+
};
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
function withMiddleware(toolName, handler, options) {
|
|
1555
|
+
const config = {
|
|
1556
|
+
toolName,
|
|
1557
|
+
...options
|
|
1558
|
+
};
|
|
1559
|
+
const chain = createMiddlewareChain(config);
|
|
1560
|
+
const wrappedHandler = (args, ctx) => {
|
|
1561
|
+
if (handler.length >= 2) {
|
|
1562
|
+
return handler(args, ctx);
|
|
1563
|
+
}
|
|
1564
|
+
return handler(args);
|
|
1565
|
+
};
|
|
1566
|
+
return chain(wrappedHandler);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/mcp/middleware/tool-wrapper.ts
|
|
1570
|
+
var DEFAULT_TIMEOUT_CONFIG = {
|
|
1571
|
+
defaultTimeoutMs: MCP_TIMEOUTS.defaultMs,
|
|
1572
|
+
maxTimeoutMs: MCP_TIMEOUTS.maxMs,
|
|
1573
|
+
enableLogging: true,
|
|
1574
|
+
uriValidation: true
|
|
1575
|
+
};
|
|
1576
|
+
var DEFAULT_TOOL_TIMEOUTS = {
|
|
1577
|
+
...MCP_TIMEOUTS.perTool
|
|
1578
|
+
};
|
|
1579
|
+
function getToolTimeout(toolName, security, explicitMs) {
|
|
1580
|
+
if (explicitMs !== void 0) {
|
|
1581
|
+
return explicitMs;
|
|
1582
|
+
}
|
|
1583
|
+
const perToolConfig = security?.timeout?.perToolTimeout;
|
|
1584
|
+
const perToolMs = perToolConfig?.[toolName];
|
|
1585
|
+
if (perToolMs !== void 0) {
|
|
1586
|
+
return perToolMs;
|
|
1587
|
+
}
|
|
1588
|
+
const builtInDefault = DEFAULT_TOOL_TIMEOUTS[toolName];
|
|
1589
|
+
if (builtInDefault !== void 0) {
|
|
1590
|
+
return builtInDefault;
|
|
1591
|
+
}
|
|
1592
|
+
return security?.timeout?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_CONFIG.defaultTimeoutMs;
|
|
1593
|
+
}
|
|
1594
|
+
function getTimeoutConfig(security, overrideMs) {
|
|
1595
|
+
const timeoutConfig = security?.timeout ?? DEFAULT_TIMEOUT_CONFIG;
|
|
1596
|
+
return {
|
|
1597
|
+
defaultTimeoutMs: overrideMs ?? timeoutConfig.defaultTimeoutMs,
|
|
1598
|
+
maxTimeoutMs: timeoutConfig.maxTimeoutMs,
|
|
1599
|
+
enableLogging: timeoutConfig.enableLogging
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function wrapToolWithTimeout(toolName, handler, options) {
|
|
1603
|
+
return withMiddleware(toolName, handler, {
|
|
1604
|
+
timeout: getTimeoutConfig(void 0, options?.timeoutMs),
|
|
1605
|
+
logger: options?.logger
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
var wrapperLogger = createLogger({ component: "tool-wrapper" });
|
|
1609
|
+
function extractProgressContext(extra) {
|
|
1610
|
+
const sdk = extra;
|
|
1611
|
+
const token = sdk?._meta?.progressToken;
|
|
1612
|
+
const sendFn = sdk?.sendNotification;
|
|
1613
|
+
if (token === void 0 || sendFn === void 0) return void 0;
|
|
1614
|
+
return {
|
|
1615
|
+
progressToken: token,
|
|
1616
|
+
sendNotification: (progress, total) => {
|
|
1617
|
+
const params = {
|
|
1618
|
+
progressToken: token,
|
|
1619
|
+
progress
|
|
1620
|
+
};
|
|
1621
|
+
if (total !== void 0) params["total"] = total;
|
|
1622
|
+
sendFn({ method: "notifications/progress", params }).catch((err2) => {
|
|
1623
|
+
wrapperLogger.debug("Failed to send progress notification", {
|
|
1624
|
+
error: getErrorMessage(err2)
|
|
1625
|
+
});
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function runWithContexts(handler, args, progressCtx, signal) {
|
|
1631
|
+
const run = () => handler(args);
|
|
1632
|
+
if (signal !== void 0 && progressCtx !== void 0) {
|
|
1633
|
+
return abortSignalStorage.run(signal, () => progressContextStorage.run(progressCtx, run));
|
|
1634
|
+
}
|
|
1635
|
+
if (signal !== void 0) {
|
|
1636
|
+
return abortSignalStorage.run(signal, run);
|
|
1637
|
+
}
|
|
1638
|
+
if (progressCtx !== void 0) {
|
|
1639
|
+
return progressContextStorage.run(progressCtx, run);
|
|
1640
|
+
}
|
|
1641
|
+
return run();
|
|
1642
|
+
}
|
|
1643
|
+
function toSdkCallback(handler) {
|
|
1644
|
+
return (args, extra) => {
|
|
1645
|
+
const progressCtx = extractProgressContext(extra);
|
|
1646
|
+
const signal = extra?.signal;
|
|
1647
|
+
return runWithContexts(handler, args, progressCtx, signal);
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
var MCP_SDK_DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
1651
|
+
var TIMEOUT_MISMATCH_TELEMETRY_REL_PATH = "mcp-telemetry/timeout-mismatch-events.jsonl";
|
|
1652
|
+
var ensuredDirs = /* @__PURE__ */ new Set();
|
|
1653
|
+
function appendTimeoutMismatchEvent(event) {
|
|
1654
|
+
try {
|
|
1655
|
+
const path = join(getNexusDataDir(), TIMEOUT_MISMATCH_TELEMETRY_REL_PATH);
|
|
1656
|
+
const dir = dirname(path);
|
|
1657
|
+
if (!ensuredDirs.has(dir)) {
|
|
1658
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1659
|
+
ensuredDirs.add(dir);
|
|
1660
|
+
}
|
|
1661
|
+
appendFileSync(path, JSON.stringify(event) + "\n", "utf-8");
|
|
1662
|
+
} catch (err2) {
|
|
1663
|
+
wrapperLogger.debug("Best-effort timeout-mismatch event recording failed", {
|
|
1664
|
+
error: getErrorMessage(err2)
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function extractErrorCategoryFromResult(result) {
|
|
1669
|
+
const envelope = result._meta?.["nexus-agents/error"];
|
|
1670
|
+
if (envelope !== null && typeof envelope === "object" && "errorCategory" in envelope) {
|
|
1671
|
+
const cat = envelope.errorCategory;
|
|
1672
|
+
if (typeof cat === "string") return cat;
|
|
1673
|
+
}
|
|
1674
|
+
return void 0;
|
|
1675
|
+
}
|
|
1676
|
+
function extractErrorMessageFromResult(result) {
|
|
1677
|
+
const first = result.content[0];
|
|
1678
|
+
if (first?.type === "text" && typeof first.text === "string") {
|
|
1679
|
+
return first.text.slice(0, 500);
|
|
1680
|
+
}
|
|
1681
|
+
return void 0;
|
|
1682
|
+
}
|
|
1683
|
+
function buildMismatchEvent(ctx, eventId, t0, outcome) {
|
|
1684
|
+
const t1 = Date.now();
|
|
1685
|
+
return {
|
|
1686
|
+
eventId,
|
|
1687
|
+
toolName: ctx.toolName,
|
|
1688
|
+
configuredTimeoutMs: ctx.configuredTimeoutMs,
|
|
1689
|
+
mcpSdkDefaultMs: MCP_SDK_DEFAULT_REQUEST_TIMEOUT_MS,
|
|
1690
|
+
startedAt: new Date(t0).toISOString(),
|
|
1691
|
+
endedAt: new Date(t1).toISOString(),
|
|
1692
|
+
durationMs: t1 - t0,
|
|
1693
|
+
outcome: outcome.outcome,
|
|
1694
|
+
...outcome.errorCategory !== void 0 ? { errorCategory: outcome.errorCategory } : {},
|
|
1695
|
+
...outcome.errorMessage !== void 0 ? { errorMessage: outcome.errorMessage } : {}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function classifyResult(result) {
|
|
1699
|
+
if (result.isError !== true) return { outcome: "success" };
|
|
1700
|
+
const errorCategory = extractErrorCategoryFromResult(result);
|
|
1701
|
+
const errorMessage = extractErrorMessageFromResult(result);
|
|
1702
|
+
return {
|
|
1703
|
+
outcome: "error",
|
|
1704
|
+
...errorCategory !== void 0 ? { errorCategory } : {},
|
|
1705
|
+
...errorMessage !== void 0 ? { errorMessage } : {}
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
async function runMismatchedCall(ctx) {
|
|
1709
|
+
const eventId = randomUUID();
|
|
1710
|
+
const t0 = Date.now();
|
|
1711
|
+
ctx.log.warn(
|
|
1712
|
+
"MCP tool budget exceeds client default and no progressToken received \u2014 request likely to be killed by client before server-side deadline",
|
|
1713
|
+
{
|
|
1714
|
+
tool: ctx.toolName,
|
|
1715
|
+
eventId,
|
|
1716
|
+
configuredTimeoutMs: ctx.configuredTimeoutMs,
|
|
1717
|
+
mcpSdkDefaultMs: MCP_SDK_DEFAULT_REQUEST_TIMEOUT_MS,
|
|
1718
|
+
remediation: "Client should pass `onprogress` and `resetTimeoutOnProgress: true` when calling, or extend `options.timeout`. See docs/architecture/MCP_PROTOCOL.md."
|
|
1719
|
+
}
|
|
1720
|
+
);
|
|
1721
|
+
try {
|
|
1722
|
+
const result = await runWithContexts(ctx.handler, ctx.args, ctx.progressCtx, ctx.signal);
|
|
1723
|
+
appendTimeoutMismatchEvent(buildMismatchEvent(ctx, eventId, t0, classifyResult(result)));
|
|
1724
|
+
return result;
|
|
1725
|
+
} catch (err2) {
|
|
1726
|
+
appendTimeoutMismatchEvent(
|
|
1727
|
+
buildMismatchEvent(ctx, eventId, t0, {
|
|
1728
|
+
outcome: "error",
|
|
1729
|
+
errorMessage: getErrorMessage(err2).slice(0, 500)
|
|
1730
|
+
})
|
|
1731
|
+
);
|
|
1732
|
+
throw err2;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function toSdkCallbackWithBudgetCheck(handler, toolName, configuredTimeoutMs, logger) {
|
|
1736
|
+
const log = logger ?? wrapperLogger;
|
|
1737
|
+
return (args, extra) => {
|
|
1738
|
+
const progressCtx = extractProgressContext(extra);
|
|
1739
|
+
const signal = extra?.signal;
|
|
1740
|
+
const isMismatch = configuredTimeoutMs > MCP_SDK_DEFAULT_REQUEST_TIMEOUT_MS && progressCtx === void 0;
|
|
1741
|
+
if (!isMismatch) return runWithContexts(handler, args, progressCtx, signal);
|
|
1742
|
+
return runMismatchedCall({
|
|
1743
|
+
log,
|
|
1744
|
+
handler,
|
|
1745
|
+
args,
|
|
1746
|
+
progressCtx,
|
|
1747
|
+
signal,
|
|
1748
|
+
toolName,
|
|
1749
|
+
configuredTimeoutMs
|
|
1750
|
+
});
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/mcp/tool-annotations.ts
|
|
1755
|
+
var TOOL_ANNOTATIONS = {
|
|
1756
|
+
// ============================================================================
|
|
1757
|
+
// Orchestration & expert lifecycle
|
|
1758
|
+
// ============================================================================
|
|
1759
|
+
/** Orchestrator coordinates expert agents; spawns workers that may mutate state. */
|
|
1760
|
+
orchestrate: {
|
|
1761
|
+
readOnlyHint: false,
|
|
1762
|
+
destructiveHint: false,
|
|
1763
|
+
idempotentHint: false,
|
|
1764
|
+
openWorldHint: true
|
|
1765
|
+
},
|
|
1766
|
+
/** Creates an expert and adds it to the in-memory expert registry. */
|
|
1767
|
+
create_expert: {
|
|
1768
|
+
readOnlyHint: false,
|
|
1769
|
+
destructiveHint: false,
|
|
1770
|
+
idempotentHint: false,
|
|
1771
|
+
openWorldHint: false
|
|
1772
|
+
},
|
|
1773
|
+
/** Executes a previously created expert; invokes external CLIs. */
|
|
1774
|
+
execute_expert: {
|
|
1775
|
+
readOnlyHint: false,
|
|
1776
|
+
destructiveHint: false,
|
|
1777
|
+
idempotentHint: false,
|
|
1778
|
+
openWorldHint: true
|
|
1779
|
+
},
|
|
1780
|
+
// ============================================================================
|
|
1781
|
+
// Workflow / pipeline execution
|
|
1782
|
+
// ============================================================================
|
|
1783
|
+
/** Runs a workflow template; steps may write to the registry or filesystem. */
|
|
1784
|
+
run_workflow: {
|
|
1785
|
+
readOnlyHint: false,
|
|
1786
|
+
destructiveHint: false,
|
|
1787
|
+
idempotentHint: false,
|
|
1788
|
+
openWorldHint: true
|
|
1789
|
+
},
|
|
1790
|
+
/** Runs a graph-based workflow with checkpointing. */
|
|
1791
|
+
run_graph_workflow: {
|
|
1792
|
+
readOnlyHint: false,
|
|
1793
|
+
destructiveHint: false,
|
|
1794
|
+
idempotentHint: false,
|
|
1795
|
+
openWorldHint: true
|
|
1796
|
+
},
|
|
1797
|
+
/** Runs a pipeline plugin by name. */
|
|
1798
|
+
run_pipeline: {
|
|
1799
|
+
readOnlyHint: false,
|
|
1800
|
+
destructiveHint: false,
|
|
1801
|
+
idempotentHint: false,
|
|
1802
|
+
openWorldHint: true
|
|
1803
|
+
},
|
|
1804
|
+
/** Full dev pipeline: research → plan → vote → implement → QA. */
|
|
1805
|
+
run_dev_pipeline: {
|
|
1806
|
+
readOnlyHint: false,
|
|
1807
|
+
destructiveHint: false,
|
|
1808
|
+
idempotentHint: false,
|
|
1809
|
+
openWorldHint: true
|
|
1810
|
+
},
|
|
1811
|
+
/** Executes an AI-software-factory spec pipeline. */
|
|
1812
|
+
execute_spec: {
|
|
1813
|
+
readOnlyHint: false,
|
|
1814
|
+
destructiveHint: false,
|
|
1815
|
+
idempotentHint: false,
|
|
1816
|
+
openWorldHint: true
|
|
1817
|
+
},
|
|
1818
|
+
// ============================================================================
|
|
1819
|
+
// Voting / decision
|
|
1820
|
+
// ============================================================================
|
|
1821
|
+
/** Records vote outcomes to the audit log; doesn't write to repo state. */
|
|
1822
|
+
consensus_vote: {
|
|
1823
|
+
readOnlyHint: false,
|
|
1824
|
+
destructiveHint: false,
|
|
1825
|
+
idempotentHint: false,
|
|
1826
|
+
openWorldHint: true
|
|
1827
|
+
},
|
|
1828
|
+
/** Per-axis tradeoff vote; records outcome to the store. */
|
|
1829
|
+
supply_chain_tradeoff_panel: {
|
|
1830
|
+
readOnlyHint: false,
|
|
1831
|
+
destructiveHint: false,
|
|
1832
|
+
idempotentHint: false,
|
|
1833
|
+
openWorldHint: true
|
|
1834
|
+
},
|
|
1835
|
+
/** Multi-voter PR review; may write review comments. */
|
|
1836
|
+
pr_review: {
|
|
1837
|
+
readOnlyHint: false,
|
|
1838
|
+
destructiveHint: false,
|
|
1839
|
+
idempotentHint: false,
|
|
1840
|
+
openWorldHint: true
|
|
1841
|
+
},
|
|
1842
|
+
// ============================================================================
|
|
1843
|
+
// Routing & introspection (read-only)
|
|
1844
|
+
// ============================================================================
|
|
1845
|
+
/** Returns a routing recommendation; doesn't execute. */
|
|
1846
|
+
delegate_to_model: {
|
|
1847
|
+
readOnlyHint: true,
|
|
1848
|
+
destructiveHint: false,
|
|
1849
|
+
idempotentHint: true,
|
|
1850
|
+
openWorldHint: false
|
|
1851
|
+
},
|
|
1852
|
+
/** Lists registered expert types. */
|
|
1853
|
+
list_experts: {
|
|
1854
|
+
readOnlyHint: true,
|
|
1855
|
+
destructiveHint: false,
|
|
1856
|
+
idempotentHint: true,
|
|
1857
|
+
openWorldHint: false
|
|
1858
|
+
},
|
|
1859
|
+
/** Lists registered workflow templates. */
|
|
1860
|
+
list_workflows: {
|
|
1861
|
+
readOnlyHint: true,
|
|
1862
|
+
destructiveHint: false,
|
|
1863
|
+
idempotentHint: true,
|
|
1864
|
+
openWorldHint: false
|
|
1865
|
+
},
|
|
1866
|
+
// ============================================================================
|
|
1867
|
+
// Research registry — reads
|
|
1868
|
+
// ============================================================================
|
|
1869
|
+
/** Queries the research registry. */
|
|
1870
|
+
research_query: {
|
|
1871
|
+
readOnlyHint: true,
|
|
1872
|
+
destructiveHint: false,
|
|
1873
|
+
idempotentHint: true,
|
|
1874
|
+
openWorldHint: false
|
|
1875
|
+
},
|
|
1876
|
+
/** Analyzes registry for gaps, trends, coverage. */
|
|
1877
|
+
research_analyze: {
|
|
1878
|
+
readOnlyHint: true,
|
|
1879
|
+
destructiveHint: false,
|
|
1880
|
+
idempotentHint: true,
|
|
1881
|
+
openWorldHint: false
|
|
1882
|
+
},
|
|
1883
|
+
/** Synthesizes registry into topic clusters. */
|
|
1884
|
+
research_synthesize: {
|
|
1885
|
+
readOnlyHint: true,
|
|
1886
|
+
destructiveHint: false,
|
|
1887
|
+
idempotentHint: true,
|
|
1888
|
+
openWorldHint: false
|
|
1889
|
+
},
|
|
1890
|
+
/** Discovers research items via external APIs; doesn't mutate registry. */
|
|
1891
|
+
research_discover: {
|
|
1892
|
+
readOnlyHint: true,
|
|
1893
|
+
destructiveHint: false,
|
|
1894
|
+
idempotentHint: true,
|
|
1895
|
+
openWorldHint: true
|
|
1896
|
+
},
|
|
1897
|
+
// ============================================================================
|
|
1898
|
+
// Research registry — mutations
|
|
1899
|
+
// ============================================================================
|
|
1900
|
+
/** Adds an arXiv paper to the registry. Dedup-checked but first call has effects. */
|
|
1901
|
+
research_add: {
|
|
1902
|
+
readOnlyHint: false,
|
|
1903
|
+
destructiveHint: false,
|
|
1904
|
+
idempotentHint: false,
|
|
1905
|
+
openWorldHint: true
|
|
1906
|
+
},
|
|
1907
|
+
/** Adds a non-paper source to the registry. */
|
|
1908
|
+
research_add_source: {
|
|
1909
|
+
readOnlyHint: false,
|
|
1910
|
+
destructiveHint: false,
|
|
1911
|
+
idempotentHint: false,
|
|
1912
|
+
openWorldHint: true
|
|
1913
|
+
},
|
|
1914
|
+
/** Reviews auto-cataloged research references; may approve/dismiss. */
|
|
1915
|
+
research_catalog_review: {
|
|
1916
|
+
readOnlyHint: false,
|
|
1917
|
+
destructiveHint: false,
|
|
1918
|
+
idempotentHint: false,
|
|
1919
|
+
openWorldHint: false
|
|
1920
|
+
},
|
|
1921
|
+
// ============================================================================
|
|
1922
|
+
// Transient lookups (no registry persistence)
|
|
1923
|
+
// ============================================================================
|
|
1924
|
+
/** Transient OSS search via GitHub API; no persistence. */
|
|
1925
|
+
survey_oss_landscape: {
|
|
1926
|
+
readOnlyHint: true,
|
|
1927
|
+
destructiveHint: false,
|
|
1928
|
+
idempotentHint: true,
|
|
1929
|
+
openWorldHint: true
|
|
1930
|
+
},
|
|
1931
|
+
/** Static lookup against curated vendor seed data. */
|
|
1932
|
+
vendor_publishing_audit: {
|
|
1933
|
+
readOnlyHint: true,
|
|
1934
|
+
destructiveHint: false,
|
|
1935
|
+
idempotentHint: true,
|
|
1936
|
+
openWorldHint: false
|
|
1937
|
+
},
|
|
1938
|
+
/** Diffs two YAML/JSON files; local-only. */
|
|
1939
|
+
compare_data_feeds: {
|
|
1940
|
+
readOnlyHint: true,
|
|
1941
|
+
destructiveHint: false,
|
|
1942
|
+
idempotentHint: true,
|
|
1943
|
+
openWorldHint: false
|
|
1944
|
+
},
|
|
1945
|
+
// ============================================================================
|
|
1946
|
+
// Memory
|
|
1947
|
+
// ============================================================================
|
|
1948
|
+
/** Reads from memory backends. */
|
|
1949
|
+
memory_query: {
|
|
1950
|
+
readOnlyHint: true,
|
|
1951
|
+
destructiveHint: false,
|
|
1952
|
+
idempotentHint: true,
|
|
1953
|
+
openWorldHint: false
|
|
1954
|
+
},
|
|
1955
|
+
/** Returns memory statistics. */
|
|
1956
|
+
memory_stats: {
|
|
1957
|
+
readOnlyHint: true,
|
|
1958
|
+
destructiveHint: false,
|
|
1959
|
+
idempotentHint: true,
|
|
1960
|
+
openWorldHint: false
|
|
1961
|
+
},
|
|
1962
|
+
/** Writes to a memory backend. */
|
|
1963
|
+
memory_write: {
|
|
1964
|
+
readOnlyHint: false,
|
|
1965
|
+
destructiveHint: false,
|
|
1966
|
+
idempotentHint: false,
|
|
1967
|
+
openWorldHint: false
|
|
1968
|
+
},
|
|
1969
|
+
// ============================================================================
|
|
1970
|
+
// Observability
|
|
1971
|
+
// ============================================================================
|
|
1972
|
+
/** Multi-CLI performance weather report; reads outcome store. */
|
|
1973
|
+
weather_report: {
|
|
1974
|
+
readOnlyHint: true,
|
|
1975
|
+
destructiveHint: false,
|
|
1976
|
+
idempotentHint: true,
|
|
1977
|
+
openWorldHint: false
|
|
1978
|
+
},
|
|
1979
|
+
/** Queries execution traces from disk. */
|
|
1980
|
+
query_trace: {
|
|
1981
|
+
readOnlyHint: true,
|
|
1982
|
+
destructiveHint: false,
|
|
1983
|
+
idempotentHint: true,
|
|
1984
|
+
openWorldHint: false
|
|
1985
|
+
},
|
|
1986
|
+
/** Queries structured task-state log. */
|
|
1987
|
+
query_task_state: {
|
|
1988
|
+
readOnlyHint: true,
|
|
1989
|
+
destructiveHint: false,
|
|
1990
|
+
idempotentHint: true,
|
|
1991
|
+
openWorldHint: false
|
|
1992
|
+
},
|
|
1993
|
+
/** Reads job-result sidecar file for an async-mode dispatch (#3042 / #2631). */
|
|
1994
|
+
get_job_result: {
|
|
1995
|
+
readOnlyHint: true,
|
|
1996
|
+
destructiveHint: false,
|
|
1997
|
+
idempotentHint: true,
|
|
1998
|
+
openWorldHint: false
|
|
1999
|
+
},
|
|
2000
|
+
/** Lists async-mode job summaries from the sidecar dir (#3046 / #2631 Stage 5). */
|
|
2001
|
+
list_jobs: {
|
|
2002
|
+
readOnlyHint: true,
|
|
2003
|
+
destructiveHint: false,
|
|
2004
|
+
idempotentHint: true,
|
|
2005
|
+
openWorldHint: false
|
|
2006
|
+
},
|
|
2007
|
+
/** Cancels an in-flight async-mode job (#3042 Stage 1b / #2631). */
|
|
2008
|
+
cancel_job: {
|
|
2009
|
+
// Mutates the sidecar record — NOT read-only.
|
|
2010
|
+
readOnlyHint: false,
|
|
2011
|
+
// Reversibility: cancel writes a new state, but doesn't delete data —
|
|
2012
|
+
// not destructive in the OWASP sense (no irreversible data loss).
|
|
2013
|
+
destructiveHint: false,
|
|
2014
|
+
// Idempotent: repeat cancels are no-ops with explicit outcome envelope.
|
|
2015
|
+
idempotentHint: true,
|
|
2016
|
+
openWorldHint: false
|
|
2017
|
+
},
|
|
2018
|
+
/** Verifies hash chain of audit log files. */
|
|
2019
|
+
verify_audit_chain: {
|
|
2020
|
+
readOnlyHint: true,
|
|
2021
|
+
destructiveHint: false,
|
|
2022
|
+
idempotentHint: true,
|
|
2023
|
+
openWorldHint: false
|
|
2024
|
+
},
|
|
2025
|
+
/** CI infrastructure health check — composes status-page + recent-runs signals (#3076). */
|
|
2026
|
+
ci_health_check: {
|
|
2027
|
+
readOnlyHint: true,
|
|
2028
|
+
destructiveHint: false,
|
|
2029
|
+
idempotentHint: true,
|
|
2030
|
+
// Hits external services (githubstatus.com + GitHub API) — `openWorldHint`.
|
|
2031
|
+
openWorldHint: true
|
|
2032
|
+
},
|
|
2033
|
+
/** Threshold-gated observability loop; may file GitHub issues when fileIssues=true. */
|
|
2034
|
+
improvement_review: {
|
|
2035
|
+
readOnlyHint: false,
|
|
2036
|
+
destructiveHint: false,
|
|
2037
|
+
idempotentHint: false,
|
|
2038
|
+
openWorldHint: true
|
|
2039
|
+
},
|
|
2040
|
+
// ============================================================================
|
|
2041
|
+
// Repository analysis
|
|
2042
|
+
// ============================================================================
|
|
2043
|
+
/** Analyzes a GitHub repository structure; doesn't write. */
|
|
2044
|
+
repo_analyze: {
|
|
2045
|
+
readOnlyHint: true,
|
|
2046
|
+
destructiveHint: false,
|
|
2047
|
+
idempotentHint: true,
|
|
2048
|
+
openWorldHint: true
|
|
2049
|
+
},
|
|
2050
|
+
/** Generates a security-scanning pipeline plan; returns plan, doesn't write it. */
|
|
2051
|
+
repo_security_plan: {
|
|
2052
|
+
readOnlyHint: true,
|
|
2053
|
+
destructiveHint: false,
|
|
2054
|
+
idempotentHint: true,
|
|
2055
|
+
openWorldHint: true
|
|
2056
|
+
},
|
|
2057
|
+
/** Extracts code symbols from source files. */
|
|
2058
|
+
extract_symbols: {
|
|
2059
|
+
readOnlyHint: true,
|
|
2060
|
+
destructiveHint: false,
|
|
2061
|
+
idempotentHint: true,
|
|
2062
|
+
openWorldHint: false
|
|
2063
|
+
},
|
|
2064
|
+
/** Searches codebase for patterns or symbols. */
|
|
2065
|
+
search_codebase: {
|
|
2066
|
+
readOnlyHint: true,
|
|
2067
|
+
destructiveHint: false,
|
|
2068
|
+
idempotentHint: true,
|
|
2069
|
+
openWorldHint: false
|
|
2070
|
+
},
|
|
2071
|
+
// ============================================================================
|
|
2072
|
+
// Issue triage & registry imports
|
|
2073
|
+
// ============================================================================
|
|
2074
|
+
/** Triages a GitHub issue; may write labels/comments when authorized. */
|
|
2075
|
+
issue_triage: {
|
|
2076
|
+
readOnlyHint: false,
|
|
2077
|
+
destructiveHint: false,
|
|
2078
|
+
idempotentHint: false,
|
|
2079
|
+
openWorldHint: true
|
|
2080
|
+
},
|
|
2081
|
+
/** Generates a draft model-registry entry; doesn't write. */
|
|
2082
|
+
registry_import: {
|
|
2083
|
+
readOnlyHint: true,
|
|
2084
|
+
destructiveHint: false,
|
|
2085
|
+
idempotentHint: true,
|
|
2086
|
+
openWorldHint: true
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
function getToolAnnotations(name) {
|
|
2090
|
+
const a = TOOL_ANNOTATIONS[name];
|
|
2091
|
+
if (a === void 0) {
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
`getToolAnnotations: no entry for tool "${name}". Add it to TOOL_ANNOTATIONS in src/mcp/tool-annotations.ts (#2648).`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
return a;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
export {
|
|
2100
|
+
validateToolInput,
|
|
2101
|
+
createValidator,
|
|
2102
|
+
RateLimiter,
|
|
2103
|
+
createDefaultRateLimiter,
|
|
2104
|
+
PolicyError,
|
|
2105
|
+
PolicyConfigSchema,
|
|
2106
|
+
denyMutationsWithoutModeRule,
|
|
2107
|
+
safePathsRule,
|
|
2108
|
+
PolicyFirewall,
|
|
2109
|
+
createDefaultPolicyFirewall,
|
|
2110
|
+
evaluatePolicy,
|
|
2111
|
+
createPolicyContext,
|
|
2112
|
+
actorFromContext,
|
|
2113
|
+
resultToOutcome,
|
|
2114
|
+
logToolInvocationAudit,
|
|
2115
|
+
logPolicyAudit,
|
|
2116
|
+
logRateLimitAudit,
|
|
2117
|
+
toolSuccess,
|
|
2118
|
+
toolSuccessStructured,
|
|
2119
|
+
toolStructuredError,
|
|
2120
|
+
toolError,
|
|
2121
|
+
createSecureHandler,
|
|
2122
|
+
createMcpNotifier,
|
|
2123
|
+
NOOP_NOTIFIER,
|
|
2124
|
+
abortSignalStorage,
|
|
2125
|
+
withProgressHeartbeat,
|
|
2126
|
+
withAccessPolicy,
|
|
2127
|
+
getToolTimeout,
|
|
2128
|
+
wrapToolWithTimeout,
|
|
2129
|
+
toSdkCallback,
|
|
2130
|
+
toSdkCallbackWithBudgetCheck,
|
|
2131
|
+
getToolAnnotations
|
|
2132
|
+
};
|
|
2133
|
+
//# sourceMappingURL=chunk-5LGAPYX7.js.map
|