@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/integrity.d.mts +29 -1
- package/dist/integrity.d.ts +29 -1
- package/dist/integrity.js +46 -8
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +45 -8
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +813 -147
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +826 -144
- package/dist/server.mjs.map +1 -1
- package/package.json +6 -6
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
|
|
1877
|
-
import { readFileSync as
|
|
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
|
|
2314
|
+
return join4(cwd, ".stackwright", "pipeline-state.json");
|
|
1955
2315
|
}
|
|
1956
2316
|
function readState(cwd) {
|
|
1957
2317
|
const p = statePath(cwd);
|
|
1958
|
-
if (!
|
|
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 (
|
|
1967
|
-
const stat =
|
|
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
|
-
|
|
2332
|
+
writeFileSync4(filePath, content);
|
|
1973
2333
|
}
|
|
1974
2334
|
function safeReadSync(filePath) {
|
|
1975
|
-
if (
|
|
1976
|
-
const stat =
|
|
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
|
|
2341
|
+
return readFileSync4(filePath, "utf-8");
|
|
1982
2342
|
}
|
|
1983
2343
|
function writeState(cwd, state) {
|
|
1984
|
-
const dir =
|
|
1985
|
-
|
|
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 =
|
|
2070
|
-
if (!
|
|
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 =
|
|
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 =
|
|
2099
|
-
if (
|
|
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 =
|
|
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 =
|
|
2137
|
-
const exists =
|
|
2510
|
+
const fullPath = join4(artifactsDir, expectedFile);
|
|
2511
|
+
const exists = existsSync5(fullPath);
|
|
2138
2512
|
if (exists) completedCount++;
|
|
2139
|
-
|
|
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 =
|
|
2176
|
-
|
|
2177
|
-
const filePath =
|
|
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 =
|
|
2606
|
+
const answersPath = join4(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2218
2607
|
let answers = {};
|
|
2219
|
-
if (
|
|
2608
|
+
if (existsSync5(answersPath)) {
|
|
2220
2609
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
2221
2610
|
}
|
|
2222
2611
|
let buildContextText = "";
|
|
2223
|
-
const buildContextPath =
|
|
2224
|
-
if (
|
|
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 =
|
|
2239
|
-
if (
|
|
2240
|
-
const
|
|
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
|
-
|
|
2258
|
-
|
|
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 =
|
|
2409
|
-
|
|
3014
|
+
const artifactsDir = join4(cwd, ".stackwright", "artifacts");
|
|
3015
|
+
mkdirSync4(artifactsDir, { recursive: true });
|
|
2410
3016
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2411
|
-
const artifactPath =
|
|
2412
|
-
|
|
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:
|
|
2448
|
-
field:
|
|
2449
|
-
value: boolCoerce(
|
|
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:
|
|
2453
|
-
incrementRetry: boolCoerce(
|
|
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:
|
|
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:
|
|
2486
|
-
responseText:
|
|
2487
|
-
questions: jsonCoerce(
|
|
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 =
|
|
2503
|
-
|
|
2504
|
-
const outPath =
|
|
2505
|
-
|
|
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:
|
|
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:
|
|
2538
|
-
responseText:
|
|
2539
|
-
artifact:
|
|
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
|
|
2556
|
-
import { writeFileSync as
|
|
2557
|
-
import { normalize, isAbsolute, dirname, join as
|
|
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 =
|
|
2730
|
-
if (
|
|
3374
|
+
const fullPath = join5(cwd, normalized);
|
|
3375
|
+
if (existsSync6(fullPath)) {
|
|
2731
3376
|
try {
|
|
2732
|
-
const stat =
|
|
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
|
-
|
|
3404
|
+
mkdirSync5(dirname(fullPath), { recursive: true });
|
|
2760
3405
|
}
|
|
2761
|
-
|
|
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:
|
|
2788
|
-
filePath:
|
|
2789
|
-
content:
|
|
2790
|
-
createDirectories: boolCoerce(
|
|
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
|
|
2808
|
-
import { readFileSync as
|
|
2809
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
3088
|
-
if (
|
|
3089
|
-
const existing =
|
|
3090
|
-
|
|
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
|
-
|
|
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 =
|
|
3119
|
-
if (!
|
|
3120
|
-
|
|
3763
|
+
const ymlPath = join6(cwd, "stackwright.yml");
|
|
3764
|
+
if (!existsSync7(ymlPath)) {
|
|
3765
|
+
writeFileSync6(ymlPath, authYaml, "utf8");
|
|
3121
3766
|
} else {
|
|
3122
|
-
const existing =
|
|
3123
|
-
|
|
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:
|
|
3163
|
-
provider:
|
|
3807
|
+
method: z13.enum(["cac", "oidc", "oauth2", "none"]),
|
|
3808
|
+
provider: z13.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
|
|
3164
3809
|
// CAC
|
|
3165
|
-
cacCaBundle:
|
|
3166
|
-
cacEdipiLookup:
|
|
3167
|
-
cacOcspEndpoint:
|
|
3168
|
-
cacCertHeader:
|
|
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:
|
|
3171
|
-
oidcClientId:
|
|
3172
|
-
oidcClientSecret:
|
|
3173
|
-
oidcScopes:
|
|
3174
|
-
oidcRoleClaim:
|
|
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:
|
|
3177
|
-
oauth2TokenUrl:
|
|
3178
|
-
oauth2ClientId:
|
|
3179
|
-
oauth2ClientSecret:
|
|
3180
|
-
oauth2Scopes:
|
|
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(
|
|
3183
|
-
rbacDefaultRole:
|
|
3827
|
+
rbacRoles: jsonCoerce(z13.array(z13.string()).optional()),
|
|
3828
|
+
rbacDefaultRole: z13.string().optional(),
|
|
3184
3829
|
// Audit
|
|
3185
|
-
auditEnabled: boolCoerce(
|
|
3186
|
-
auditRetentionDays: numCoerce(
|
|
3830
|
+
auditEnabled: boolCoerce(z13.boolean().optional()),
|
|
3831
|
+
auditRetentionDays: numCoerce(z13.number().int().positive().optional()),
|
|
3187
3832
|
// Routes
|
|
3188
|
-
protectedRoutes: jsonCoerce(
|
|
3833
|
+
protectedRoutes: jsonCoerce(z13.array(z13.string()).optional()),
|
|
3189
3834
|
// Injection for tests
|
|
3190
|
-
_cwd:
|
|
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
|
|
3201
|
-
import { readFileSync as
|
|
3202
|
-
import { join as
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
3867
|
+
"e80d4e7bab87d8647debadb238a58aac498ec5074ff25b21abd3b13ff778bf71"
|
|
3223
3868
|
],
|
|
3224
3869
|
[
|
|
3225
3870
|
"stackwright-pro-foreman-otter.json",
|
|
3226
|
-
"
|
|
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
|
-
"
|
|
3879
|
+
"d3a15871b71a466c12c4711fe37cbb018c768cb99eff15c40dbbc7061d4e966b"
|
|
3235
3880
|
],
|
|
3236
3881
|
[
|
|
3237
3882
|
"stackwright-pro-workflow-otter.json",
|
|
3238
|
-
"
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3991
|
+
const filePath = join7(otterDir, filename);
|
|
3335
3992
|
try {
|
|
3336
|
-
if (
|
|
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 =
|
|
4019
|
+
const candidate = join7(cwd, relative);
|
|
3363
4020
|
try {
|
|
3364
|
-
|
|
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
|
-
|
|
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:
|
|
4092
|
+
isError: !allGood
|
|
3412
4093
|
};
|
|
3413
4094
|
}
|
|
3414
4095
|
);
|
|
3415
4096
|
}
|
|
3416
4097
|
|
|
3417
4098
|
// src/tools/domain.ts
|
|
3418
|
-
import { z as
|
|
3419
|
-
import { readFileSync as
|
|
3420
|
-
import { join as
|
|
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:
|
|
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:
|
|
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 (!
|
|
4123
|
+
if (!existsSync8(path3)) continue;
|
|
3443
4124
|
try {
|
|
3444
|
-
const collections = parse(
|
|
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 =
|
|
3596
|
-
if (!
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
4599
|
+
zod: "^4.4.3"
|
|
3919
4600
|
},
|
|
3920
4601
|
devDependencies: {
|
|
3921
|
-
"@types/node": "
|
|
3922
|
-
tsup: "
|
|
3923
|
-
typescript: "
|
|
3924
|
-
vitest: "
|
|
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.
|
|
4616
|
+
version: "0.2.0-alpha.48",
|
|
3936
4617
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3937
|
-
license: "
|
|
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() {
|