@stackwright-pro/mcp 0.2.0-alpha.3 → 0.2.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,32 @@
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
+ declare function registerIntegrityTools(server: McpServer): void;
31
+
32
+ export { type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
@@ -0,0 +1,32 @@
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
+ declare function registerIntegrityTools(server: McpServer): void;
31
+
32
+ export { type VerifyAllOttersResult, type VerifyOtterFileResult, computeSha256, registerIntegrityTools, verifyAllOtters, verifyOtterFile };
@@ -0,0 +1,252 @@
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
+ registerIntegrityTools: () => registerIntegrityTools,
25
+ verifyAllOtters: () => verifyAllOtters,
26
+ verifyOtterFile: () => verifyOtterFile
27
+ });
28
+ module.exports = __toCommonJS(integrity_exports);
29
+ var import_crypto = require("crypto");
30
+ var import_fs = require("fs");
31
+ var import_path = require("path");
32
+ var _checksums = /* @__PURE__ */ new Map([
33
+ [
34
+ "stackwright-pro-api-otter.json",
35
+ "0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
36
+ ],
37
+ [
38
+ "stackwright-pro-auth-otter.json",
39
+ "e4314897e7dead94cbd07cf58cd9df1a2614a207c85bdddf9259121945903721"
40
+ ],
41
+ [
42
+ "stackwright-pro-dashboard-otter.json",
43
+ "600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
44
+ ],
45
+ [
46
+ "stackwright-pro-data-otter.json",
47
+ "08352843c3dbfd1e20171493fb95ae7c73fde9dca0e2d6eecb5dc2d7d7b3cda7"
48
+ ],
49
+ [
50
+ "stackwright-pro-designer-otter.json",
51
+ "f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
52
+ ],
53
+ [
54
+ "stackwright-pro-foreman-otter.json",
55
+ "e361cc30f013e1e423ebb4f59c54fd1452d049fabcc0539ce56b4a49cb5ec3ea"
56
+ ],
57
+ [
58
+ "stackwright-pro-page-otter.json",
59
+ "0323d9c9f4b4008b516d7615f24c0ebab9470bdf9cd37e1d48cfc06ccf6fccee"
60
+ ],
61
+ [
62
+ "stackwright-pro-theme-otter.json",
63
+ "a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc"
64
+ ],
65
+ [
66
+ "stackwright-pro-workflow-otter.json",
67
+ "ec203f222b2771f2bc3b29f340d881480c6a23188667e533476f4245e78453ef"
68
+ ]
69
+ ]);
70
+ Object.freeze(_checksums);
71
+ var CANONICAL_CHECKSUMS = _checksums;
72
+ var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
73
+ for (const [name, digest] of CANONICAL_CHECKSUMS) {
74
+ if (!SHA256_HEX_RE.test(digest)) {
75
+ throw new Error(
76
+ `Malformed SHA-256 in CANONICAL_CHECKSUMS for "${name}": expected 64 hex chars, got ${digest.length}: "${digest}"`
77
+ );
78
+ }
79
+ }
80
+ var MAX_OTTER_BYTES = 1 * 1024 * 1024;
81
+ function computeSha256(data) {
82
+ return (0, import_crypto.createHash)("sha256").update(data).digest("hex");
83
+ }
84
+ function safeEqual(a, b) {
85
+ if (a.length !== b.length) return false;
86
+ return (0, import_crypto.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
87
+ }
88
+ function verifyOtterFile(filePath) {
89
+ const filename = (0, import_path.basename)(filePath);
90
+ const expected = CANONICAL_CHECKSUMS.get(filename);
91
+ if (expected === void 0) {
92
+ return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
93
+ }
94
+ let stat;
95
+ try {
96
+ stat = (0, import_fs.lstatSync)(filePath);
97
+ } catch (err) {
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ return { verified: false, filename, error: `Cannot stat file: ${msg}` };
100
+ }
101
+ if (stat.isSymbolicLink()) {
102
+ return { verified: false, filename, error: "Refusing to verify symlink" };
103
+ }
104
+ const size = stat.size;
105
+ if (size > MAX_OTTER_BYTES) {
106
+ return {
107
+ verified: false,
108
+ filename,
109
+ error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`
110
+ };
111
+ }
112
+ let raw;
113
+ try {
114
+ raw = (0, import_fs.readFileSync)(filePath);
115
+ } catch (err) {
116
+ const msg = err instanceof Error ? err.message : String(err);
117
+ return { verified: false, filename, error: `Cannot read file: ${msg}` };
118
+ }
119
+ if (raw.length > MAX_OTTER_BYTES) {
120
+ return {
121
+ verified: false,
122
+ filename,
123
+ error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`
124
+ };
125
+ }
126
+ const actual = computeSha256(raw);
127
+ if (!safeEqual(actual, expected)) {
128
+ return {
129
+ verified: false,
130
+ filename,
131
+ error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}\u2026, got ${actual.substring(0, 8)}\u2026`
132
+ };
133
+ }
134
+ try {
135
+ const decoder = new TextDecoder("utf-8", { fatal: true });
136
+ decoder.decode(raw);
137
+ } catch {
138
+ return {
139
+ verified: false,
140
+ filename,
141
+ error: "File is not valid UTF-8 \u2014 may be corrupted or contain binary injection"
142
+ };
143
+ }
144
+ return { verified: true, filename };
145
+ }
146
+ function verifyAllOtters(otterDir) {
147
+ const verified = [];
148
+ const failed = [];
149
+ const unknown = [];
150
+ let entries;
151
+ try {
152
+ entries = (0, import_fs.readdirSync)(otterDir);
153
+ } catch (err) {
154
+ const msg = err instanceof Error ? err.message : String(err);
155
+ return {
156
+ verified: [],
157
+ failed: [{ filename: "<directory>", error: `Cannot read directory: ${msg}` }],
158
+ unknown: []
159
+ };
160
+ }
161
+ const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
162
+ for (const filename of otterFiles) {
163
+ const filePath = (0, import_path.join)(otterDir, filename);
164
+ try {
165
+ if ((0, import_fs.lstatSync)(filePath).isSymbolicLink()) {
166
+ failed.push({ filename, error: "Skipped: symlink" });
167
+ continue;
168
+ }
169
+ } catch {
170
+ }
171
+ const result = verifyOtterFile(filePath);
172
+ if (result.verified) {
173
+ verified.push(result.filename);
174
+ } else if (result.error?.startsWith("Unknown otter file")) {
175
+ unknown.push(result.filename);
176
+ } else {
177
+ failed.push({ filename: result.filename, error: result.error ?? "Unknown error" });
178
+ }
179
+ }
180
+ for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {
181
+ if (!otterFiles.includes(canonicalName)) {
182
+ failed.push({ filename: canonicalName, error: "Missing from directory" });
183
+ }
184
+ }
185
+ return { verified, failed, unknown };
186
+ }
187
+ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packages/otters/src/"];
188
+ function resolveOtterDir() {
189
+ const cwd = process.cwd();
190
+ for (const relative of DEFAULT_SEARCH_PATHS) {
191
+ const candidate = (0, import_path.join)(cwd, relative);
192
+ try {
193
+ (0, import_fs.lstatSync)(candidate);
194
+ return candidate;
195
+ } catch {
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ function registerIntegrityTools(server) {
201
+ server.tool(
202
+ "stackwright_pro_verify_otter_integrity",
203
+ "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.",
204
+ {},
205
+ async () => {
206
+ const resolved = resolveOtterDir();
207
+ if (!resolved) {
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: JSON.stringify({
213
+ error: true,
214
+ message: "Could not locate otter directory. Searched: " + DEFAULT_SEARCH_PATHS.join(", ")
215
+ })
216
+ }
217
+ ],
218
+ isError: true
219
+ };
220
+ }
221
+ const result = verifyAllOtters(resolved);
222
+ const allGood = result.failed.length === 0 && result.unknown.length === 0;
223
+ return {
224
+ content: [
225
+ {
226
+ type: "text",
227
+ text: JSON.stringify({
228
+ otterDir: resolved,
229
+ totalCanonical: CANONICAL_CHECKSUMS.size,
230
+ verifiedCount: result.verified.length,
231
+ failedCount: result.failed.length,
232
+ unknownCount: result.unknown.length,
233
+ verified: result.verified,
234
+ failed: result.failed,
235
+ unknown: result.unknown,
236
+ warning: result.failed.length > 0 ? "SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon." : void 0
237
+ })
238
+ }
239
+ ],
240
+ isError: false
241
+ };
242
+ }
243
+ );
244
+ }
245
+ // Annotate the CommonJS export names for ESM import in node:
246
+ 0 && (module.exports = {
247
+ computeSha256,
248
+ registerIntegrityTools,
249
+ verifyAllOtters,
250
+ verifyOtterFile
251
+ });
252
+ //# sourceMappingURL=integrity.js.map
@@ -0,0 +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 '0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n 'e4314897e7dead94cbd07cf58cd9df1a2614a207c85bdddf9259121945903721',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '08352843c3dbfd1e20171493fb95ae7c73fde9dca0e2d6eecb5dc2d7d7b3cda7',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n 'f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n 'e361cc30f013e1e423ebb4f59c54fd1452d049fabcc0539ce56b4a49cb5ec3ea',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '0323d9c9f4b4008b516d7615f24c0ebab9470bdf9cd37e1d48cfc06ccf6fccee',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n 'ec203f222b2771f2bc3b29f340d881480c6a23188667e533476f4245e78453ef',\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":[]}