@stackwright-pro/mcp 0.2.0-alpha.10 → 0.2.0-alpha.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/integrity.d.mts +30 -1
- package/dist/integrity.d.ts +30 -1
- package/dist/integrity.js +77 -12
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +75 -12
- package/dist/integrity.mjs.map +1 -1
- package/dist/pipeline-constants.d.mts +30 -0
- package/dist/pipeline-constants.d.ts +30 -0
- package/dist/pipeline-constants.js +446 -0
- package/dist/pipeline-constants.js.map +1 -0
- package/dist/pipeline-constants.mjs +430 -0
- package/dist/pipeline-constants.mjs.map +1 -0
- package/dist/server.js +4759 -599
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +4873 -663
- package/dist/server.mjs.map +1 -1
- package/dist/tools/type-schemas.d.mts +51 -0
- package/dist/tools/type-schemas.d.ts +51 -0
- package/dist/tools/type-schemas.js +120 -0
- package/dist/tools/type-schemas.js.map +1 -0
- package/dist/tools/type-schemas.mjs +94 -0
- package/dist/tools/type-schemas.mjs.map +1 -0
- package/package.json +32 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Per Aspera LLC. All Rights Reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are the
|
|
6
|
+
proprietary and confidential property of Per Aspera LLC ("Company").
|
|
7
|
+
|
|
8
|
+
RESTRICTIONS: You may not use, copy, modify, merge, publish, distribute,
|
|
9
|
+
sublicense, sell, or otherwise exploit this Software or any portion thereof
|
|
10
|
+
without the express prior written consent of the Company.
|
|
11
|
+
|
|
12
|
+
GOVERNMENT USE: Use, duplication, or disclosure by the U.S. Government is
|
|
13
|
+
subject to restrictions as set forth in FAR 52.227-19 (Commercial Computer
|
|
14
|
+
Software - Restricted Rights) and DFARS 252.227-7013 (Rights in Technical
|
|
15
|
+
Data and Computer Software), as applicable.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED. IN NO EVENT SHALL THE COMPANY BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
19
|
+
OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
For licensing inquiries: legal@peraspera.com
|
package/dist/integrity.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
|
|
3
|
+
declare const CANONICAL_OTTERS: readonly string[];
|
|
3
4
|
/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */
|
|
4
5
|
declare function computeSha256(data: Buffer): string;
|
|
5
6
|
interface VerifyOtterFileResult {
|
|
@@ -27,6 +28,34 @@ interface VerifyAllOttersResult {
|
|
|
27
28
|
* canonical checksums. Returns lists of verified, failed, and unknown files.
|
|
28
29
|
*/
|
|
29
30
|
declare function verifyAllOtters(otterDir: string): VerifyAllOttersResult;
|
|
31
|
+
/**
|
|
32
|
+
* Structured audit event parameters for an integrity failure.
|
|
33
|
+
* Kept as a plain interface so callers can pass partial results without
|
|
34
|
+
* constructing the full VerifyAllOttersResult.
|
|
35
|
+
*/
|
|
36
|
+
interface IntegrityAuditEvent {
|
|
37
|
+
otterDir: string;
|
|
38
|
+
failed: Array<{
|
|
39
|
+
filename: string;
|
|
40
|
+
error: string;
|
|
41
|
+
}>;
|
|
42
|
+
unknown: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Emit a structured INTEGRITY_FAIL audit event to stderr.
|
|
46
|
+
*
|
|
47
|
+
* Writes a single line to process.stderr in the format:
|
|
48
|
+
* INTEGRITY_FAIL {"level":"AUDIT","event":"INTEGRITY_FAIL",...}
|
|
49
|
+
*
|
|
50
|
+
* The line prefix "INTEGRITY_FAIL" (without JSON) allows log shippers
|
|
51
|
+
* (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the
|
|
52
|
+
* event to a dedicated audit stream using a simple string filter, even
|
|
53
|
+
* before attempting JSON parsing.
|
|
54
|
+
*
|
|
55
|
+
* Exported for unit testing. Do not call directly in production code —
|
|
56
|
+
* use registerIntegrityTools() which calls this automatically.
|
|
57
|
+
*/
|
|
58
|
+
declare function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void;
|
|
30
59
|
declare function registerIntegrityTools(server: McpServer): void;
|
|
31
60
|
|
|
32
|
-
export { type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
|
|
61
|
+
export { CANONICAL_OTTERS, type IntegrityAuditEvent, type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, emitIntegrityAuditEvent, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
|
package/dist/integrity.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
|
|
3
|
+
declare const CANONICAL_OTTERS: readonly string[];
|
|
3
4
|
/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */
|
|
4
5
|
declare function computeSha256(data: Buffer): string;
|
|
5
6
|
interface VerifyOtterFileResult {
|
|
@@ -27,6 +28,34 @@ interface VerifyAllOttersResult {
|
|
|
27
28
|
* canonical checksums. Returns lists of verified, failed, and unknown files.
|
|
28
29
|
*/
|
|
29
30
|
declare function verifyAllOtters(otterDir: string): VerifyAllOttersResult;
|
|
31
|
+
/**
|
|
32
|
+
* Structured audit event parameters for an integrity failure.
|
|
33
|
+
* Kept as a plain interface so callers can pass partial results without
|
|
34
|
+
* constructing the full VerifyAllOttersResult.
|
|
35
|
+
*/
|
|
36
|
+
interface IntegrityAuditEvent {
|
|
37
|
+
otterDir: string;
|
|
38
|
+
failed: Array<{
|
|
39
|
+
filename: string;
|
|
40
|
+
error: string;
|
|
41
|
+
}>;
|
|
42
|
+
unknown: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Emit a structured INTEGRITY_FAIL audit event to stderr.
|
|
46
|
+
*
|
|
47
|
+
* Writes a single line to process.stderr in the format:
|
|
48
|
+
* INTEGRITY_FAIL {"level":"AUDIT","event":"INTEGRITY_FAIL",...}
|
|
49
|
+
*
|
|
50
|
+
* The line prefix "INTEGRITY_FAIL" (without JSON) allows log shippers
|
|
51
|
+
* (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the
|
|
52
|
+
* event to a dedicated audit stream using a simple string filter, even
|
|
53
|
+
* before attempting JSON parsing.
|
|
54
|
+
*
|
|
55
|
+
* Exported for unit testing. Do not call directly in production code —
|
|
56
|
+
* use registerIntegrityTools() which calls this automatically.
|
|
57
|
+
*/
|
|
58
|
+
declare function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void;
|
|
30
59
|
declare function registerIntegrityTools(server: McpServer): void;
|
|
31
60
|
|
|
32
|
-
export { type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
|
|
61
|
+
export { CANONICAL_OTTERS, type IntegrityAuditEvent, type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, emitIntegrityAuditEvent, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
|
package/dist/integrity.js
CHANGED
|
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/integrity.ts
|
|
21
21
|
var integrity_exports = {};
|
|
22
22
|
__export(integrity_exports, {
|
|
23
|
+
CANONICAL_OTTERS: () => CANONICAL_OTTERS,
|
|
23
24
|
computeSha256: () => computeSha256,
|
|
25
|
+
emitIntegrityAuditEvent: () => emitIntegrityAuditEvent,
|
|
24
26
|
registerIntegrityTools: () => registerIntegrityTools,
|
|
25
27
|
verifyAllOtters: () => verifyAllOtters,
|
|
26
28
|
verifyOtterFile: () => verifyOtterFile
|
|
@@ -32,39 +34,63 @@ var import_path = require("path");
|
|
|
32
34
|
var _checksums = /* @__PURE__ */ new Map([
|
|
33
35
|
[
|
|
34
36
|
"stackwright-pro-api-otter.json",
|
|
35
|
-
"
|
|
37
|
+
"175cca39127beea84ed8f1b264b8cd5cf58733db050d32328c039cca0cf0b981"
|
|
36
38
|
],
|
|
37
39
|
[
|
|
38
40
|
"stackwright-pro-auth-otter.json",
|
|
39
|
-
"
|
|
41
|
+
"1a21d9f23e250f23291124307e5a2a32af5a716566319a3290372dae3d02e637"
|
|
40
42
|
],
|
|
41
43
|
[
|
|
42
44
|
"stackwright-pro-dashboard-otter.json",
|
|
43
|
-
"
|
|
45
|
+
"6918b51d1007c69e635f252b20b82774fa7c942fa1d2399769c14f8d1df4df70"
|
|
44
46
|
],
|
|
45
47
|
[
|
|
46
48
|
"stackwright-pro-data-otter.json",
|
|
47
|
-
"
|
|
49
|
+
"1ad3ed99bbe7b550f654c679a8c0ea3363b2c52031042cd177c6e5f9e1c50a21"
|
|
48
50
|
],
|
|
49
51
|
[
|
|
50
52
|
"stackwright-pro-designer-otter.json",
|
|
51
|
-
"
|
|
53
|
+
"69a6c09fbb54f971c48eae7fd52faa63cf79be2923e435aca9ff802e8d541d0f"
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"stackwright-pro-domain-expert-otter.json",
|
|
57
|
+
"14d77cade020f44d1b5a2b406e2599501f3466ee90ca4248500a441d419bc59c"
|
|
52
58
|
],
|
|
53
59
|
[
|
|
54
60
|
"stackwright-pro-foreman-otter.json",
|
|
55
|
-
"
|
|
61
|
+
"9191e176241c15f3d77180e1b843cc3a517b708737b5bd3cda7db62f2d6675d1"
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"stackwright-pro-form-wizard-otter.json",
|
|
65
|
+
"7f6f94192f26face57dc8b2e22e0a49d23ceeeb356ca4ebde05492f1b25e3c47"
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"stackwright-pro-geo-otter.json",
|
|
69
|
+
"fc3d18e02a6147d95d3dd9093ce74c0e8fffaecc2f9ecdbd19c163a80129d264"
|
|
56
70
|
],
|
|
57
71
|
[
|
|
58
72
|
"stackwright-pro-page-otter.json",
|
|
59
|
-
"
|
|
73
|
+
"879e8ff4aa645bac2c63dc962cad1a549876983c257aec756e95293c3b22ce7e"
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"stackwright-pro-polish-otter.json",
|
|
77
|
+
"9d0c53651b60507607efc0d9a0f9d166bf5ae8571f1645682e0b151d42c276ab"
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"stackwright-pro-qa-otter.json",
|
|
81
|
+
"8e6007e18687b6b023f2c40f5517937a857da3f31e4e423cc308493ed601793c"
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"stackwright-pro-scaffold-otter.json",
|
|
85
|
+
"5756ea77178334684b4df1d9d3395bca62cedb4991ab07dc4cf61aa7e540ea14"
|
|
60
86
|
],
|
|
61
87
|
[
|
|
62
88
|
"stackwright-pro-theme-otter.json",
|
|
63
|
-
"
|
|
89
|
+
"b0eccc1945e30b80a0f9a3209c5428e8e32cd09063bd9b2ef7ecb891343774f2"
|
|
64
90
|
],
|
|
65
91
|
[
|
|
66
|
-
"stackwright-
|
|
67
|
-
"
|
|
92
|
+
"stackwright-services-otter.json",
|
|
93
|
+
"2a50ceb3fad166251d0ad8709a34a32118645713e0e34628165d16dced1d5c93"
|
|
68
94
|
]
|
|
69
95
|
]);
|
|
70
96
|
Object.freeze(_checksums);
|
|
@@ -77,6 +103,7 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
|
|
|
77
103
|
);
|
|
78
104
|
}
|
|
79
105
|
}
|
|
106
|
+
var CANONICAL_OTTERS = Object.freeze([...CANONICAL_CHECKSUMS.keys()]);
|
|
80
107
|
var MAX_OTTER_BYTES = 1 * 1024 * 1024;
|
|
81
108
|
function computeSha256(data) {
|
|
82
109
|
return (0, import_crypto.createHash)("sha256").update(data).digest("hex");
|
|
@@ -144,6 +171,18 @@ function verifyOtterFile(filePath) {
|
|
|
144
171
|
return { verified: true, filename };
|
|
145
172
|
}
|
|
146
173
|
function verifyAllOtters(otterDir) {
|
|
174
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
|
|
175
|
+
return {
|
|
176
|
+
verified: [],
|
|
177
|
+
failed: [
|
|
178
|
+
{
|
|
179
|
+
filename: "<directory>",
|
|
180
|
+
error: `Security: path traversal sequence detected in otter directory parameter`
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
unknown: []
|
|
184
|
+
};
|
|
185
|
+
}
|
|
147
186
|
const verified = [];
|
|
148
187
|
const failed = [];
|
|
149
188
|
const unknown = [];
|
|
@@ -197,6 +236,21 @@ function resolveOtterDir() {
|
|
|
197
236
|
}
|
|
198
237
|
return null;
|
|
199
238
|
}
|
|
239
|
+
function emitIntegrityAuditEvent(params) {
|
|
240
|
+
const record = JSON.stringify({
|
|
241
|
+
level: "AUDIT",
|
|
242
|
+
event: "INTEGRITY_FAIL",
|
|
243
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
244
|
+
source: "stackwright_pro_verify_otter_integrity",
|
|
245
|
+
otterDir: params.otterDir,
|
|
246
|
+
failedCount: params.failed.length,
|
|
247
|
+
unknownCount: params.unknown.length,
|
|
248
|
+
failures: params.failed,
|
|
249
|
+
unknown: params.unknown
|
|
250
|
+
});
|
|
251
|
+
process.stderr.write(`INTEGRITY_FAIL ${record}
|
|
252
|
+
`);
|
|
253
|
+
}
|
|
200
254
|
function registerIntegrityTools(server) {
|
|
201
255
|
server.tool(
|
|
202
256
|
"stackwright_pro_verify_otter_integrity",
|
|
@@ -220,6 +274,13 @@ function registerIntegrityTools(server) {
|
|
|
220
274
|
}
|
|
221
275
|
const result = verifyAllOtters(resolved);
|
|
222
276
|
const allGood = result.failed.length === 0 && result.unknown.length === 0;
|
|
277
|
+
if (!allGood) {
|
|
278
|
+
emitIntegrityAuditEvent({
|
|
279
|
+
otterDir: resolved,
|
|
280
|
+
failed: result.failed,
|
|
281
|
+
unknown: result.unknown
|
|
282
|
+
});
|
|
283
|
+
}
|
|
223
284
|
return {
|
|
224
285
|
content: [
|
|
225
286
|
{
|
|
@@ -233,18 +294,22 @@ function registerIntegrityTools(server) {
|
|
|
233
294
|
verified: result.verified,
|
|
234
295
|
failed: result.failed,
|
|
235
296
|
unknown: result.unknown,
|
|
236
|
-
|
|
297
|
+
...allGood ? {} : {
|
|
298
|
+
error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
|
|
299
|
+
}
|
|
237
300
|
})
|
|
238
301
|
}
|
|
239
302
|
],
|
|
240
|
-
isError:
|
|
303
|
+
isError: !allGood
|
|
241
304
|
};
|
|
242
305
|
}
|
|
243
306
|
);
|
|
244
307
|
}
|
|
245
308
|
// Annotate the CommonJS export names for ESM import in node:
|
|
246
309
|
0 && (module.exports = {
|
|
310
|
+
CANONICAL_OTTERS,
|
|
247
311
|
computeSha256,
|
|
312
|
+
emitIntegrityAuditEvent,
|
|
248
313
|
registerIntegrityTools,
|
|
249
314
|
verifyAllOtters,
|
|
250
315
|
verifyOtterFile
|
package/dist/integrity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n 'a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n 'b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001',\n ],\n [\n 'stackwright-pro-data-otter.json',\n 'bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '27f4dfd4676246c9fea14fd1de4f148928eeaf06e58b08240761fd3e663dd540',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n '64ffaeeceacd739922788a1d074f6feaffc3f91d09706c2c104f0c0281677732',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n warning:\n result.failed.length > 0\n ? 'SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon.'\n : undefined,\n }),\n },\n ],\n isError: false,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n '175cca39127beea84ed8f1b264b8cd5cf58733db050d32328c039cca0cf0b981',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '1a21d9f23e250f23291124307e5a2a32af5a716566319a3290372dae3d02e637',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '6918b51d1007c69e635f252b20b82774fa7c942fa1d2399769c14f8d1df4df70',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '1ad3ed99bbe7b550f654c679a8c0ea3363b2c52031042cd177c6e5f9e1c50a21',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n '69a6c09fbb54f971c48eae7fd52faa63cf79be2923e435aca9ff802e8d541d0f',\n ],\n [\n 'stackwright-pro-domain-expert-otter.json',\n '14d77cade020f44d1b5a2b406e2599501f3466ee90ca4248500a441d419bc59c',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '9191e176241c15f3d77180e1b843cc3a517b708737b5bd3cda7db62f2d6675d1',\n ],\n [\n 'stackwright-pro-form-wizard-otter.json',\n '7f6f94192f26face57dc8b2e22e0a49d23ceeeb356ca4ebde05492f1b25e3c47',\n ],\n [\n 'stackwright-pro-geo-otter.json',\n 'fc3d18e02a6147d95d3dd9093ce74c0e8fffaecc2f9ecdbd19c163a80129d264',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '879e8ff4aa645bac2c63dc962cad1a549876983c257aec756e95293c3b22ce7e',\n ],\n [\n 'stackwright-pro-polish-otter.json',\n '9d0c53651b60507607efc0d9a0f9d166bf5ae8571f1645682e0b151d42c276ab',\n ],\n [\n 'stackwright-pro-qa-otter.json',\n '8e6007e18687b6b023f2c40f5517937a857da3f31e4e423cc308493ed601793c',\n ],\n [\n 'stackwright-pro-scaffold-otter.json',\n '5756ea77178334684b4df1d9d3395bca62cedb4991ab07dc4cf61aa7e540ea14',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'b0eccc1945e30b80a0f9a3209c5428e8e32cd09063bd9b2ef7ecb891343774f2',\n ],\n [\n 'stackwright-services-otter.json',\n '2a50ceb3fad166251d0ad8709a34a32118645713e0e34628165d16dced1d5c93',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// Exported for test assertions — derive counts from here, not hardcoded numbers.\nexport const CANONICAL_OTTERS: readonly string[] = Object.freeze([...CANONICAL_CHECKSUMS.keys()]);\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n // ---------------------------------------------------------------------------\n // Path traversal guard — reject any input containing \"..\" sequences before\n // any I/O. An attacker controlling this parameter could otherwise scan\n // directories outside the expected otter install locations.\n // ---------------------------------------------------------------------------\n if (/(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/.test(otterDir) || otterDir.includes('..')) {\n return {\n verified: [],\n failed: [\n {\n filename: '<directory>',\n error: `Security: path traversal sequence detected in otter directory parameter`,\n },\n ],\n unknown: [],\n };\n }\n\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Audit stream — SOC2 / FedRAMP / DoD ATO compliance\n// ---------------------------------------------------------------------------\n\n/**\n * Structured audit event parameters for an integrity failure.\n * Kept as a plain interface so callers can pass partial results without\n * constructing the full VerifyAllOttersResult.\n */\nexport interface IntegrityAuditEvent {\n otterDir: string;\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Emit a structured INTEGRITY_FAIL audit event to stderr.\n *\n * Writes a single line to process.stderr in the format:\n * INTEGRITY_FAIL {\"level\":\"AUDIT\",\"event\":\"INTEGRITY_FAIL\",...}\n *\n * The line prefix \"INTEGRITY_FAIL\" (without JSON) allows log shippers\n * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the\n * event to a dedicated audit stream using a simple string filter, even\n * before attempting JSON parsing.\n *\n * Exported for unit testing. Do not call directly in production code —\n * use registerIntegrityTools() which calls this automatically.\n */\nexport function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void {\n const record = JSON.stringify({\n level: 'AUDIT',\n event: 'INTEGRITY_FAIL',\n timestamp: new Date().toISOString(),\n source: 'stackwright_pro_verify_otter_integrity',\n otterDir: params.otterDir,\n failedCount: params.failed.length,\n unknownCount: params.unknown.length,\n failures: params.failed,\n unknown: params.unknown,\n });\n process.stderr.write(`INTEGRITY_FAIL ${record}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n // Emit to dedicated audit stream for SOC2/FedRAMP/DoD ATO compliance\n if (!allGood) {\n emitIntegrityAuditEvent({\n otterDir: resolved,\n failed: result.failed,\n unknown: result.unknown,\n });\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n ...(allGood\n ? {}\n : {\n error:\n 'INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed — otter files may have been tampered with.',\n }),\n }),\n },\n ],\n isError: !allGood,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/integrity.mjs
CHANGED
|
@@ -5,39 +5,63 @@ import { join, basename } from "path";
|
|
|
5
5
|
var _checksums = /* @__PURE__ */ new Map([
|
|
6
6
|
[
|
|
7
7
|
"stackwright-pro-api-otter.json",
|
|
8
|
-
"
|
|
8
|
+
"175cca39127beea84ed8f1b264b8cd5cf58733db050d32328c039cca0cf0b981"
|
|
9
9
|
],
|
|
10
10
|
[
|
|
11
11
|
"stackwright-pro-auth-otter.json",
|
|
12
|
-
"
|
|
12
|
+
"1a21d9f23e250f23291124307e5a2a32af5a716566319a3290372dae3d02e637"
|
|
13
13
|
],
|
|
14
14
|
[
|
|
15
15
|
"stackwright-pro-dashboard-otter.json",
|
|
16
|
-
"
|
|
16
|
+
"6918b51d1007c69e635f252b20b82774fa7c942fa1d2399769c14f8d1df4df70"
|
|
17
17
|
],
|
|
18
18
|
[
|
|
19
19
|
"stackwright-pro-data-otter.json",
|
|
20
|
-
"
|
|
20
|
+
"1ad3ed99bbe7b550f654c679a8c0ea3363b2c52031042cd177c6e5f9e1c50a21"
|
|
21
21
|
],
|
|
22
22
|
[
|
|
23
23
|
"stackwright-pro-designer-otter.json",
|
|
24
|
-
"
|
|
24
|
+
"69a6c09fbb54f971c48eae7fd52faa63cf79be2923e435aca9ff802e8d541d0f"
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
"stackwright-pro-domain-expert-otter.json",
|
|
28
|
+
"14d77cade020f44d1b5a2b406e2599501f3466ee90ca4248500a441d419bc59c"
|
|
25
29
|
],
|
|
26
30
|
[
|
|
27
31
|
"stackwright-pro-foreman-otter.json",
|
|
28
|
-
"
|
|
32
|
+
"9191e176241c15f3d77180e1b843cc3a517b708737b5bd3cda7db62f2d6675d1"
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
"stackwright-pro-form-wizard-otter.json",
|
|
36
|
+
"7f6f94192f26face57dc8b2e22e0a49d23ceeeb356ca4ebde05492f1b25e3c47"
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"stackwright-pro-geo-otter.json",
|
|
40
|
+
"fc3d18e02a6147d95d3dd9093ce74c0e8fffaecc2f9ecdbd19c163a80129d264"
|
|
29
41
|
],
|
|
30
42
|
[
|
|
31
43
|
"stackwright-pro-page-otter.json",
|
|
32
|
-
"
|
|
44
|
+
"879e8ff4aa645bac2c63dc962cad1a549876983c257aec756e95293c3b22ce7e"
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"stackwright-pro-polish-otter.json",
|
|
48
|
+
"9d0c53651b60507607efc0d9a0f9d166bf5ae8571f1645682e0b151d42c276ab"
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"stackwright-pro-qa-otter.json",
|
|
52
|
+
"8e6007e18687b6b023f2c40f5517937a857da3f31e4e423cc308493ed601793c"
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"stackwright-pro-scaffold-otter.json",
|
|
56
|
+
"5756ea77178334684b4df1d9d3395bca62cedb4991ab07dc4cf61aa7e540ea14"
|
|
33
57
|
],
|
|
34
58
|
[
|
|
35
59
|
"stackwright-pro-theme-otter.json",
|
|
36
|
-
"
|
|
60
|
+
"b0eccc1945e30b80a0f9a3209c5428e8e32cd09063bd9b2ef7ecb891343774f2"
|
|
37
61
|
],
|
|
38
62
|
[
|
|
39
|
-
"stackwright-
|
|
40
|
-
"
|
|
63
|
+
"stackwright-services-otter.json",
|
|
64
|
+
"2a50ceb3fad166251d0ad8709a34a32118645713e0e34628165d16dced1d5c93"
|
|
41
65
|
]
|
|
42
66
|
]);
|
|
43
67
|
Object.freeze(_checksums);
|
|
@@ -50,6 +74,7 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
|
|
|
50
74
|
);
|
|
51
75
|
}
|
|
52
76
|
}
|
|
77
|
+
var CANONICAL_OTTERS = Object.freeze([...CANONICAL_CHECKSUMS.keys()]);
|
|
53
78
|
var MAX_OTTER_BYTES = 1 * 1024 * 1024;
|
|
54
79
|
function computeSha256(data) {
|
|
55
80
|
return createHash("sha256").update(data).digest("hex");
|
|
@@ -117,6 +142,18 @@ function verifyOtterFile(filePath) {
|
|
|
117
142
|
return { verified: true, filename };
|
|
118
143
|
}
|
|
119
144
|
function verifyAllOtters(otterDir) {
|
|
145
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
|
|
146
|
+
return {
|
|
147
|
+
verified: [],
|
|
148
|
+
failed: [
|
|
149
|
+
{
|
|
150
|
+
filename: "<directory>",
|
|
151
|
+
error: `Security: path traversal sequence detected in otter directory parameter`
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
unknown: []
|
|
155
|
+
};
|
|
156
|
+
}
|
|
120
157
|
const verified = [];
|
|
121
158
|
const failed = [];
|
|
122
159
|
const unknown = [];
|
|
@@ -170,6 +207,21 @@ function resolveOtterDir() {
|
|
|
170
207
|
}
|
|
171
208
|
return null;
|
|
172
209
|
}
|
|
210
|
+
function emitIntegrityAuditEvent(params) {
|
|
211
|
+
const record = JSON.stringify({
|
|
212
|
+
level: "AUDIT",
|
|
213
|
+
event: "INTEGRITY_FAIL",
|
|
214
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
215
|
+
source: "stackwright_pro_verify_otter_integrity",
|
|
216
|
+
otterDir: params.otterDir,
|
|
217
|
+
failedCount: params.failed.length,
|
|
218
|
+
unknownCount: params.unknown.length,
|
|
219
|
+
failures: params.failed,
|
|
220
|
+
unknown: params.unknown
|
|
221
|
+
});
|
|
222
|
+
process.stderr.write(`INTEGRITY_FAIL ${record}
|
|
223
|
+
`);
|
|
224
|
+
}
|
|
173
225
|
function registerIntegrityTools(server) {
|
|
174
226
|
server.tool(
|
|
175
227
|
"stackwright_pro_verify_otter_integrity",
|
|
@@ -193,6 +245,13 @@ function registerIntegrityTools(server) {
|
|
|
193
245
|
}
|
|
194
246
|
const result = verifyAllOtters(resolved);
|
|
195
247
|
const allGood = result.failed.length === 0 && result.unknown.length === 0;
|
|
248
|
+
if (!allGood) {
|
|
249
|
+
emitIntegrityAuditEvent({
|
|
250
|
+
otterDir: resolved,
|
|
251
|
+
failed: result.failed,
|
|
252
|
+
unknown: result.unknown
|
|
253
|
+
});
|
|
254
|
+
}
|
|
196
255
|
return {
|
|
197
256
|
content: [
|
|
198
257
|
{
|
|
@@ -206,17 +265,21 @@ function registerIntegrityTools(server) {
|
|
|
206
265
|
verified: result.verified,
|
|
207
266
|
failed: result.failed,
|
|
208
267
|
unknown: result.unknown,
|
|
209
|
-
|
|
268
|
+
...allGood ? {} : {
|
|
269
|
+
error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
|
|
270
|
+
}
|
|
210
271
|
})
|
|
211
272
|
}
|
|
212
273
|
],
|
|
213
|
-
isError:
|
|
274
|
+
isError: !allGood
|
|
214
275
|
};
|
|
215
276
|
}
|
|
216
277
|
);
|
|
217
278
|
}
|
|
218
279
|
export {
|
|
280
|
+
CANONICAL_OTTERS,
|
|
219
281
|
computeSha256,
|
|
282
|
+
emitIntegrityAuditEvent,
|
|
220
283
|
registerIntegrityTools,
|
|
221
284
|
verifyAllOtters,
|
|
222
285
|
verifyOtterFile
|
package/dist/integrity.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n 'a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n 'b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001',\n ],\n [\n 'stackwright-pro-data-otter.json',\n 'bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '27f4dfd4676246c9fea14fd1de4f148928eeaf06e58b08240761fd3e663dd540',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n '64ffaeeceacd739922788a1d074f6feaffc3f91d09706c2c104f0c0281677732',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n warning:\n result.failed.length > 0\n ? 'SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon.'\n : undefined,\n }),\n },\n ],\n isError: false,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n '175cca39127beea84ed8f1b264b8cd5cf58733db050d32328c039cca0cf0b981',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '1a21d9f23e250f23291124307e5a2a32af5a716566319a3290372dae3d02e637',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '6918b51d1007c69e635f252b20b82774fa7c942fa1d2399769c14f8d1df4df70',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '1ad3ed99bbe7b550f654c679a8c0ea3363b2c52031042cd177c6e5f9e1c50a21',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n '69a6c09fbb54f971c48eae7fd52faa63cf79be2923e435aca9ff802e8d541d0f',\n ],\n [\n 'stackwright-pro-domain-expert-otter.json',\n '14d77cade020f44d1b5a2b406e2599501f3466ee90ca4248500a441d419bc59c',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '9191e176241c15f3d77180e1b843cc3a517b708737b5bd3cda7db62f2d6675d1',\n ],\n [\n 'stackwright-pro-form-wizard-otter.json',\n '7f6f94192f26face57dc8b2e22e0a49d23ceeeb356ca4ebde05492f1b25e3c47',\n ],\n [\n 'stackwright-pro-geo-otter.json',\n 'fc3d18e02a6147d95d3dd9093ce74c0e8fffaecc2f9ecdbd19c163a80129d264',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '879e8ff4aa645bac2c63dc962cad1a549876983c257aec756e95293c3b22ce7e',\n ],\n [\n 'stackwright-pro-polish-otter.json',\n '9d0c53651b60507607efc0d9a0f9d166bf5ae8571f1645682e0b151d42c276ab',\n ],\n [\n 'stackwright-pro-qa-otter.json',\n '8e6007e18687b6b023f2c40f5517937a857da3f31e4e423cc308493ed601793c',\n ],\n [\n 'stackwright-pro-scaffold-otter.json',\n '5756ea77178334684b4df1d9d3395bca62cedb4991ab07dc4cf61aa7e540ea14',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'b0eccc1945e30b80a0f9a3209c5428e8e32cd09063bd9b2ef7ecb891343774f2',\n ],\n [\n 'stackwright-services-otter.json',\n '2a50ceb3fad166251d0ad8709a34a32118645713e0e34628165d16dced1d5c93',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// Exported for test assertions — derive counts from here, not hardcoded numbers.\nexport const CANONICAL_OTTERS: readonly string[] = Object.freeze([...CANONICAL_CHECKSUMS.keys()]);\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n // ---------------------------------------------------------------------------\n // Path traversal guard — reject any input containing \"..\" sequences before\n // any I/O. An attacker controlling this parameter could otherwise scan\n // directories outside the expected otter install locations.\n // ---------------------------------------------------------------------------\n if (/(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/.test(otterDir) || otterDir.includes('..')) {\n return {\n verified: [],\n failed: [\n {\n filename: '<directory>',\n error: `Security: path traversal sequence detected in otter directory parameter`,\n },\n ],\n unknown: [],\n };\n }\n\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Audit stream — SOC2 / FedRAMP / DoD ATO compliance\n// ---------------------------------------------------------------------------\n\n/**\n * Structured audit event parameters for an integrity failure.\n * Kept as a plain interface so callers can pass partial results without\n * constructing the full VerifyAllOttersResult.\n */\nexport interface IntegrityAuditEvent {\n otterDir: string;\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Emit a structured INTEGRITY_FAIL audit event to stderr.\n *\n * Writes a single line to process.stderr in the format:\n * INTEGRITY_FAIL {\"level\":\"AUDIT\",\"event\":\"INTEGRITY_FAIL\",...}\n *\n * The line prefix \"INTEGRITY_FAIL\" (without JSON) allows log shippers\n * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the\n * event to a dedicated audit stream using a simple string filter, even\n * before attempting JSON parsing.\n *\n * Exported for unit testing. Do not call directly in production code —\n * use registerIntegrityTools() which calls this automatically.\n */\nexport function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void {\n const record = JSON.stringify({\n level: 'AUDIT',\n event: 'INTEGRITY_FAIL',\n timestamp: new Date().toISOString(),\n source: 'stackwright_pro_verify_otter_integrity',\n otterDir: params.otterDir,\n failedCount: params.failed.length,\n unknownCount: params.unknown.length,\n failures: params.failed,\n unknown: params.unknown,\n });\n process.stderr.write(`INTEGRITY_FAIL ${record}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n // Emit to dedicated audit stream for SOC2/FedRAMP/DoD ATO compliance\n if (!allGood) {\n emitIntegrityAuditEvent({\n otterDir: resolved,\n failed: result.failed,\n unknown: result.unknown,\n });\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n ...(allGood\n ? {}\n : {\n error:\n 'INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed — otter files may have been tampered with.',\n }),\n }),\n },\n ],\n isError: !allGood,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
|