@stackwright-pro/mcp 0.2.0-alpha.40 → 0.2.0-alpha.49

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/dist/server.mjs CHANGED
@@ -1378,6 +1378,25 @@ function registerQuestionTools(server2) {
1378
1378
  }
1379
1379
  }
1380
1380
  const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
1381
+ const labelSchema = z8.string().max(50, "Value should have at most 50 characters");
1382
+ for (const q of adapted) {
1383
+ for (const opt of q.options) {
1384
+ const check = labelSchema.safeParse(opt.label);
1385
+ if (!check.success) {
1386
+ return {
1387
+ content: [
1388
+ {
1389
+ type: "text",
1390
+ text: JSON.stringify({
1391
+ error: `Option label for phase "${phase}" exceeds 50 characters: ${check.error.issues[0]?.message ?? "label too long"}. Truncate option labels to \u226450 characters before calling this tool.`
1392
+ })
1393
+ }
1394
+ ],
1395
+ isError: true
1396
+ };
1397
+ }
1398
+ }
1399
+ }
1381
1400
  if (adapted.length === 0) {
1382
1401
  return {
1383
1402
  content: [
@@ -1873,9 +1892,350 @@ function registerOrchestrationTools(server2) {
1873
1892
  }
1874
1893
 
1875
1894
  // src/tools/pipeline.ts
1876
- import { z as z10 } from "zod";
1877
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, lstatSync as lstatSync4 } from "fs";
1895
+ import { z as z11 } from "zod";
1896
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
1897
+ import { join as join4 } from "path";
1898
+ import { createHash as createHash3 } from "crypto";
1899
+
1900
+ // src/artifact-signing.ts
1901
+ import {
1902
+ createHash as createHash2,
1903
+ generateKeyPairSync,
1904
+ createPublicKey,
1905
+ createPrivateKey,
1906
+ sign,
1907
+ verify,
1908
+ timingSafeEqual
1909
+ } from "crypto";
1910
+ import {
1911
+ readFileSync as readFileSync3,
1912
+ writeFileSync as writeFileSync3,
1913
+ existsSync as existsSync4,
1914
+ mkdirSync as mkdirSync3,
1915
+ lstatSync as lstatSync4,
1916
+ unlinkSync,
1917
+ readdirSync
1918
+ } from "fs";
1878
1919
  import { join as join3 } from "path";
1920
+ import { z as z10 } from "zod";
1921
+ var ALGORITHM = "ECDSA-P384-SHA384";
1922
+ var KEY_FILE = "pipeline-keys.json";
1923
+ var KEY_DIR = ".stackwright";
1924
+ var SIGNATURE_MANIFEST = "signatures.json";
1925
+ var ARTIFACTS_DIR = ".stackwright/artifacts";
1926
+ function rejectSymlink(filePath, context) {
1927
+ if (!existsSync4(filePath)) return;
1928
+ const stat = lstatSync4(filePath);
1929
+ if (stat.isSymbolicLink()) {
1930
+ throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
1931
+ }
1932
+ }
1933
+ function computeSha384(data) {
1934
+ return createHash2("sha384").update(data).digest("hex");
1935
+ }
1936
+ function safeDigestEqual(a, b) {
1937
+ if (a.length !== b.length) return false;
1938
+ return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
1939
+ }
1940
+ function emptyManifest() {
1941
+ return {
1942
+ version: "1.0",
1943
+ algorithm: ALGORITHM,
1944
+ signatures: {}
1945
+ };
1946
+ }
1947
+ function initPipelineKeys(cwd) {
1948
+ const keyDir = join3(cwd, KEY_DIR);
1949
+ const keyPath = join3(keyDir, KEY_FILE);
1950
+ rejectSymlink(keyPath, "pipeline-keys.json");
1951
+ mkdirSync3(keyDir, { recursive: true });
1952
+ const { publicKey, privateKey } = generateKeyPairSync("ec", {
1953
+ namedCurve: "P-384"
1954
+ });
1955
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
1956
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
1957
+ const fingerprint = createHash2("sha256").update(publicKeyPem).digest("hex");
1958
+ const keyFile = {
1959
+ version: "1.0",
1960
+ algorithm: ALGORITHM,
1961
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1962
+ publicKeyPem,
1963
+ privateKeyPem
1964
+ };
1965
+ writeFileSync3(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
1966
+ return { publicKeyPem, fingerprint };
1967
+ }
1968
+ function loadPipelineKeys(cwd) {
1969
+ const keyPath = join3(cwd, KEY_DIR, KEY_FILE);
1970
+ rejectSymlink(keyPath, "pipeline-keys.json");
1971
+ if (!existsSync4(keyPath)) {
1972
+ throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
1973
+ }
1974
+ let raw;
1975
+ try {
1976
+ raw = readFileSync3(keyPath, "utf-8");
1977
+ } catch (err) {
1978
+ const msg = err instanceof Error ? err.message : String(err);
1979
+ throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
1980
+ }
1981
+ let parsed;
1982
+ try {
1983
+ parsed = JSON.parse(raw);
1984
+ } catch {
1985
+ throw new Error("Pipeline keys file is not valid JSON");
1986
+ }
1987
+ if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
1988
+ throw new Error("Invalid public key PEM in pipeline keys file");
1989
+ }
1990
+ if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
1991
+ throw new Error("Invalid private key PEM in pipeline keys file");
1992
+ }
1993
+ const publicKey = createPublicKey(parsed.publicKeyPem);
1994
+ const privateKey = createPrivateKey(parsed.privateKeyPem);
1995
+ return { privateKey, publicKey };
1996
+ }
1997
+ function signArtifact(artifactBytes, privateKey) {
1998
+ const digest = computeSha384(artifactBytes);
1999
+ const sig = sign("SHA384", artifactBytes, privateKey);
2000
+ return {
2001
+ digest,
2002
+ signature: sig.toString("base64"),
2003
+ algorithm: ALGORITHM,
2004
+ signedAt: (/* @__PURE__ */ new Date()).toISOString()
2005
+ };
2006
+ }
2007
+ function verifyArtifact(artifactBytes, signature, publicKey) {
2008
+ const sigValid = verify(
2009
+ "SHA384",
2010
+ artifactBytes,
2011
+ publicKey,
2012
+ Buffer.from(signature.signature, "base64")
2013
+ );
2014
+ if (!sigValid) return false;
2015
+ const actualDigest = computeSha384(artifactBytes);
2016
+ return safeDigestEqual(actualDigest, signature.digest);
2017
+ }
2018
+ function loadSignatureManifest(cwd) {
2019
+ const manifestPath = join3(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
2020
+ rejectSymlink(manifestPath, "signatures.json");
2021
+ if (!existsSync4(manifestPath)) return emptyManifest();
2022
+ let raw;
2023
+ try {
2024
+ raw = readFileSync3(manifestPath, "utf-8");
2025
+ } catch (err) {
2026
+ const msg = err instanceof Error ? err.message : String(err);
2027
+ throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
2028
+ }
2029
+ try {
2030
+ const parsed = JSON.parse(raw);
2031
+ if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
2032
+ throw new Error("Malformed signature manifest: invalid version or missing signatures object");
2033
+ }
2034
+ return parsed;
2035
+ } catch (err) {
2036
+ if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
2037
+ throw new Error("Signature manifest is not valid JSON", { cause: err });
2038
+ }
2039
+ }
2040
+ function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
2041
+ const artifactsDir = join3(cwd, ARTIFACTS_DIR);
2042
+ const manifestPath = join3(artifactsDir, SIGNATURE_MANIFEST);
2043
+ rejectSymlink(manifestPath, "signatures.json (save)");
2044
+ mkdirSync3(artifactsDir, { recursive: true });
2045
+ const manifest = loadSignatureManifest(cwd);
2046
+ manifest.signatures[artifactFilename] = {
2047
+ ...sig,
2048
+ signedBy: signerOtter
2049
+ };
2050
+ writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
2051
+ }
2052
+ function getArtifactSignature(cwd, artifactFilename) {
2053
+ const manifest = loadSignatureManifest(cwd);
2054
+ const entry = manifest.signatures[artifactFilename];
2055
+ if (!entry) return null;
2056
+ return {
2057
+ digest: entry.digest,
2058
+ signature: entry.signature,
2059
+ algorithm: entry.algorithm,
2060
+ signedAt: entry.signedAt
2061
+ };
2062
+ }
2063
+ function emitSignatureAuditEvent(params) {
2064
+ const record = JSON.stringify({
2065
+ level: "AUDIT",
2066
+ event: "ARTIFACT_SIGNATURE_FAIL",
2067
+ timestamp: params.timestamp,
2068
+ source: params.source,
2069
+ artifactFilename: params.artifactFilename,
2070
+ expectedDigest: params.expectedDigest,
2071
+ actualDigest: params.actualDigest,
2072
+ phase: params.phase
2073
+ });
2074
+ process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
2075
+ `);
2076
+ }
2077
+ function registerArtifactSigningTools(server2) {
2078
+ server2.tool(
2079
+ "stackwright_pro_verify_artifact_signatures",
2080
+ "Verify ECDSA P-384 signatures for all pipeline artifacts in .stackwright/artifacts/. Auto-discovers keys from .stackwright/pipeline-keys.json. Returns per-artifact verification status.",
2081
+ {
2082
+ cwd: z10.string().optional().describe("Project root directory. Defaults to process.cwd().")
2083
+ },
2084
+ async ({ cwd: cwdParam }) => {
2085
+ const cwd = cwdParam ?? process.cwd();
2086
+ let publicKey;
2087
+ try {
2088
+ const keys = loadPipelineKeys(cwd);
2089
+ publicKey = keys.publicKey;
2090
+ } catch (err) {
2091
+ const msg = err instanceof Error ? err.message : String(err);
2092
+ return {
2093
+ content: [
2094
+ {
2095
+ type: "text",
2096
+ text: JSON.stringify({
2097
+ error: true,
2098
+ message: `Cannot load pipeline keys: ${msg}`
2099
+ })
2100
+ }
2101
+ ],
2102
+ isError: true
2103
+ };
2104
+ }
2105
+ let manifest;
2106
+ try {
2107
+ manifest = loadSignatureManifest(cwd);
2108
+ } catch (err) {
2109
+ const msg = err instanceof Error ? err.message : String(err);
2110
+ return {
2111
+ content: [
2112
+ {
2113
+ type: "text",
2114
+ text: JSON.stringify({
2115
+ error: true,
2116
+ message: `Cannot load signature manifest: ${msg}`
2117
+ })
2118
+ }
2119
+ ],
2120
+ isError: true
2121
+ };
2122
+ }
2123
+ const artifactsPath = join3(cwd, ARTIFACTS_DIR);
2124
+ let artifactFiles = [];
2125
+ try {
2126
+ if (existsSync4(artifactsPath)) {
2127
+ artifactFiles = readdirSync(artifactsPath).filter(
2128
+ (f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
2129
+ );
2130
+ }
2131
+ } catch {
2132
+ }
2133
+ const results = [];
2134
+ let hasFailure = false;
2135
+ for (const filename of artifactFiles) {
2136
+ const filePath = join3(artifactsPath, filename);
2137
+ try {
2138
+ rejectSymlink(filePath, `artifact ${filename}`);
2139
+ } catch {
2140
+ results.push({
2141
+ filename,
2142
+ verified: false,
2143
+ error: "Refusing to verify symlink"
2144
+ });
2145
+ hasFailure = true;
2146
+ continue;
2147
+ }
2148
+ let artifactBytes;
2149
+ try {
2150
+ artifactBytes = readFileSync3(filePath);
2151
+ } catch (err) {
2152
+ const msg = err instanceof Error ? err.message : String(err);
2153
+ results.push({
2154
+ filename,
2155
+ verified: false,
2156
+ error: `Cannot read artifact: ${msg}`
2157
+ });
2158
+ hasFailure = true;
2159
+ continue;
2160
+ }
2161
+ const entry = manifest.signatures[filename];
2162
+ if (!entry) {
2163
+ results.push({
2164
+ filename,
2165
+ verified: false,
2166
+ error: "No signature found in manifest"
2167
+ });
2168
+ hasFailure = true;
2169
+ continue;
2170
+ }
2171
+ const sig = {
2172
+ digest: entry.digest,
2173
+ signature: entry.signature,
2174
+ algorithm: entry.algorithm,
2175
+ signedAt: entry.signedAt
2176
+ };
2177
+ const verified = verifyArtifact(artifactBytes, sig, publicKey);
2178
+ if (!verified) {
2179
+ const actualDigest = computeSha384(artifactBytes);
2180
+ emitSignatureAuditEvent({
2181
+ artifactFilename: filename,
2182
+ expectedDigest: sig.digest,
2183
+ actualDigest,
2184
+ phase: entry.signedBy ?? "unknown",
2185
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2186
+ source: "stackwright_pro_verify_artifact_signatures"
2187
+ });
2188
+ results.push({
2189
+ filename,
2190
+ verified: false,
2191
+ error: `Signature verification failed \u2014 artifact may have been tampered with`,
2192
+ signedBy: entry.signedBy,
2193
+ signedAt: entry.signedAt
2194
+ });
2195
+ hasFailure = true;
2196
+ } else {
2197
+ results.push({
2198
+ filename,
2199
+ verified: true,
2200
+ signedBy: entry.signedBy,
2201
+ signedAt: entry.signedAt
2202
+ });
2203
+ }
2204
+ }
2205
+ for (const manifestFilename of Object.keys(manifest.signatures)) {
2206
+ if (!artifactFiles.includes(manifestFilename)) {
2207
+ results.push({
2208
+ filename: manifestFilename,
2209
+ verified: false,
2210
+ error: "Artifact referenced in manifest but missing from disk"
2211
+ });
2212
+ hasFailure = true;
2213
+ }
2214
+ }
2215
+ const verifiedCount = results.filter((r) => r.verified).length;
2216
+ const failedCount = results.filter((r) => !r.verified).length;
2217
+ return {
2218
+ content: [
2219
+ {
2220
+ type: "text",
2221
+ text: JSON.stringify({
2222
+ totalArtifacts: artifactFiles.length,
2223
+ verifiedCount,
2224
+ failedCount,
2225
+ results,
2226
+ ...hasFailure ? {
2227
+ error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
2228
+ } : {}
2229
+ })
2230
+ }
2231
+ ],
2232
+ isError: hasFailure
2233
+ };
2234
+ }
2235
+ );
2236
+ }
2237
+
2238
+ // src/tools/pipeline.ts
1879
2239
  import { WorkflowFileSchema, authConfigSchema } from "@stackwright-pro/types";
1880
2240
  var PHASE_ORDER = [
1881
2241
  "designer",
@@ -1951,11 +2311,11 @@ function createDefaultState() {
1951
2311
  };
1952
2312
  }
1953
2313
  function statePath(cwd) {
1954
- return join3(cwd, ".stackwright", "pipeline-state.json");
2314
+ return join4(cwd, ".stackwright", "pipeline-state.json");
1955
2315
  }
1956
2316
  function readState(cwd) {
1957
2317
  const p = statePath(cwd);
1958
- if (!existsSync4(p)) return createDefaultState();
2318
+ if (!existsSync5(p)) return createDefaultState();
1959
2319
  const raw = JSON.parse(safeReadSync(p));
1960
2320
  if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
1961
2321
  return createDefaultState();
@@ -1963,26 +2323,26 @@ function readState(cwd) {
1963
2323
  return raw;
1964
2324
  }
1965
2325
  function safeWriteSync(filePath, content) {
1966
- if (existsSync4(filePath)) {
1967
- const stat = lstatSync4(filePath);
2326
+ if (existsSync5(filePath)) {
2327
+ const stat = lstatSync5(filePath);
1968
2328
  if (stat.isSymbolicLink()) {
1969
2329
  throw new Error(`Refusing to write to symlink: ${filePath}`);
1970
2330
  }
1971
2331
  }
1972
- writeFileSync3(filePath, content);
2332
+ writeFileSync4(filePath, content);
1973
2333
  }
1974
2334
  function safeReadSync(filePath) {
1975
- if (existsSync4(filePath)) {
1976
- const stat = lstatSync4(filePath);
2335
+ if (existsSync5(filePath)) {
2336
+ const stat = lstatSync5(filePath);
1977
2337
  if (stat.isSymbolicLink()) {
1978
2338
  throw new Error(`Refusing to read symlink: ${filePath}`);
1979
2339
  }
1980
2340
  }
1981
- return readFileSync3(filePath, "utf-8");
2341
+ return readFileSync4(filePath, "utf-8");
1982
2342
  }
1983
2343
  function writeState(cwd, state) {
1984
- const dir = join3(cwd, ".stackwright");
1985
- mkdirSync3(dir, { recursive: true });
2344
+ const dir = join4(cwd, ".stackwright");
2345
+ mkdirSync4(dir, { recursive: true });
1986
2346
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1987
2347
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
1988
2348
  }
@@ -2003,6 +2363,15 @@ function handleGetPipelineState(_cwd) {
2003
2363
  const cwd = _cwd ?? process.cwd();
2004
2364
  try {
2005
2365
  const state = readState(cwd);
2366
+ const keyPath = join4(cwd, ".stackwright", "pipeline-keys.json");
2367
+ if (!existsSync5(keyPath)) {
2368
+ try {
2369
+ const { fingerprint } = initPipelineKeys(cwd);
2370
+ state["signingKeyFingerprint"] = fingerprint;
2371
+ writeState(cwd, state);
2372
+ } catch {
2373
+ }
2374
+ }
2006
2375
  return { text: JSON.stringify(state), isError: false };
2007
2376
  } catch (err) {
2008
2377
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
@@ -2066,8 +2435,8 @@ function handleCheckExecutionReady(_cwd, phase) {
2066
2435
  isError: true
2067
2436
  };
2068
2437
  }
2069
- const answerFile = join3(cwd, ".stackwright", "answers", `${phase}.json`);
2070
- if (!existsSync4(answerFile)) {
2438
+ const answerFile = join4(cwd, ".stackwright", "answers", `${phase}.json`);
2439
+ if (!existsSync5(answerFile)) {
2071
2440
  return {
2072
2441
  text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
2073
2442
  isError: false
@@ -2091,12 +2460,12 @@ function handleCheckExecutionReady(_cwd, phase) {
2091
2460
  }
2092
2461
  }
2093
2462
  try {
2094
- const answersDir = join3(cwd, ".stackwright", "answers");
2463
+ const answersDir = join4(cwd, ".stackwright", "answers");
2095
2464
  const answeredPhases = [];
2096
2465
  const missingPhases = [];
2097
2466
  for (const phase2 of PHASE_ORDER) {
2098
- const answerFile = join3(answersDir, `${phase2}.json`);
2099
- if (existsSync4(answerFile)) {
2467
+ const answerFile = join4(answersDir, `${phase2}.json`);
2468
+ if (existsSync5(answerFile)) {
2100
2469
  try {
2101
2470
  const raw = safeReadSync(answerFile);
2102
2471
  const parsed = JSON.parse(raw);
@@ -2128,15 +2497,35 @@ function handleCheckExecutionReady(_cwd, phase) {
2128
2497
  function handleListArtifacts(_cwd) {
2129
2498
  const cwd = _cwd ?? process.cwd();
2130
2499
  try {
2131
- const artifactsDir = join3(cwd, ".stackwright", "artifacts");
2500
+ const artifactsDir = join4(cwd, ".stackwright", "artifacts");
2501
+ let manifest = null;
2502
+ try {
2503
+ manifest = loadSignatureManifest(cwd);
2504
+ } catch {
2505
+ }
2132
2506
  const artifacts = [];
2133
2507
  let completedCount = 0;
2134
2508
  for (const phase of PHASE_ORDER) {
2135
2509
  const expectedFile = PHASE_ARTIFACT[phase];
2136
- const fullPath = join3(artifactsDir, expectedFile);
2137
- const exists = existsSync4(fullPath);
2510
+ const fullPath = join4(artifactsDir, expectedFile);
2511
+ const exists = existsSync5(fullPath);
2138
2512
  if (exists) completedCount++;
2139
- artifacts.push({ phase, expectedFile, exists, path: fullPath });
2513
+ let signed = false;
2514
+ let signatureValid = null;
2515
+ if (exists && manifest) {
2516
+ const entry = manifest.signatures[expectedFile];
2517
+ if (entry) {
2518
+ signed = true;
2519
+ try {
2520
+ const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
2521
+ const { publicKey } = loadPipelineKeys(cwd);
2522
+ signatureValid = verifyArtifact(rawBytes, entry, publicKey);
2523
+ } catch {
2524
+ signatureValid = null;
2525
+ }
2526
+ }
2527
+ }
2528
+ artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
2140
2529
  }
2141
2530
  return {
2142
2531
  text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
@@ -2172,9 +2561,9 @@ function handleWritePhaseQuestions(input) {
2172
2561
  }
2173
2562
  } catch {
2174
2563
  }
2175
- const questionsDir = join3(cwd, ".stackwright", "questions");
2176
- mkdirSync3(questionsDir, { recursive: true });
2177
- const filePath = join3(questionsDir, `${phase}.json`);
2564
+ const questionsDir = join4(cwd, ".stackwright", "questions");
2565
+ mkdirSync4(questionsDir, { recursive: true });
2566
+ const filePath = join4(questionsDir, `${phase}.json`);
2178
2567
  const payload = {
2179
2568
  version: "1.0",
2180
2569
  phase,
@@ -2214,14 +2603,14 @@ function handleBuildSpecialistPrompt(input) {
2214
2603
  };
2215
2604
  }
2216
2605
  try {
2217
- const answersPath = join3(cwd, ".stackwright", "answers", `${phase}.json`);
2606
+ const answersPath = join4(cwd, ".stackwright", "answers", `${phase}.json`);
2218
2607
  let answers = {};
2219
- if (existsSync4(answersPath)) {
2608
+ if (existsSync5(answersPath)) {
2220
2609
  answers = JSON.parse(safeReadSync(answersPath));
2221
2610
  }
2222
2611
  let buildContextText = "";
2223
- const buildContextPath = join3(cwd, ".stackwright", "build-context.json");
2224
- if (existsSync4(buildContextPath)) {
2612
+ const buildContextPath = join4(cwd, ".stackwright", "build-context.json");
2613
+ if (existsSync5(buildContextPath)) {
2225
2614
  try {
2226
2615
  const bcRaw = JSON.parse(safeReadSync(buildContextPath));
2227
2616
  if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
@@ -2235,9 +2624,39 @@ function handleBuildSpecialistPrompt(input) {
2235
2624
  const missingDependencies = [];
2236
2625
  for (const dep of deps) {
2237
2626
  const artifactFile = PHASE_ARTIFACT[dep];
2238
- const artifactPath = join3(cwd, ".stackwright", "artifacts", artifactFile);
2239
- if (existsSync4(artifactPath)) {
2240
- const content = JSON.parse(safeReadSync(artifactPath));
2627
+ const artifactPath = join4(cwd, ".stackwright", "artifacts", artifactFile);
2628
+ if (existsSync5(artifactPath)) {
2629
+ const rawContent = safeReadSync(artifactPath);
2630
+ const rawBytes = Buffer.from(rawContent, "utf-8");
2631
+ const content = JSON.parse(rawContent);
2632
+ let signatureVerified = false;
2633
+ let signatureAvailable = false;
2634
+ try {
2635
+ const sig = getArtifactSignature(cwd, artifactFile);
2636
+ if (sig) {
2637
+ signatureAvailable = true;
2638
+ const { publicKey } = loadPipelineKeys(cwd);
2639
+ signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
2640
+ if (!signatureVerified) {
2641
+ const actualDigest = createHash3("sha384").update(rawBytes).digest("hex");
2642
+ emitSignatureAuditEvent({
2643
+ artifactFilename: artifactFile,
2644
+ expectedDigest: sig.digest,
2645
+ actualDigest,
2646
+ phase: dep,
2647
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2648
+ source: "stackwright_pro_build_specialist_prompt"
2649
+ });
2650
+ missingDependencies.push(dep);
2651
+ artifactSections.push(
2652
+ `[${artifactFile}]:
2653
+ (integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
2654
+ );
2655
+ continue;
2656
+ }
2657
+ }
2658
+ } catch {
2659
+ }
2241
2660
  const expectedOtter = PHASE_TO_OTTER2[dep];
2242
2661
  const artifactOtter = content["generatedBy"];
2243
2662
  const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
@@ -2254,8 +2673,11 @@ function handleBuildSpecialistPrompt(input) {
2254
2673
  (integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
2255
2674
  );
2256
2675
  } else {
2257
- artifactSections.push(`[${artifactFile}]:
2258
- ${JSON.stringify(content, null, 2)}`);
2676
+ const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
2677
+ artifactSections.push(
2678
+ `[${artifactFile}]${sigStatus}:
2679
+ ${JSON.stringify(content, null, 2)}`
2680
+ );
2259
2681
  }
2260
2682
  } else {
2261
2683
  missingDependencies.push(dep);
@@ -2271,6 +2693,9 @@ ${JSON.stringify(content, null, 2)}`);
2271
2693
  if (artifactSections.length > 0) {
2272
2694
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
2273
2695
  }
2696
+ const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
2697
+ parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
2698
+ parts.push(artifactSchema);
2274
2699
  parts.push("", "Execute using these answers and the upstream artifacts provided.");
2275
2700
  const prompt = parts.join("\n");
2276
2701
  const dependenciesSatisfied = missingDependencies.length === 0;
@@ -2312,6 +2737,187 @@ var PHASE_REQUIRED_KEYS = {
2312
2737
  dashboard: ["version", "generatedBy"],
2313
2738
  workflow: ["version", "generatedBy"]
2314
2739
  };
2740
+ var PHASE_ARTIFACT_SCHEMA = {
2741
+ designer: JSON.stringify(
2742
+ {
2743
+ version: "1.0",
2744
+ generatedBy: "stackwright-pro-designer-otter",
2745
+ application: {
2746
+ type: "<operational|data-explorer|admin|logistics|general>",
2747
+ environment: "<workstation|field|control-room|mixed>",
2748
+ density: "<compact|balanced|spacious>",
2749
+ accessibility: "<wcag-aa|section-508|none>",
2750
+ colorScheme: "<light|dark|both>"
2751
+ },
2752
+ designLanguage: {
2753
+ rationale: "<design rationale>",
2754
+ spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
2755
+ colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
2756
+ typography: {
2757
+ dataFont: "Inter",
2758
+ headingFont: "Inter",
2759
+ monoFont: "monospace",
2760
+ dataSizePx: 12,
2761
+ bodySizePx: 14
2762
+ },
2763
+ contrastRatio: "4.5",
2764
+ borderRadius: "4",
2765
+ shadowElevation: "standard"
2766
+ },
2767
+ themeTokenSeeds: {
2768
+ light: {
2769
+ background: "#ffffff",
2770
+ foreground: "#1a1a1a",
2771
+ primary: "#1a365d",
2772
+ surface: "#f7f7f7",
2773
+ border: "#e2e8f0"
2774
+ },
2775
+ dark: {
2776
+ background: "#1a1a1a",
2777
+ foreground: "#ffffff",
2778
+ primary: "#90cdf4",
2779
+ surface: "#2d2d2d",
2780
+ border: "#4a5568"
2781
+ }
2782
+ },
2783
+ conformsTo: null,
2784
+ operationalNotes: []
2785
+ },
2786
+ null,
2787
+ 2
2788
+ ),
2789
+ theme: JSON.stringify(
2790
+ {
2791
+ version: "1.0",
2792
+ generatedBy: "stackwright-pro-theme-otter",
2793
+ componentLibrary: "shadcn",
2794
+ colorScheme: "<light|dark|both>",
2795
+ tokens: {
2796
+ colors: { "primary-500": "#1a365d", background: "#ffffff" },
2797
+ spacing: { "spacing-1": "8px", "spacing-2": "16px" },
2798
+ typography: { "font-data": "Inter", "text-sm": "12px" },
2799
+ shape: { "radius-sm": "4px", "radius-md": "8px" },
2800
+ shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
2801
+ },
2802
+ cssVariables: {
2803
+ "--background": "0 0% 100%",
2804
+ "--foreground": "222.2 84% 4.9%",
2805
+ "--primary": "222.2 47.4% 11.2%",
2806
+ "--primary-foreground": "210 40% 98%",
2807
+ "--surface": "210 40% 98%",
2808
+ "--border": "214.3 31.8% 91.4%"
2809
+ },
2810
+ dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
2811
+ },
2812
+ null,
2813
+ 2
2814
+ ),
2815
+ api: JSON.stringify(
2816
+ {
2817
+ version: "1.0",
2818
+ generatedBy: "stackwright-pro-api-otter",
2819
+ entities: [
2820
+ {
2821
+ name: "Shipment",
2822
+ endpoint: "/shipments",
2823
+ method: "GET",
2824
+ revalidate: 60,
2825
+ mutationType: null
2826
+ }
2827
+ ],
2828
+ auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
2829
+ baseUrl: "https://api.example.mil/v2",
2830
+ specPath: "./specs/api.yaml"
2831
+ },
2832
+ null,
2833
+ 2
2834
+ ),
2835
+ data: JSON.stringify(
2836
+ {
2837
+ version: "1.0",
2838
+ generatedBy: "stackwright-pro-data-otter",
2839
+ strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
2840
+ pulseMode: false,
2841
+ collections: [{ name: "equipment", revalidate: 60, pulse: false }],
2842
+ endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
2843
+ requiredPackages: { dependencies: {}, devPackages: {} }
2844
+ },
2845
+ null,
2846
+ 2
2847
+ ),
2848
+ workflow: JSON.stringify(
2849
+ {
2850
+ version: "1.0",
2851
+ generatedBy: "stackwright-pro-workflow-otter",
2852
+ workflowConfig: {
2853
+ id: "procurement-approval",
2854
+ route: "/procurement",
2855
+ files: ["workflows/procurement-approval.yml"],
2856
+ serviceDependencies: ["service:workflow-state"],
2857
+ warnings: []
2858
+ }
2859
+ },
2860
+ null,
2861
+ 2
2862
+ ),
2863
+ pages: JSON.stringify(
2864
+ {
2865
+ version: "1.0",
2866
+ generatedBy: "stackwright-pro-page-otter",
2867
+ pages: [
2868
+ {
2869
+ slug: "catalog",
2870
+ type: "collection_listing",
2871
+ collection: "products",
2872
+ themeApplied: true,
2873
+ authRequired: false
2874
+ },
2875
+ {
2876
+ slug: "admin",
2877
+ type: "protected",
2878
+ collection: null,
2879
+ themeApplied: true,
2880
+ authRequired: true
2881
+ }
2882
+ ]
2883
+ },
2884
+ null,
2885
+ 2
2886
+ ),
2887
+ dashboard: JSON.stringify(
2888
+ {
2889
+ version: "1.0",
2890
+ generatedBy: "stackwright-pro-dashboard-otter",
2891
+ pages: [
2892
+ {
2893
+ slug: "dashboard",
2894
+ layout: "<grid|table|mixed>",
2895
+ collections: ["equipment", "supplies"],
2896
+ mode: "<ISR|Pulse>"
2897
+ }
2898
+ ]
2899
+ },
2900
+ null,
2901
+ 2
2902
+ ),
2903
+ auth: JSON.stringify(
2904
+ {
2905
+ version: "1.0",
2906
+ generatedBy: "stackwright-pro-auth-otter",
2907
+ authConfig: {
2908
+ method: "<cac|oidc|oauth2|none>",
2909
+ provider: "<azure-ad|okta|ping|cognito \u2014 if OIDC, else null>",
2910
+ rbacRoles: ["ADMIN", "ANALYST"],
2911
+ rbacDefaultRole: "ANALYST",
2912
+ protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
2913
+ auditEnabled: true,
2914
+ auditRetentionDays: 90
2915
+ }
2916
+ },
2917
+ null,
2918
+ 2
2919
+ )
2920
+ };
2315
2921
  function handleValidateArtifact(input) {
2316
2922
  const cwd = input._cwd ?? process.cwd();
2317
2923
  const { phase, responseText, artifact: directArtifact } = input;
@@ -2405,11 +3011,22 @@ function handleValidateArtifact(input) {
2405
3011
  }
2406
3012
  }
2407
3013
  try {
2408
- const artifactsDir = join3(cwd, ".stackwright", "artifacts");
2409
- mkdirSync3(artifactsDir, { recursive: true });
3014
+ const artifactsDir = join4(cwd, ".stackwright", "artifacts");
3015
+ mkdirSync4(artifactsDir, { recursive: true });
2410
3016
  const artifactFile = PHASE_ARTIFACT[phase];
2411
- const artifactPath = join3(artifactsDir, artifactFile);
2412
- safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
3017
+ const artifactPath = join4(artifactsDir, artifactFile);
3018
+ const serialized = JSON.stringify(artifact, null, 2) + "\n";
3019
+ const artifactBytes = Buffer.from(serialized, "utf-8");
3020
+ safeWriteSync(artifactPath, serialized);
3021
+ let signed = false;
3022
+ try {
3023
+ const { privateKey } = loadPipelineKeys(cwd);
3024
+ const sig = signArtifact(artifactBytes, privateKey);
3025
+ const signerOtter = PHASE_TO_OTTER2[phase];
3026
+ saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
3027
+ signed = true;
3028
+ } catch {
3029
+ }
2413
3030
  const state = readState(cwd);
2414
3031
  if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
2415
3032
  const ps = state.phases[phase];
@@ -2420,7 +3037,7 @@ function handleValidateArtifact(input) {
2420
3037
  valid: true,
2421
3038
  phase,
2422
3039
  artifactPath,
2423
- summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
3040
+ summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
2424
3041
  };
2425
3042
  return { text: JSON.stringify(result), isError: false };
2426
3043
  } catch (err) {
@@ -2444,13 +3061,13 @@ function registerPipelineTools(server2) {
2444
3061
  "stackwright_pro_set_pipeline_state",
2445
3062
  `Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
2446
3063
  {
2447
- phase: z10.string().optional().describe('Phase to update, e.g. "designer"'),
2448
- field: z10.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
2449
- value: boolCoerce(z10.boolean().optional()).describe(
3064
+ phase: z11.string().optional().describe('Phase to update, e.g. "designer"'),
3065
+ field: z11.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
3066
+ value: boolCoerce(z11.boolean().optional()).describe(
2450
3067
  'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
2451
3068
  ),
2452
- status: z10.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
2453
- incrementRetry: boolCoerce(z10.boolean().optional()).describe(
3069
+ status: z11.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
3070
+ incrementRetry: boolCoerce(z11.boolean().optional()).describe(
2454
3071
  "Bump retryCount by 1 \u2014 must be a JSON boolean"
2455
3072
  )
2456
3073
  },
@@ -2468,7 +3085,7 @@ function registerPipelineTools(server2) {
2468
3085
  "stackwright_pro_check_execution_ready",
2469
3086
  `Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
2470
3087
  {
2471
- phase: z10.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
3088
+ phase: z11.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
2472
3089
  },
2473
3090
  async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
2474
3091
  );
@@ -2482,9 +3099,9 @@ function registerPipelineTools(server2) {
2482
3099
  "stackwright_pro_write_phase_questions",
2483
3100
  `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2484
3101
  {
2485
- phase: z10.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
2486
- responseText: z10.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
2487
- questions: jsonCoerce(z10.array(z10.any()).optional()).describe(
3102
+ phase: z11.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
3103
+ responseText: z11.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
3104
+ questions: jsonCoerce(z11.array(z11.any()).optional()).describe(
2488
3105
  "Questions array for direct specialist write"
2489
3106
  )
2490
3107
  },
@@ -2499,10 +3116,10 @@ function registerPipelineTools(server2) {
2499
3116
  isError: true
2500
3117
  };
2501
3118
  }
2502
- const questionsDir = join3(process.cwd(), ".stackwright", "questions");
2503
- mkdirSync3(questionsDir, { recursive: true });
2504
- const outPath = join3(questionsDir, `${phase}.json`);
2505
- writeFileSync3(
3119
+ const questionsDir = join4(process.cwd(), ".stackwright", "questions");
3120
+ mkdirSync4(questionsDir, { recursive: true });
3121
+ const outPath = join4(questionsDir, `${phase}.json`);
3122
+ writeFileSync4(
2506
3123
  outPath,
2507
3124
  JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
2508
3125
  );
@@ -2527,16 +3144,16 @@ function registerPipelineTools(server2) {
2527
3144
  server2.tool(
2528
3145
  "stackwright_pro_build_specialist_prompt",
2529
3146
  `Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
2530
- { phase: z10.string().describe('Phase to build prompt for, e.g. "pages"') },
3147
+ { phase: z11.string().describe('Phase to build prompt for, e.g. "pages"') },
2531
3148
  async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
2532
3149
  );
2533
3150
  server2.tool(
2534
3151
  "stackwright_pro_validate_artifact",
2535
3152
  `Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
2536
3153
  {
2537
- phase: z10.string().describe('Phase that produced this artifact, e.g. "designer"'),
2538
- responseText: z10.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
2539
- artifact: z10.record(z10.unknown()).optional().describe(
3154
+ phase: z11.string().describe('Phase that produced this artifact, e.g. "designer"'),
3155
+ responseText: z11.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
3156
+ artifact: jsonCoerce(z11.record(z11.string(), z11.unknown()).optional()).describe(
2540
3157
  "Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
2541
3158
  )
2542
3159
  },
@@ -2552,9 +3169,9 @@ function registerPipelineTools(server2) {
2552
3169
  }
2553
3170
 
2554
3171
  // src/tools/safe-write.ts
2555
- import { z as z11 } from "zod";
2556
- import { writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
2557
- import { normalize, isAbsolute, dirname, join as join4 } from "path";
3172
+ import { z as z12 } from "zod";
3173
+ import { writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
3174
+ import { normalize, isAbsolute, dirname, join as join5 } from "path";
2558
3175
  var OTTER_WRITE_ALLOWLISTS = {
2559
3176
  "stackwright-pro-designer-otter": [
2560
3177
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
@@ -2597,9 +3214,25 @@ var OTTER_WRITE_ALLOWLISTS = {
2597
3214
  };
2598
3215
  var PROTECTED_PATH_PREFIXES = [
2599
3216
  ".stackwright/pipeline-state.json",
3217
+ ".stackwright/pipeline-keys.json",
3218
+ // ephemeral signing keys
3219
+ ".stackwright/artifacts/signatures.json",
3220
+ // artifact signature manifest
2600
3221
  ".stackwright/questions/",
2601
3222
  ".stackwright/answers/"
2602
3223
  ];
3224
+ var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
3225
+ var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
3226
+ var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
3227
+ var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
3228
+ function getMaxBytesForPath(filePath) {
3229
+ if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
3230
+ if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
3231
+ return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
3232
+ if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
3233
+ return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
3234
+ return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
3235
+ }
2603
3236
  function checkPathAllowed(callerOtter, filePath) {
2604
3237
  const normalized = normalize(filePath);
2605
3238
  if (normalized.includes("..")) {
@@ -2725,11 +3358,23 @@ function handleSafeWrite(input) {
2725
3358
  };
2726
3359
  return { text: JSON.stringify(result), isError: true };
2727
3360
  }
3361
+ const contentBytes = Buffer.byteLength(content, "utf-8");
3362
+ const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
3363
+ if (contentBytes > maxBytes) {
3364
+ const result = {
3365
+ success: false,
3366
+ error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
3367
+ callerOtter,
3368
+ attemptedPath: filePath,
3369
+ allowedPaths: []
3370
+ };
3371
+ return { text: JSON.stringify(result), isError: true };
3372
+ }
2728
3373
  const normalized = normalize(filePath);
2729
- const fullPath = join4(cwd, normalized);
2730
- if (existsSync5(fullPath)) {
3374
+ const fullPath = join5(cwd, normalized);
3375
+ if (existsSync6(fullPath)) {
2731
3376
  try {
2732
- const stat = lstatSync5(fullPath);
3377
+ const stat = lstatSync6(fullPath);
2733
3378
  if (stat.isSymbolicLink()) {
2734
3379
  const result = {
2735
3380
  success: false,
@@ -2756,9 +3401,9 @@ function handleSafeWrite(input) {
2756
3401
  }
2757
3402
  try {
2758
3403
  if (createDirectories) {
2759
- mkdirSync4(dirname(fullPath), { recursive: true });
3404
+ mkdirSync5(dirname(fullPath), { recursive: true });
2760
3405
  }
2761
- writeFileSync4(fullPath, content, { encoding: "utf-8" });
3406
+ writeFileSync5(fullPath, content, { encoding: "utf-8" });
2762
3407
  const result = {
2763
3408
  success: true,
2764
3409
  path: normalized,
@@ -2784,10 +3429,10 @@ function registerSafeWriteTools(server2) {
2784
3429
  "stackwright_pro_safe_write",
2785
3430
  DESC,
2786
3431
  {
2787
- callerOtter: z11.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
2788
- filePath: z11.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
2789
- content: z11.string().describe("File content to write"),
2790
- createDirectories: boolCoerce(z11.boolean().optional().default(true)).describe(
3432
+ callerOtter: z12.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
3433
+ filePath: z12.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
3434
+ content: z12.string().describe("File content to write"),
3435
+ createDirectories: boolCoerce(z12.boolean().optional().default(true)).describe(
2791
3436
  "Create parent directories if they don't exist. Default: true"
2792
3437
  )
2793
3438
  },
@@ -2804,9 +3449,9 @@ function registerSafeWriteTools(server2) {
2804
3449
  }
2805
3450
 
2806
3451
  // src/tools/auth.ts
2807
- import { z as z12 } from "zod";
2808
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2809
- import { join as join5 } from "path";
3452
+ import { z as z13 } from "zod";
3453
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
3454
+ import { join as join6 } from "path";
2810
3455
  function buildHierarchy(roles) {
2811
3456
  const h = {};
2812
3457
  for (let i = 0; i < roles.length - 1; i++) {
@@ -3068,7 +3713,7 @@ async function configureAuthHandler(params, cwd) {
3068
3713
  auditRetentionDays,
3069
3714
  protectedRoutes
3070
3715
  );
3071
- writeFileSync5(join5(cwd, "middleware.ts"), middlewareContent, "utf8");
3716
+ writeFileSync6(join6(cwd, "middleware.ts"), middlewareContent, "utf8");
3072
3717
  filesWritten.push("middleware.ts");
3073
3718
  } catch (err) {
3074
3719
  const msg = err instanceof Error ? err.message : String(err);
@@ -3084,12 +3729,12 @@ async function configureAuthHandler(params, cwd) {
3084
3729
  }
3085
3730
  try {
3086
3731
  const envBlock = generateEnvBlock(method, params);
3087
- const envPath = join5(cwd, ".env.example");
3088
- if (existsSync6(envPath)) {
3089
- const existing = readFileSync4(envPath, "utf8");
3090
- writeFileSync5(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
3732
+ const envPath = join6(cwd, ".env.example");
3733
+ if (existsSync7(envPath)) {
3734
+ const existing = readFileSync5(envPath, "utf8");
3735
+ writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
3091
3736
  } else {
3092
- writeFileSync5(envPath, envBlock, "utf8");
3737
+ writeFileSync6(envPath, envBlock, "utf8");
3093
3738
  }
3094
3739
  filesWritten.push(".env.example");
3095
3740
  } catch (err) {
@@ -3115,12 +3760,12 @@ async function configureAuthHandler(params, cwd) {
3115
3760
  auditRetentionDays,
3116
3761
  protectedRoutes
3117
3762
  );
3118
- const ymlPath = join5(cwd, "stackwright.yml");
3119
- if (!existsSync6(ymlPath)) {
3120
- writeFileSync5(ymlPath, authYaml, "utf8");
3763
+ const ymlPath = join6(cwd, "stackwright.yml");
3764
+ if (!existsSync7(ymlPath)) {
3765
+ writeFileSync6(ymlPath, authYaml, "utf8");
3121
3766
  } else {
3122
- const existing = readFileSync4(ymlPath, "utf8");
3123
- writeFileSync5(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
3767
+ const existing = readFileSync5(ymlPath, "utf8");
3768
+ writeFileSync6(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
3124
3769
  }
3125
3770
  filesWritten.push("stackwright.yml");
3126
3771
  } catch (err) {
@@ -3159,35 +3804,35 @@ function registerAuthTools(server2) {
3159
3804
  "stackwright_pro_configure_auth",
3160
3805
  "Generate authentication middleware and configuration for a Next.js Stackwright application. Writes `middleware.ts` from a secure template, appends/updates the `auth:` section in `stackwright.yml`, and generates `.env.example` with required environment variables. \u26A0\uFE0F For CAC/PKI: generated `middleware.ts` carries a SECURITY REVIEW REQUIRED comment \u2014 certificate chain validation must be verified by a DoD security officer before production deployment. This is the ONLY approved path to generating `middleware.ts`. Never write TypeScript auth files directly.",
3161
3806
  {
3162
- method: z12.enum(["cac", "oidc", "oauth2", "none"]),
3163
- provider: z12.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
3807
+ method: z13.enum(["cac", "oidc", "oauth2", "none"]),
3808
+ provider: z13.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
3164
3809
  // CAC
3165
- cacCaBundle: z12.string().optional(),
3166
- cacEdipiLookup: z12.string().optional(),
3167
- cacOcspEndpoint: z12.string().optional(),
3168
- cacCertHeader: z12.string().optional(),
3810
+ cacCaBundle: z13.string().optional(),
3811
+ cacEdipiLookup: z13.string().optional(),
3812
+ cacOcspEndpoint: z13.string().optional(),
3813
+ cacCertHeader: z13.string().optional(),
3169
3814
  // OIDC
3170
- oidcDiscoveryUrl: z12.string().optional(),
3171
- oidcClientId: z12.string().optional(),
3172
- oidcClientSecret: z12.string().optional(),
3173
- oidcScopes: z12.string().optional(),
3174
- oidcRoleClaim: z12.string().optional(),
3815
+ oidcDiscoveryUrl: z13.string().optional(),
3816
+ oidcClientId: z13.string().optional(),
3817
+ oidcClientSecret: z13.string().optional(),
3818
+ oidcScopes: z13.string().optional(),
3819
+ oidcRoleClaim: z13.string().optional(),
3175
3820
  // OAuth2
3176
- oauth2AuthUrl: z12.string().optional(),
3177
- oauth2TokenUrl: z12.string().optional(),
3178
- oauth2ClientId: z12.string().optional(),
3179
- oauth2ClientSecret: z12.string().optional(),
3180
- oauth2Scopes: z12.string().optional(),
3821
+ oauth2AuthUrl: z13.string().optional(),
3822
+ oauth2TokenUrl: z13.string().optional(),
3823
+ oauth2ClientId: z13.string().optional(),
3824
+ oauth2ClientSecret: z13.string().optional(),
3825
+ oauth2Scopes: z13.string().optional(),
3181
3826
  // RBAC
3182
- rbacRoles: jsonCoerce(z12.array(z12.string()).optional()),
3183
- rbacDefaultRole: z12.string().optional(),
3827
+ rbacRoles: jsonCoerce(z13.array(z13.string()).optional()),
3828
+ rbacDefaultRole: z13.string().optional(),
3184
3829
  // Audit
3185
- auditEnabled: boolCoerce(z12.boolean().optional()),
3186
- auditRetentionDays: numCoerce(z12.number().int().positive().optional()),
3830
+ auditEnabled: boolCoerce(z13.boolean().optional()),
3831
+ auditRetentionDays: numCoerce(z13.number().int().positive().optional()),
3187
3832
  // Routes
3188
- protectedRoutes: jsonCoerce(z12.array(z12.string()).optional()),
3833
+ protectedRoutes: jsonCoerce(z13.array(z13.string()).optional()),
3189
3834
  // Injection for tests
3190
- _cwd: z12.string().optional()
3835
+ _cwd: z13.string().optional()
3191
3836
  },
3192
3837
  async (params) => {
3193
3838
  const cwd = params._cwd ?? process.cwd();
@@ -3197,13 +3842,13 @@ function registerAuthTools(server2) {
3197
3842
  }
3198
3843
 
3199
3844
  // src/integrity.ts
3200
- import { createHash as createHash2, timingSafeEqual } from "crypto";
3201
- import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync6 } from "fs";
3202
- import { join as join6, basename } from "path";
3845
+ import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
3846
+ import { readFileSync as readFileSync6, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
3847
+ import { join as join7, basename } from "path";
3203
3848
  var _checksums = /* @__PURE__ */ new Map([
3204
3849
  [
3205
3850
  "stackwright-pro-api-otter.json",
3206
- "1fd28747ff43121533d40d6446f2d2670d6247afb04e3025cbbcb9ace0e7d1e2"
3851
+ "ed667124af3f025e090c0e65d0a86f0ef08fea06c0029b8fd0edf6df33df9f9c"
3207
3852
  ],
3208
3853
  [
3209
3854
  "stackwright-pro-auth-otter.json",
@@ -3211,7 +3856,7 @@ var _checksums = /* @__PURE__ */ new Map([
3211
3856
  ],
3212
3857
  [
3213
3858
  "stackwright-pro-dashboard-otter.json",
3214
- "a9e50f26e8b2b687910685f15104b4e76a74ad2e1e5a6021237e1eeb1cbde2ae"
3859
+ "5e930b4092b9002e3c1a413b36418e49c865199af12a546890ccf7f9e56a5593"
3215
3860
  ],
3216
3861
  [
3217
3862
  "stackwright-pro-data-otter.json",
@@ -3219,11 +3864,11 @@ var _checksums = /* @__PURE__ */ new Map([
3219
3864
  ],
3220
3865
  [
3221
3866
  "stackwright-pro-designer-otter.json",
3222
- "41c5b6b9f1f0f6eb0851e473f9d7d6ebd6a7e00dafd5cdeb8a8b12b0b756e245"
3867
+ "e80d4e7bab87d8647debadb238a58aac498ec5074ff25b21abd3b13ff778bf71"
3223
3868
  ],
3224
3869
  [
3225
3870
  "stackwright-pro-foreman-otter.json",
3226
- "7c8af9ce5b157ad3030f0255218a6ea923df18a36fe44db9bd5f04897434fc05"
3871
+ "02dd2485562361f2f3cfd998981349020d7599dcd2d969bb022f9f6d537f3517"
3227
3872
  ],
3228
3873
  [
3229
3874
  "stackwright-pro-page-otter.json",
@@ -3231,11 +3876,11 @@ var _checksums = /* @__PURE__ */ new Map([
3231
3876
  ],
3232
3877
  [
3233
3878
  "stackwright-pro-theme-otter.json",
3234
- "3a37d4bd696f142c4a4278ef653984fca4b776caa610182c2cb82f6732ef9b62"
3879
+ "d3a15871b71a466c12c4711fe37cbb018c768cb99eff15c40dbbc7061d4e966b"
3235
3880
  ],
3236
3881
  [
3237
3882
  "stackwright-pro-workflow-otter.json",
3238
- "fa2bae06e0f9e6b844008adc933d24b6a210708c0812ce068fc43733ee98b98e"
3883
+ "2ce1bcbb5c45dbb214499ea08c7175f8b743b51b8cb2ad539faf7df11edcf88a"
3239
3884
  ]
3240
3885
  ]);
3241
3886
  Object.freeze(_checksums);
@@ -3250,11 +3895,11 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
3250
3895
  }
3251
3896
  var MAX_OTTER_BYTES = 1 * 1024 * 1024;
3252
3897
  function computeSha256(data) {
3253
- return createHash2("sha256").update(data).digest("hex");
3898
+ return createHash4("sha256").update(data).digest("hex");
3254
3899
  }
3255
3900
  function safeEqual(a, b) {
3256
3901
  if (a.length !== b.length) return false;
3257
- return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
3902
+ return timingSafeEqual2(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
3258
3903
  }
3259
3904
  function verifyOtterFile(filePath) {
3260
3905
  const filename = basename(filePath);
@@ -3264,7 +3909,7 @@ function verifyOtterFile(filePath) {
3264
3909
  }
3265
3910
  let stat;
3266
3911
  try {
3267
- stat = lstatSync6(filePath);
3912
+ stat = lstatSync7(filePath);
3268
3913
  } catch (err) {
3269
3914
  const msg = err instanceof Error ? err.message : String(err);
3270
3915
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -3282,7 +3927,7 @@ function verifyOtterFile(filePath) {
3282
3927
  }
3283
3928
  let raw;
3284
3929
  try {
3285
- raw = readFileSync5(filePath);
3930
+ raw = readFileSync6(filePath);
3286
3931
  } catch (err) {
3287
3932
  const msg = err instanceof Error ? err.message : String(err);
3288
3933
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -3315,12 +3960,24 @@ function verifyOtterFile(filePath) {
3315
3960
  return { verified: true, filename };
3316
3961
  }
3317
3962
  function verifyAllOtters(otterDir) {
3963
+ if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
3964
+ return {
3965
+ verified: [],
3966
+ failed: [
3967
+ {
3968
+ filename: "<directory>",
3969
+ error: `Security: path traversal sequence detected in otter directory parameter`
3970
+ }
3971
+ ],
3972
+ unknown: []
3973
+ };
3974
+ }
3318
3975
  const verified = [];
3319
3976
  const failed = [];
3320
3977
  const unknown = [];
3321
3978
  let entries;
3322
3979
  try {
3323
- entries = readdirSync(otterDir);
3980
+ entries = readdirSync2(otterDir);
3324
3981
  } catch (err) {
3325
3982
  const msg = err instanceof Error ? err.message : String(err);
3326
3983
  return {
@@ -3331,9 +3988,9 @@ function verifyAllOtters(otterDir) {
3331
3988
  }
3332
3989
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
3333
3990
  for (const filename of otterFiles) {
3334
- const filePath = join6(otterDir, filename);
3991
+ const filePath = join7(otterDir, filename);
3335
3992
  try {
3336
- if (lstatSync6(filePath).isSymbolicLink()) {
3993
+ if (lstatSync7(filePath).isSymbolicLink()) {
3337
3994
  failed.push({ filename, error: "Skipped: symlink" });
3338
3995
  continue;
3339
3996
  }
@@ -3359,15 +4016,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
3359
4016
  function resolveOtterDir() {
3360
4017
  const cwd = process.cwd();
3361
4018
  for (const relative of DEFAULT_SEARCH_PATHS) {
3362
- const candidate = join6(cwd, relative);
4019
+ const candidate = join7(cwd, relative);
3363
4020
  try {
3364
- lstatSync6(candidate);
4021
+ lstatSync7(candidate);
3365
4022
  return candidate;
3366
4023
  } catch {
3367
4024
  }
3368
4025
  }
3369
4026
  return null;
3370
4027
  }
4028
+ function emitIntegrityAuditEvent(params) {
4029
+ const record = JSON.stringify({
4030
+ level: "AUDIT",
4031
+ event: "INTEGRITY_FAIL",
4032
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4033
+ source: "stackwright_pro_verify_otter_integrity",
4034
+ otterDir: params.otterDir,
4035
+ failedCount: params.failed.length,
4036
+ unknownCount: params.unknown.length,
4037
+ failures: params.failed,
4038
+ unknown: params.unknown
4039
+ });
4040
+ process.stderr.write(`INTEGRITY_FAIL ${record}
4041
+ `);
4042
+ }
3371
4043
  function registerIntegrityTools(server2) {
3372
4044
  server2.tool(
3373
4045
  "stackwright_pro_verify_otter_integrity",
@@ -3391,6 +4063,13 @@ function registerIntegrityTools(server2) {
3391
4063
  }
3392
4064
  const result = verifyAllOtters(resolved);
3393
4065
  const allGood = result.failed.length === 0 && result.unknown.length === 0;
4066
+ if (!allGood) {
4067
+ emitIntegrityAuditEvent({
4068
+ otterDir: resolved,
4069
+ failed: result.failed,
4070
+ unknown: result.unknown
4071
+ });
4072
+ }
3394
4073
  return {
3395
4074
  content: [
3396
4075
  {
@@ -3404,25 +4083,27 @@ function registerIntegrityTools(server2) {
3404
4083
  verified: result.verified,
3405
4084
  failed: result.failed,
3406
4085
  unknown: result.unknown,
3407
- warning: result.failed.length > 0 ? "SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon." : void 0
4086
+ ...allGood ? {} : {
4087
+ error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
4088
+ }
3408
4089
  })
3409
4090
  }
3410
4091
  ],
3411
- isError: false
4092
+ isError: !allGood
3412
4093
  };
3413
4094
  }
3414
4095
  );
3415
4096
  }
3416
4097
 
3417
4098
  // src/tools/domain.ts
3418
- import { z as z13 } from "zod";
3419
- import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
3420
- import { join as join7 } from "path";
4099
+ import { z as z14 } from "zod";
4100
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
4101
+ import { join as join8 } from "path";
3421
4102
  function handleListCollections(input) {
3422
4103
  const cwd = input._cwd ?? process.cwd();
3423
4104
  const sources = [
3424
4105
  {
3425
- path: join7(cwd, ".stackwright", "artifacts", "data-config.json"),
4106
+ path: join8(cwd, ".stackwright", "artifacts", "data-config.json"),
3426
4107
  source: "data-config.json",
3427
4108
  parse: (raw) => {
3428
4109
  const parsed = JSON.parse(raw);
@@ -3433,15 +4114,15 @@ function handleListCollections(input) {
3433
4114
  }
3434
4115
  },
3435
4116
  {
3436
- path: join7(cwd, "stackwright.yml"),
4117
+ path: join8(cwd, "stackwright.yml"),
3437
4118
  source: "stackwright.yml",
3438
4119
  parse: extractCollectionsFromYaml
3439
4120
  }
3440
4121
  ];
3441
4122
  for (const { path: path3, source, parse } of sources) {
3442
- if (!existsSync7(path3)) continue;
4123
+ if (!existsSync8(path3)) continue;
3443
4124
  try {
3444
- const collections = parse(readFileSync6(path3, "utf8"));
4125
+ const collections = parse(readFileSync7(path3, "utf8"));
3445
4126
  return {
3446
4127
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
3447
4128
  isError: false
@@ -3592,8 +4273,8 @@ function handleValidateWorkflow(input) {
3592
4273
  if (input.workflow && Object.keys(input.workflow).length > 0) {
3593
4274
  raw = input.workflow;
3594
4275
  } else {
3595
- const artifactPath = join7(cwd, ".stackwright", "artifacts", "workflow-config.json");
3596
- if (!existsSync7(artifactPath)) {
4276
+ const artifactPath = join8(cwd, ".stackwright", "artifacts", "workflow-config.json");
4277
+ if (!existsSync8(artifactPath)) {
3597
4278
  return fail([
3598
4279
  {
3599
4280
  code: "NO_WORKFLOW",
@@ -3602,7 +4283,7 @@ function handleValidateWorkflow(input) {
3602
4283
  ]);
3603
4284
  }
3604
4285
  try {
3605
- raw = JSON.parse(readFileSync6(artifactPath, "utf8"));
4286
+ raw = JSON.parse(readFileSync7(artifactPath, "utf8"));
3606
4287
  } catch (err) {
3607
4288
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
3608
4289
  }
@@ -3801,7 +4482,7 @@ function registerDomainTools(server2) {
3801
4482
  "stackwright_pro_resolve_data_strategy",
3802
4483
  "Look up the data freshness strategy configuration from the user's answer. Returns mechanism, revalidation seconds, required packages, and the exact MCP tool call to make. Replaces the strategy table in the data-otter prompt.",
3803
4484
  {
3804
- strategy: z13.string().describe(
4485
+ strategy: z14.string().describe(
3805
4486
  'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
3806
4487
  )
3807
4488
  },
@@ -3811,7 +4492,7 @@ function registerDomainTools(server2) {
3811
4492
  "stackwright_pro_validate_workflow",
3812
4493
  "Validate a workflow definition against the Stackwright workflow schema. Checks step ID uniqueness, transition targets, terminal state existence, and service references. Call this after the workflow otter produces output.",
3813
4494
  {
3814
- workflow: jsonCoerce(z13.record(z13.string(), z13.unknown()).optional()).describe(
4495
+ workflow: jsonCoerce(z14.record(z14.string(), z14.unknown()).optional()).describe(
3815
4496
  "Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
3816
4497
  )
3817
4498
  },
@@ -3820,7 +4501,7 @@ function registerDomainTools(server2) {
3820
4501
  }
3821
4502
 
3822
4503
  // src/tools/type-schemas.ts
3823
- import { z as z14 } from "zod";
4504
+ import { z as z15 } from "zod";
3824
4505
  function buildTypeSchemaSummary() {
3825
4506
  return {
3826
4507
  version: "1.0",
@@ -3897,7 +4578,7 @@ function registerTypeSchemasTool(server2) {
3897
4578
  "stackwright_pro_get_type_schemas",
3898
4579
  "Returns a structured summary of all canonical @stackwright-pro/types schemas, organized by domain. Use this to determine which otter owns a given schema and what artifact key to expect.",
3899
4580
  {
3900
- format: z14.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
4581
+ format: z15.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
3901
4582
  },
3902
4583
  async ({ format }) => {
3903
4584
  const summary = buildTypeSchemaSummary();
@@ -3915,13 +4596,13 @@ var package_default = {
3915
4596
  "@stackwright-pro/types": "workspace:*",
3916
4597
  "@modelcontextprotocol/sdk": "^1.10.0",
3917
4598
  "@stackwright-pro/cli-data-explorer": "workspace:*",
3918
- zod: "^4.3.6"
4599
+ zod: "^4.4.3"
3919
4600
  },
3920
4601
  devDependencies: {
3921
- "@types/node": "^24.1.0",
3922
- tsup: "^8.5.0",
3923
- typescript: "^5.8.3",
3924
- vitest: "^4.0.18"
4602
+ "@types/node": "catalog:",
4603
+ tsup: "catalog:",
4604
+ typescript: "catalog:",
4605
+ vitest: "catalog:"
3925
4606
  },
3926
4607
  scripts: {
3927
4608
  prepublishOnly: "node scripts/verify-integrity-sync.js",
@@ -3932,9 +4613,9 @@ var package_default = {
3932
4613
  "test:coverage": "vitest run --coverage"
3933
4614
  },
3934
4615
  name: "@stackwright-pro/mcp",
3935
- version: "0.2.0-alpha.32",
4616
+ version: "0.2.0-alpha.48",
3936
4617
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3937
- license: "PROPRIETARY",
4618
+ license: "SEE LICENSE IN LICENSE",
3938
4619
  main: "./dist/server.js",
3939
4620
  bin: {
3940
4621
  "stackwright-pro-mcp": "./dist/server.js"
@@ -3983,6 +4664,7 @@ registerPipelineTools(server);
3983
4664
  registerSafeWriteTools(server);
3984
4665
  registerAuthTools(server);
3985
4666
  registerIntegrityTools(server);
4667
+ registerArtifactSigningTools(server);
3986
4668
  registerDomainTools(server);
3987
4669
  registerTypeSchemasTool(server);
3988
4670
  async function main() {