@stackwright-pro/mcp 0.2.0-alpha.4 → 0.2.0-alpha.48

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 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/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # @stackwright-pro/mcp
2
+
3
+ The MCP server that powers the Pro Otter Raft. Exposes 30 deterministic TypeScript tools to Claude Code and other MCP clients for structured pipeline orchestration, artifact validation, and controlled file I/O.
4
+
5
+ ---
6
+
7
+ ## Architecture: Sinks Not Pipes
8
+
9
+ When the Otter Raft scaled past nine concurrent agents, the foreman's context window became the bottleneck. Every specialist was shovelling its output back into the conversation, bloating the shared context until the whole raft flipped. Coordination collapsed: later otters couldn't reliably read what earlier otters had produced, and the foreman had no authoritative record of what was actually done versus what was merely narrated.
10
+
11
+ The fix is a single discipline: **every intermediate result is a file on disk; MCP tools are the only I/O interface**. Otters never write anything directly — they call a tool, the tool validates and writes, and the next otter reads via another tool. The foreman never accumulates state in memory; it reads the current truth from disk on every step. This makes the pipeline inspectable, resumable, and safe to parallelize — because there is nothing to lose when a context window expires.
12
+
13
+ ```
14
+ .stackwright/
15
+ ├── init-context.json ← launcher writes once
16
+ ├── pipeline-state.json ← set_pipeline_state
17
+ ├── questions/{phase}.json ← write_phase_questions
18
+ ├── answers/{phase}.json ← save_phase_answers
19
+ └── artifacts/{output}.json ← validate_artifact (the chokepoint)
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Tools Reference
25
+
26
+ ### Pipeline State & Control
27
+
28
+ Sourced from `registerPipelineTools` (`tools/pipeline.ts`).
29
+
30
+ | Tool | Purpose | Reads | Writes |
31
+ | --------------------------------------- | ---------------------------------------------- | ---------------------------------- | ---------------------------------- |
32
+ | `stackwright_pro_get_pipeline_state` | Read current pipeline state | `.stackwright/pipeline-state.json` | — |
33
+ | `stackwright_pro_set_pipeline_state` | Update phase field or top-level status | `.stackwright/pipeline-state.json` | `.stackwright/pipeline-state.json` |
34
+ | `stackwright_pro_check_execution_ready` | Gate: true only when all 8 phases have answers | `.stackwright/answers/` | — |
35
+ | `stackwright_pro_list_artifacts` | Inventory of completed specialist outputs | `.stackwright/artifacts/` | — |
36
+
37
+ ---
38
+
39
+ ### Question Collection
40
+
41
+ Sourced from `registerPipelineTools` (`tools/pipeline.ts`), `registerQuestionTools` (`tools/questions.ts`), and `registerOrchestrationTools` (`tools/orchestration.ts`).
42
+
43
+ | Tool | Purpose | Reads | Writes |
44
+ | ----------------------------------------- | ---------------------------------------------------------------- | ------------------------------------- | ------------------------------------- |
45
+ | `stackwright_pro_write_phase_questions` | Parse otter `QUESTION_COLLECTION_MODE` response and sink to disk | — | `.stackwright/questions/{phase}.json` |
46
+ | `stackwright_pro_present_phase_questions` | Adapt questions for `ask_user_question` format | `.stackwright/questions/{phase}.json` | — |
47
+ | `stackwright_pro_save_phase_answers` | Sink user answers to disk | — | `.stackwright/answers/{phase}.json` |
48
+ | `stackwright_pro_read_phase_answers` | Read answers for a phase | `.stackwright/answers/{phase}.json` | — |
49
+
50
+ ---
51
+
52
+ ### Specialist Execution
53
+
54
+ Sourced from `registerPipelineTools` (`tools/pipeline.ts`) and `registerOrchestrationTools` (`tools/orchestration.ts`).
55
+
56
+ | Tool | Purpose | Reads | Writes |
57
+ | ----------------------------------------- | ------------------------------------------------------------- | --------------------------------------------- | ------------------------------------- |
58
+ | `stackwright_pro_build_specialist_prompt` | Assemble answers + upstream artifacts into specialist prompt | `answers/{phase}.json`, upstream `artifacts/` | — |
59
+ | `stackwright_pro_validate_artifact` | Validate specialist output, detect off-script, write if valid | — | `.stackwright/artifacts/{phase}.json` |
60
+ | `stackwright_pro_get_otter_name` | Resolve phase name → otter agent name | — | — |
61
+ | `stackwright_pro_parse_otter_response` | Extract JSON from otter response text | — | — |
62
+ | `stackwright_pro_save_manifest` | Write consolidated question manifest (legacy path) | — | `.stackwright/question-manifest.json` |
63
+
64
+ ---
65
+
66
+ ### Controlled File I/O
67
+
68
+ Sourced from `registerSafeWriteTools` (`tools/safe-write.ts`).
69
+
70
+ | Tool | Purpose | Reads | Writes |
71
+ | ---------------------------- | ----------------------------------------------------------------------------------- | ----- | ---------------------- |
72
+ | `stackwright_pro_safe_write` | Per-otter path allowlist enforcement — the only way specialists write content files | — | Allowlisted paths only |
73
+
74
+ ---
75
+
76
+ ### Domain Knowledge
77
+
78
+ Sourced from `registerDomainTools` (`tools/domain.ts`).
79
+
80
+ | Tool | Purpose | Reads | Writes |
81
+ | --------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | ------ |
82
+ | `stackwright_pro_list_collections` | List API-backed collections for page generation | `.stackwright/artifacts/data-config.json` → `stackwright.yml` | — |
83
+ | `stackwright_pro_resolve_data_strategy` | Deterministic data freshness strategy lookup | — | — |
84
+ | `stackwright_pro_validate_workflow` | Validate workflow.yml graph structure | `.stackwright/artifacts/workflow-config.json` | — |
85
+
86
+ ---
87
+
88
+ ### Integrity
89
+
90
+ Sourced from `registerIntegrityTools` (`integrity.ts`).
91
+
92
+ | Tool | Purpose | Reads | Writes |
93
+ | ---------------------------------------- | --------------------------------------------------------------- | ---------------------------------- | ------ |
94
+ | `stackwright_pro_verify_otter_integrity` | SHA-256 certificate-pinned verification of all otter JSON files | `packages/otters/src/*-otter.json` | — |
95
+
96
+ ---
97
+
98
+ ### Data Explorer
99
+
100
+ Sourced from `registerDataExplorerTools` (`tools/data-explorer.ts`).
101
+
102
+ | Tool | Purpose |
103
+ | --------------------------------- | ----------------------------------- |
104
+ | `stackwright_pro_list_entities` | List API entities from OpenAPI spec |
105
+ | `stackwright_pro_generate_filter` | Generate endpoint filter config |
106
+
107
+ ---
108
+
109
+ ### Security
110
+
111
+ Sourced from `registerSecurityTools` (`tools/security.ts`).
112
+
113
+ | Tool | Purpose |
114
+ | ------------------------------------- | ------------------------------------------- |
115
+ | `stackwright_pro_validate_spec` | Validate OpenAPI spec against approved list |
116
+ | `stackwright_pro_add_approved_spec` | Add spec to approved list |
117
+ | `stackwright_pro_list_approved_specs` | List approved specs |
118
+
119
+ ---
120
+
121
+ ### ISR Configuration
122
+
123
+ Sourced from `registerIsrTools` (`tools/isr.ts`).
124
+
125
+ | Tool | Purpose |
126
+ | ------------------------------------- | -------------------------------------- |
127
+ | `stackwright_pro_configure_isr` | Configure ISR for a single collection |
128
+ | `stackwright_pro_configure_isr_batch` | Configure ISR for multiple collections |
129
+
130
+ ---
131
+
132
+ ### Auth
133
+
134
+ Sourced from `registerAuthTools` (`tools/auth.ts`).
135
+
136
+ | Tool | Purpose |
137
+ | -------------------------------- | --------------------------------------------- |
138
+ | `stackwright_pro_configure_auth` | Generate auth middleware from provider config |
139
+
140
+ ---
141
+
142
+ ### Clarification
143
+
144
+ Sourced from `registerClarificationTools` (`tools/clarification.ts`).
145
+
146
+ | Tool | Purpose |
147
+ | --------------------------------- | -------------------------------------- |
148
+ | `stackwright_pro_clarify` | Surface mid-execution question to user |
149
+ | `stackwright_pro_detect_conflict` | Detect conflicting user preferences |
150
+
151
+ ---
152
+
153
+ ### Packages
154
+
155
+ Sourced from `registerPackageTools` (`tools/packages.ts`).
156
+
157
+ | Tool | Purpose |
158
+ | -------------------------------- | ------------------------------ |
159
+ | `stackwright_pro_setup_packages` | Bootstrap Pro npm dependencies |
160
+
161
+ ---
162
+
163
+ ### Dashboard Generation
164
+
165
+ Sourced from `registerDashboardTools` (`tools/dashboard.ts`).
166
+
167
+ | Tool | Purpose |
168
+ | -------------------------------------- | ---------------------------- |
169
+ | `stackwright_pro_generate_dashboard` | Generate dashboard page spec |
170
+ | `stackwright_pro_generate_detail_page` | Generate detail page spec |
171
+
172
+ ---
173
+
174
+ ## Security Model
175
+
176
+ Three layers keep the raft from sinking:
177
+
178
+ 1. **Per-otter path allowlists** — `stackwright_pro_safe_write` enforces that each specialist otter can only write to approved paths. `page-otter` is limited to `pages/*/content.yml`; `auth-otter` is limited to `config/*.yml` and `.env*`. Attempts to write outside the allowlist are rejected with an error — the otter never touches the filesystem directly.
179
+
180
+ 2. **Artifact validation chokepoint** — specialists never write artifacts directly. All specialist output flows through `stackwright_pro_validate_artifact`, which validates the JSON structure and detects off-script code output (e.g. raw TypeScript or markdown smuggled inside a JSON field) before the file is committed. Bad output is rejected and the phase stays incomplete.
181
+
182
+ 3. **Certificate-pinned integrity** — canonical SHA-256 checksums for all otter JSON prompt files are hardcoded inside this MCP package. `stackwright_pro_verify_otter_integrity` re-hashes every file in `packages/otters/src/*-otter.json` at runtime and compares against the pinned values. A modified or tampered otter file is rejected before its prompt ever reaches an LLM.
183
+
184
+ ---
185
+
186
+ ## Phase Dependency Graph
187
+
188
+ The pipeline enforces this dependency order. A phase cannot begin specialist execution until all of its upstream phases have committed artifacts.
189
+
190
+ ```
191
+ designer ──────────────────────────────┐
192
+ api ────────────────────────────────┐ │
193
+ auth ───────────────────────────┐ │ │
194
+ │ │ │
195
+ data ← api │ │ │
196
+ theme ← designer │ │ │
197
+ pages ← designer, theme, api, data, auth
198
+ dashboard ← designer, theme, api, data
199
+ workflow ← auth
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Integrity
205
+
206
+ The full integrity enforcement model is documented in [docs/INTEGRITY_MODEL.md](docs/INTEGRITY_MODEL.md), including certificate pinning at startup, known enforcement gaps, and the threat model table for each pipeline component.
207
+
208
+ ```bash
209
+ # The MCP server is configured automatically by the launcher
210
+ npx @stackwright-pro/launch-stackwright-pro
211
+
212
+ # Or manually in claude_desktop_config.json / .mcp.json:
213
+ {
214
+ "mcpServers": {
215
+ "stackwright-pro": {
216
+ "command": "node",
217
+ "args": ["node_modules/@stackwright-pro/mcp/dist/server.js"]
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Development
226
+
227
+ ```bash
228
+ pnpm --filter @stackwright-pro/mcp test
229
+ pnpm --filter @stackwright-pro/mcp build
230
+ ```
231
+
232
+ ---
233
+
234
+ ## License
235
+
236
+ Proprietary — Per Aspera LLC
@@ -0,0 +1,60 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ /** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */
4
+ declare function computeSha256(data: Buffer): string;
5
+ interface VerifyOtterFileResult {
6
+ verified: boolean;
7
+ filename: string;
8
+ error?: string;
9
+ }
10
+ /**
11
+ * Read a single otter JSON file, check its size, compute its SHA-256,
12
+ * and constant-time compare against the canonical checksum.
13
+ *
14
+ * Single read → hash → decode. No TOCTOU window.
15
+ */
16
+ declare function verifyOtterFile(filePath: string): VerifyOtterFileResult;
17
+ interface VerifyAllOttersResult {
18
+ verified: string[];
19
+ failed: Array<{
20
+ filename: string;
21
+ error: string;
22
+ }>;
23
+ unknown: string[];
24
+ }
25
+ /**
26
+ * Scan a directory for `*-otter.json` files, verify each one against
27
+ * canonical checksums. Returns lists of verified, failed, and unknown files.
28
+ */
29
+ declare function verifyAllOtters(otterDir: string): VerifyAllOttersResult;
30
+ /**
31
+ * Structured audit event parameters for an integrity failure.
32
+ * Kept as a plain interface so callers can pass partial results without
33
+ * constructing the full VerifyAllOttersResult.
34
+ */
35
+ interface IntegrityAuditEvent {
36
+ otterDir: string;
37
+ failed: Array<{
38
+ filename: string;
39
+ error: string;
40
+ }>;
41
+ unknown: string[];
42
+ }
43
+ /**
44
+ * Emit a structured INTEGRITY_FAIL audit event to stderr.
45
+ *
46
+ * Writes a single line to process.stderr in the format:
47
+ * INTEGRITY_FAIL {"level":"AUDIT","event":"INTEGRITY_FAIL",...}
48
+ *
49
+ * The line prefix "INTEGRITY_FAIL" (without JSON) allows log shippers
50
+ * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the
51
+ * event to a dedicated audit stream using a simple string filter, even
52
+ * before attempting JSON parsing.
53
+ *
54
+ * Exported for unit testing. Do not call directly in production code —
55
+ * use registerIntegrityTools() which calls this automatically.
56
+ */
57
+ declare function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void;
58
+ declare function registerIntegrityTools(server: McpServer): void;
59
+
60
+ export { type IntegrityAuditEvent, type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, emitIntegrityAuditEvent, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
@@ -0,0 +1,60 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ /** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */
4
+ declare function computeSha256(data: Buffer): string;
5
+ interface VerifyOtterFileResult {
6
+ verified: boolean;
7
+ filename: string;
8
+ error?: string;
9
+ }
10
+ /**
11
+ * Read a single otter JSON file, check its size, compute its SHA-256,
12
+ * and constant-time compare against the canonical checksum.
13
+ *
14
+ * Single read → hash → decode. No TOCTOU window.
15
+ */
16
+ declare function verifyOtterFile(filePath: string): VerifyOtterFileResult;
17
+ interface VerifyAllOttersResult {
18
+ verified: string[];
19
+ failed: Array<{
20
+ filename: string;
21
+ error: string;
22
+ }>;
23
+ unknown: string[];
24
+ }
25
+ /**
26
+ * Scan a directory for `*-otter.json` files, verify each one against
27
+ * canonical checksums. Returns lists of verified, failed, and unknown files.
28
+ */
29
+ declare function verifyAllOtters(otterDir: string): VerifyAllOttersResult;
30
+ /**
31
+ * Structured audit event parameters for an integrity failure.
32
+ * Kept as a plain interface so callers can pass partial results without
33
+ * constructing the full VerifyAllOttersResult.
34
+ */
35
+ interface IntegrityAuditEvent {
36
+ otterDir: string;
37
+ failed: Array<{
38
+ filename: string;
39
+ error: string;
40
+ }>;
41
+ unknown: string[];
42
+ }
43
+ /**
44
+ * Emit a structured INTEGRITY_FAIL audit event to stderr.
45
+ *
46
+ * Writes a single line to process.stderr in the format:
47
+ * INTEGRITY_FAIL {"level":"AUDIT","event":"INTEGRITY_FAIL",...}
48
+ *
49
+ * The line prefix "INTEGRITY_FAIL" (without JSON) allows log shippers
50
+ * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the
51
+ * event to a dedicated audit stream using a simple string filter, even
52
+ * before attempting JSON parsing.
53
+ *
54
+ * Exported for unit testing. Do not call directly in production code —
55
+ * use registerIntegrityTools() which calls this automatically.
56
+ */
57
+ declare function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void;
58
+ declare function registerIntegrityTools(server: McpServer): void;
59
+
60
+ export { type IntegrityAuditEvent, type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, emitIntegrityAuditEvent, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/integrity.ts
21
+ var integrity_exports = {};
22
+ __export(integrity_exports, {
23
+ computeSha256: () => computeSha256,
24
+ emitIntegrityAuditEvent: () => emitIntegrityAuditEvent,
25
+ registerIntegrityTools: () => registerIntegrityTools,
26
+ verifyAllOtters: () => verifyAllOtters,
27
+ verifyOtterFile: () => verifyOtterFile
28
+ });
29
+ module.exports = __toCommonJS(integrity_exports);
30
+ var import_crypto = require("crypto");
31
+ var import_fs = require("fs");
32
+ var import_path = require("path");
33
+ var _checksums = /* @__PURE__ */ new Map([
34
+ [
35
+ "stackwright-pro-api-otter.json",
36
+ "ed667124af3f025e090c0e65d0a86f0ef08fea06c0029b8fd0edf6df33df9f9c"
37
+ ],
38
+ [
39
+ "stackwright-pro-auth-otter.json",
40
+ "b5e901262d7b3f26ef390f1d3c9aadfa68376c05f5057edc241eb37b32b40afd"
41
+ ],
42
+ [
43
+ "stackwright-pro-dashboard-otter.json",
44
+ "5e930b4092b9002e3c1a413b36418e49c865199af12a546890ccf7f9e56a5593"
45
+ ],
46
+ [
47
+ "stackwright-pro-data-otter.json",
48
+ "04b07f982f73a2904a1d92c6af3c58ecc132b474c57cab3eaec8566d718d2623"
49
+ ],
50
+ [
51
+ "stackwright-pro-designer-otter.json",
52
+ "e80d4e7bab87d8647debadb238a58aac498ec5074ff25b21abd3b13ff778bf71"
53
+ ],
54
+ [
55
+ "stackwright-pro-foreman-otter.json",
56
+ "02dd2485562361f2f3cfd998981349020d7599dcd2d969bb022f9f6d537f3517"
57
+ ],
58
+ [
59
+ "stackwright-pro-page-otter.json",
60
+ "d672dc4dfd6a3b6d66c6cec93c8db6075dcd4c8f1e8d15e2704aca2fca6856a6"
61
+ ],
62
+ [
63
+ "stackwright-pro-theme-otter.json",
64
+ "d3a15871b71a466c12c4711fe37cbb018c768cb99eff15c40dbbc7061d4e966b"
65
+ ],
66
+ [
67
+ "stackwright-pro-workflow-otter.json",
68
+ "2ce1bcbb5c45dbb214499ea08c7175f8b743b51b8cb2ad539faf7df11edcf88a"
69
+ ]
70
+ ]);
71
+ Object.freeze(_checksums);
72
+ var CANONICAL_CHECKSUMS = _checksums;
73
+ var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
74
+ for (const [name, digest] of CANONICAL_CHECKSUMS) {
75
+ if (!SHA256_HEX_RE.test(digest)) {
76
+ throw new Error(
77
+ `Malformed SHA-256 in CANONICAL_CHECKSUMS for "${name}": expected 64 hex chars, got ${digest.length}: "${digest}"`
78
+ );
79
+ }
80
+ }
81
+ var MAX_OTTER_BYTES = 1 * 1024 * 1024;
82
+ function computeSha256(data) {
83
+ return (0, import_crypto.createHash)("sha256").update(data).digest("hex");
84
+ }
85
+ function safeEqual(a, b) {
86
+ if (a.length !== b.length) return false;
87
+ return (0, import_crypto.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
88
+ }
89
+ function verifyOtterFile(filePath) {
90
+ const filename = (0, import_path.basename)(filePath);
91
+ const expected = CANONICAL_CHECKSUMS.get(filename);
92
+ if (expected === void 0) {
93
+ return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
94
+ }
95
+ let stat;
96
+ try {
97
+ stat = (0, import_fs.lstatSync)(filePath);
98
+ } catch (err) {
99
+ const msg = err instanceof Error ? err.message : String(err);
100
+ return { verified: false, filename, error: `Cannot stat file: ${msg}` };
101
+ }
102
+ if (stat.isSymbolicLink()) {
103
+ return { verified: false, filename, error: "Refusing to verify symlink" };
104
+ }
105
+ const size = stat.size;
106
+ if (size > MAX_OTTER_BYTES) {
107
+ return {
108
+ verified: false,
109
+ filename,
110
+ error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`
111
+ };
112
+ }
113
+ let raw;
114
+ try {
115
+ raw = (0, import_fs.readFileSync)(filePath);
116
+ } catch (err) {
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ return { verified: false, filename, error: `Cannot read file: ${msg}` };
119
+ }
120
+ if (raw.length > MAX_OTTER_BYTES) {
121
+ return {
122
+ verified: false,
123
+ filename,
124
+ error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`
125
+ };
126
+ }
127
+ const actual = computeSha256(raw);
128
+ if (!safeEqual(actual, expected)) {
129
+ return {
130
+ verified: false,
131
+ filename,
132
+ error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}\u2026, got ${actual.substring(0, 8)}\u2026`
133
+ };
134
+ }
135
+ try {
136
+ const decoder = new TextDecoder("utf-8", { fatal: true });
137
+ decoder.decode(raw);
138
+ } catch {
139
+ return {
140
+ verified: false,
141
+ filename,
142
+ error: "File is not valid UTF-8 \u2014 may be corrupted or contain binary injection"
143
+ };
144
+ }
145
+ return { verified: true, filename };
146
+ }
147
+ function verifyAllOtters(otterDir) {
148
+ if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
149
+ return {
150
+ verified: [],
151
+ failed: [
152
+ {
153
+ filename: "<directory>",
154
+ error: `Security: path traversal sequence detected in otter directory parameter`
155
+ }
156
+ ],
157
+ unknown: []
158
+ };
159
+ }
160
+ const verified = [];
161
+ const failed = [];
162
+ const unknown = [];
163
+ let entries;
164
+ try {
165
+ entries = (0, import_fs.readdirSync)(otterDir);
166
+ } catch (err) {
167
+ const msg = err instanceof Error ? err.message : String(err);
168
+ return {
169
+ verified: [],
170
+ failed: [{ filename: "<directory>", error: `Cannot read directory: ${msg}` }],
171
+ unknown: []
172
+ };
173
+ }
174
+ const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
175
+ for (const filename of otterFiles) {
176
+ const filePath = (0, import_path.join)(otterDir, filename);
177
+ try {
178
+ if ((0, import_fs.lstatSync)(filePath).isSymbolicLink()) {
179
+ failed.push({ filename, error: "Skipped: symlink" });
180
+ continue;
181
+ }
182
+ } catch {
183
+ }
184
+ const result = verifyOtterFile(filePath);
185
+ if (result.verified) {
186
+ verified.push(result.filename);
187
+ } else if (result.error?.startsWith("Unknown otter file")) {
188
+ unknown.push(result.filename);
189
+ } else {
190
+ failed.push({ filename: result.filename, error: result.error ?? "Unknown error" });
191
+ }
192
+ }
193
+ for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {
194
+ if (!otterFiles.includes(canonicalName)) {
195
+ failed.push({ filename: canonicalName, error: "Missing from directory" });
196
+ }
197
+ }
198
+ return { verified, failed, unknown };
199
+ }
200
+ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packages/otters/src/"];
201
+ function resolveOtterDir() {
202
+ const cwd = process.cwd();
203
+ for (const relative of DEFAULT_SEARCH_PATHS) {
204
+ const candidate = (0, import_path.join)(cwd, relative);
205
+ try {
206
+ (0, import_fs.lstatSync)(candidate);
207
+ return candidate;
208
+ } catch {
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+ function emitIntegrityAuditEvent(params) {
214
+ const record = JSON.stringify({
215
+ level: "AUDIT",
216
+ event: "INTEGRITY_FAIL",
217
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
218
+ source: "stackwright_pro_verify_otter_integrity",
219
+ otterDir: params.otterDir,
220
+ failedCount: params.failed.length,
221
+ unknownCount: params.unknown.length,
222
+ failures: params.failed,
223
+ unknown: params.unknown
224
+ });
225
+ process.stderr.write(`INTEGRITY_FAIL ${record}
226
+ `);
227
+ }
228
+ function registerIntegrityTools(server) {
229
+ server.tool(
230
+ "stackwright_pro_verify_otter_integrity",
231
+ "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.",
232
+ {},
233
+ async () => {
234
+ const resolved = resolveOtterDir();
235
+ if (!resolved) {
236
+ return {
237
+ content: [
238
+ {
239
+ type: "text",
240
+ text: JSON.stringify({
241
+ error: true,
242
+ message: "Could not locate otter directory. Searched: " + DEFAULT_SEARCH_PATHS.join(", ")
243
+ })
244
+ }
245
+ ],
246
+ isError: true
247
+ };
248
+ }
249
+ const result = verifyAllOtters(resolved);
250
+ const allGood = result.failed.length === 0 && result.unknown.length === 0;
251
+ if (!allGood) {
252
+ emitIntegrityAuditEvent({
253
+ otterDir: resolved,
254
+ failed: result.failed,
255
+ unknown: result.unknown
256
+ });
257
+ }
258
+ return {
259
+ content: [
260
+ {
261
+ type: "text",
262
+ text: JSON.stringify({
263
+ otterDir: resolved,
264
+ totalCanonical: CANONICAL_CHECKSUMS.size,
265
+ verifiedCount: result.verified.length,
266
+ failedCount: result.failed.length,
267
+ unknownCount: result.unknown.length,
268
+ verified: result.verified,
269
+ failed: result.failed,
270
+ unknown: result.unknown,
271
+ ...allGood ? {} : {
272
+ error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
273
+ }
274
+ })
275
+ }
276
+ ],
277
+ isError: !allGood
278
+ };
279
+ }
280
+ );
281
+ }
282
+ // Annotate the CommonJS export names for ESM import in node:
283
+ 0 && (module.exports = {
284
+ computeSha256,
285
+ emitIntegrityAuditEvent,
286
+ registerIntegrityTools,
287
+ verifyAllOtters,
288
+ verifyOtterFile
289
+ });
290
+ //# sourceMappingURL=integrity.js.map