@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.js
CHANGED
|
@@ -1402,6 +1402,25 @@ function registerQuestionTools(server2) {
|
|
|
1402
1402
|
}
|
|
1403
1403
|
}
|
|
1404
1404
|
const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
|
|
1405
|
+
const labelSchema = import_zod8.z.string().max(50, "Value should have at most 50 characters");
|
|
1406
|
+
for (const q of adapted) {
|
|
1407
|
+
for (const opt of q.options) {
|
|
1408
|
+
const check = labelSchema.safeParse(opt.label);
|
|
1409
|
+
if (!check.success) {
|
|
1410
|
+
return {
|
|
1411
|
+
content: [
|
|
1412
|
+
{
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: JSON.stringify({
|
|
1415
|
+
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.`
|
|
1416
|
+
})
|
|
1417
|
+
}
|
|
1418
|
+
],
|
|
1419
|
+
isError: true
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1405
1424
|
if (adapted.length === 0) {
|
|
1406
1425
|
return {
|
|
1407
1426
|
content: [
|
|
@@ -1897,9 +1916,334 @@ function registerOrchestrationTools(server2) {
|
|
|
1897
1916
|
}
|
|
1898
1917
|
|
|
1899
1918
|
// src/tools/pipeline.ts
|
|
1900
|
-
var
|
|
1919
|
+
var import_zod11 = require("zod");
|
|
1920
|
+
var import_fs5 = require("fs");
|
|
1921
|
+
var import_path5 = require("path");
|
|
1922
|
+
var import_crypto3 = require("crypto");
|
|
1923
|
+
|
|
1924
|
+
// src/artifact-signing.ts
|
|
1925
|
+
var import_crypto2 = require("crypto");
|
|
1901
1926
|
var import_fs4 = require("fs");
|
|
1902
1927
|
var import_path4 = require("path");
|
|
1928
|
+
var import_zod10 = require("zod");
|
|
1929
|
+
var ALGORITHM = "ECDSA-P384-SHA384";
|
|
1930
|
+
var KEY_FILE = "pipeline-keys.json";
|
|
1931
|
+
var KEY_DIR = ".stackwright";
|
|
1932
|
+
var SIGNATURE_MANIFEST = "signatures.json";
|
|
1933
|
+
var ARTIFACTS_DIR = ".stackwright/artifacts";
|
|
1934
|
+
function rejectSymlink(filePath, context) {
|
|
1935
|
+
if (!(0, import_fs4.existsSync)(filePath)) return;
|
|
1936
|
+
const stat = (0, import_fs4.lstatSync)(filePath);
|
|
1937
|
+
if (stat.isSymbolicLink()) {
|
|
1938
|
+
throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function computeSha384(data) {
|
|
1942
|
+
return (0, import_crypto2.createHash)("sha384").update(data).digest("hex");
|
|
1943
|
+
}
|
|
1944
|
+
function safeDigestEqual(a, b) {
|
|
1945
|
+
if (a.length !== b.length) return false;
|
|
1946
|
+
return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
1947
|
+
}
|
|
1948
|
+
function emptyManifest() {
|
|
1949
|
+
return {
|
|
1950
|
+
version: "1.0",
|
|
1951
|
+
algorithm: ALGORITHM,
|
|
1952
|
+
signatures: {}
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
function initPipelineKeys(cwd) {
|
|
1956
|
+
const keyDir = (0, import_path4.join)(cwd, KEY_DIR);
|
|
1957
|
+
const keyPath = (0, import_path4.join)(keyDir, KEY_FILE);
|
|
1958
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
1959
|
+
(0, import_fs4.mkdirSync)(keyDir, { recursive: true });
|
|
1960
|
+
const { publicKey, privateKey } = (0, import_crypto2.generateKeyPairSync)("ec", {
|
|
1961
|
+
namedCurve: "P-384"
|
|
1962
|
+
});
|
|
1963
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
1964
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
1965
|
+
const fingerprint = (0, import_crypto2.createHash)("sha256").update(publicKeyPem).digest("hex");
|
|
1966
|
+
const keyFile = {
|
|
1967
|
+
version: "1.0",
|
|
1968
|
+
algorithm: ALGORITHM,
|
|
1969
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1970
|
+
publicKeyPem,
|
|
1971
|
+
privateKeyPem
|
|
1972
|
+
};
|
|
1973
|
+
(0, import_fs4.writeFileSync)(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
|
|
1974
|
+
return { publicKeyPem, fingerprint };
|
|
1975
|
+
}
|
|
1976
|
+
function loadPipelineKeys(cwd) {
|
|
1977
|
+
const keyPath = (0, import_path4.join)(cwd, KEY_DIR, KEY_FILE);
|
|
1978
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
1979
|
+
if (!(0, import_fs4.existsSync)(keyPath)) {
|
|
1980
|
+
throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
|
|
1981
|
+
}
|
|
1982
|
+
let raw;
|
|
1983
|
+
try {
|
|
1984
|
+
raw = (0, import_fs4.readFileSync)(keyPath, "utf-8");
|
|
1985
|
+
} catch (err) {
|
|
1986
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1987
|
+
throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
|
|
1988
|
+
}
|
|
1989
|
+
let parsed;
|
|
1990
|
+
try {
|
|
1991
|
+
parsed = JSON.parse(raw);
|
|
1992
|
+
} catch {
|
|
1993
|
+
throw new Error("Pipeline keys file is not valid JSON");
|
|
1994
|
+
}
|
|
1995
|
+
if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
1996
|
+
throw new Error("Invalid public key PEM in pipeline keys file");
|
|
1997
|
+
}
|
|
1998
|
+
if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
|
|
1999
|
+
throw new Error("Invalid private key PEM in pipeline keys file");
|
|
2000
|
+
}
|
|
2001
|
+
const publicKey = (0, import_crypto2.createPublicKey)(parsed.publicKeyPem);
|
|
2002
|
+
const privateKey = (0, import_crypto2.createPrivateKey)(parsed.privateKeyPem);
|
|
2003
|
+
return { privateKey, publicKey };
|
|
2004
|
+
}
|
|
2005
|
+
function signArtifact(artifactBytes, privateKey) {
|
|
2006
|
+
const digest = computeSha384(artifactBytes);
|
|
2007
|
+
const sig = (0, import_crypto2.sign)("SHA384", artifactBytes, privateKey);
|
|
2008
|
+
return {
|
|
2009
|
+
digest,
|
|
2010
|
+
signature: sig.toString("base64"),
|
|
2011
|
+
algorithm: ALGORITHM,
|
|
2012
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
function verifyArtifact(artifactBytes, signature, publicKey) {
|
|
2016
|
+
const sigValid = (0, import_crypto2.verify)(
|
|
2017
|
+
"SHA384",
|
|
2018
|
+
artifactBytes,
|
|
2019
|
+
publicKey,
|
|
2020
|
+
Buffer.from(signature.signature, "base64")
|
|
2021
|
+
);
|
|
2022
|
+
if (!sigValid) return false;
|
|
2023
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2024
|
+
return safeDigestEqual(actualDigest, signature.digest);
|
|
2025
|
+
}
|
|
2026
|
+
function loadSignatureManifest(cwd) {
|
|
2027
|
+
const manifestPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
|
|
2028
|
+
rejectSymlink(manifestPath, "signatures.json");
|
|
2029
|
+
if (!(0, import_fs4.existsSync)(manifestPath)) return emptyManifest();
|
|
2030
|
+
let raw;
|
|
2031
|
+
try {
|
|
2032
|
+
raw = (0, import_fs4.readFileSync)(manifestPath, "utf-8");
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2035
|
+
throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
|
|
2036
|
+
}
|
|
2037
|
+
try {
|
|
2038
|
+
const parsed = JSON.parse(raw);
|
|
2039
|
+
if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
|
|
2040
|
+
throw new Error("Malformed signature manifest: invalid version or missing signatures object");
|
|
2041
|
+
}
|
|
2042
|
+
return parsed;
|
|
2043
|
+
} catch (err) {
|
|
2044
|
+
if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
|
|
2045
|
+
throw new Error("Signature manifest is not valid JSON", { cause: err });
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
|
|
2049
|
+
const artifactsDir = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
|
|
2050
|
+
const manifestPath = (0, import_path4.join)(artifactsDir, SIGNATURE_MANIFEST);
|
|
2051
|
+
rejectSymlink(manifestPath, "signatures.json (save)");
|
|
2052
|
+
(0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
|
|
2053
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2054
|
+
manifest.signatures[artifactFilename] = {
|
|
2055
|
+
...sig,
|
|
2056
|
+
signedBy: signerOtter
|
|
2057
|
+
};
|
|
2058
|
+
(0, import_fs4.writeFileSync)(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
|
|
2059
|
+
}
|
|
2060
|
+
function getArtifactSignature(cwd, artifactFilename) {
|
|
2061
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2062
|
+
const entry = manifest.signatures[artifactFilename];
|
|
2063
|
+
if (!entry) return null;
|
|
2064
|
+
return {
|
|
2065
|
+
digest: entry.digest,
|
|
2066
|
+
signature: entry.signature,
|
|
2067
|
+
algorithm: entry.algorithm,
|
|
2068
|
+
signedAt: entry.signedAt
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function emitSignatureAuditEvent(params) {
|
|
2072
|
+
const record = JSON.stringify({
|
|
2073
|
+
level: "AUDIT",
|
|
2074
|
+
event: "ARTIFACT_SIGNATURE_FAIL",
|
|
2075
|
+
timestamp: params.timestamp,
|
|
2076
|
+
source: params.source,
|
|
2077
|
+
artifactFilename: params.artifactFilename,
|
|
2078
|
+
expectedDigest: params.expectedDigest,
|
|
2079
|
+
actualDigest: params.actualDigest,
|
|
2080
|
+
phase: params.phase
|
|
2081
|
+
});
|
|
2082
|
+
process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
|
|
2083
|
+
`);
|
|
2084
|
+
}
|
|
2085
|
+
function registerArtifactSigningTools(server2) {
|
|
2086
|
+
server2.tool(
|
|
2087
|
+
"stackwright_pro_verify_artifact_signatures",
|
|
2088
|
+
"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.",
|
|
2089
|
+
{
|
|
2090
|
+
cwd: import_zod10.z.string().optional().describe("Project root directory. Defaults to process.cwd().")
|
|
2091
|
+
},
|
|
2092
|
+
async ({ cwd: cwdParam }) => {
|
|
2093
|
+
const cwd = cwdParam ?? process.cwd();
|
|
2094
|
+
let publicKey;
|
|
2095
|
+
try {
|
|
2096
|
+
const keys = loadPipelineKeys(cwd);
|
|
2097
|
+
publicKey = keys.publicKey;
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2100
|
+
return {
|
|
2101
|
+
content: [
|
|
2102
|
+
{
|
|
2103
|
+
type: "text",
|
|
2104
|
+
text: JSON.stringify({
|
|
2105
|
+
error: true,
|
|
2106
|
+
message: `Cannot load pipeline keys: ${msg}`
|
|
2107
|
+
})
|
|
2108
|
+
}
|
|
2109
|
+
],
|
|
2110
|
+
isError: true
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
let manifest;
|
|
2114
|
+
try {
|
|
2115
|
+
manifest = loadSignatureManifest(cwd);
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2118
|
+
return {
|
|
2119
|
+
content: [
|
|
2120
|
+
{
|
|
2121
|
+
type: "text",
|
|
2122
|
+
text: JSON.stringify({
|
|
2123
|
+
error: true,
|
|
2124
|
+
message: `Cannot load signature manifest: ${msg}`
|
|
2125
|
+
})
|
|
2126
|
+
}
|
|
2127
|
+
],
|
|
2128
|
+
isError: true
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
const artifactsPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
|
|
2132
|
+
let artifactFiles = [];
|
|
2133
|
+
try {
|
|
2134
|
+
if ((0, import_fs4.existsSync)(artifactsPath)) {
|
|
2135
|
+
artifactFiles = (0, import_fs4.readdirSync)(artifactsPath).filter(
|
|
2136
|
+
(f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
} catch {
|
|
2140
|
+
}
|
|
2141
|
+
const results = [];
|
|
2142
|
+
let hasFailure = false;
|
|
2143
|
+
for (const filename of artifactFiles) {
|
|
2144
|
+
const filePath = (0, import_path4.join)(artifactsPath, filename);
|
|
2145
|
+
try {
|
|
2146
|
+
rejectSymlink(filePath, `artifact ${filename}`);
|
|
2147
|
+
} catch {
|
|
2148
|
+
results.push({
|
|
2149
|
+
filename,
|
|
2150
|
+
verified: false,
|
|
2151
|
+
error: "Refusing to verify symlink"
|
|
2152
|
+
});
|
|
2153
|
+
hasFailure = true;
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
let artifactBytes;
|
|
2157
|
+
try {
|
|
2158
|
+
artifactBytes = (0, import_fs4.readFileSync)(filePath);
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2161
|
+
results.push({
|
|
2162
|
+
filename,
|
|
2163
|
+
verified: false,
|
|
2164
|
+
error: `Cannot read artifact: ${msg}`
|
|
2165
|
+
});
|
|
2166
|
+
hasFailure = true;
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
const entry = manifest.signatures[filename];
|
|
2170
|
+
if (!entry) {
|
|
2171
|
+
results.push({
|
|
2172
|
+
filename,
|
|
2173
|
+
verified: false,
|
|
2174
|
+
error: "No signature found in manifest"
|
|
2175
|
+
});
|
|
2176
|
+
hasFailure = true;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const sig = {
|
|
2180
|
+
digest: entry.digest,
|
|
2181
|
+
signature: entry.signature,
|
|
2182
|
+
algorithm: entry.algorithm,
|
|
2183
|
+
signedAt: entry.signedAt
|
|
2184
|
+
};
|
|
2185
|
+
const verified = verifyArtifact(artifactBytes, sig, publicKey);
|
|
2186
|
+
if (!verified) {
|
|
2187
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2188
|
+
emitSignatureAuditEvent({
|
|
2189
|
+
artifactFilename: filename,
|
|
2190
|
+
expectedDigest: sig.digest,
|
|
2191
|
+
actualDigest,
|
|
2192
|
+
phase: entry.signedBy ?? "unknown",
|
|
2193
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2194
|
+
source: "stackwright_pro_verify_artifact_signatures"
|
|
2195
|
+
});
|
|
2196
|
+
results.push({
|
|
2197
|
+
filename,
|
|
2198
|
+
verified: false,
|
|
2199
|
+
error: `Signature verification failed \u2014 artifact may have been tampered with`,
|
|
2200
|
+
signedBy: entry.signedBy,
|
|
2201
|
+
signedAt: entry.signedAt
|
|
2202
|
+
});
|
|
2203
|
+
hasFailure = true;
|
|
2204
|
+
} else {
|
|
2205
|
+
results.push({
|
|
2206
|
+
filename,
|
|
2207
|
+
verified: true,
|
|
2208
|
+
signedBy: entry.signedBy,
|
|
2209
|
+
signedAt: entry.signedAt
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
for (const manifestFilename of Object.keys(manifest.signatures)) {
|
|
2214
|
+
if (!artifactFiles.includes(manifestFilename)) {
|
|
2215
|
+
results.push({
|
|
2216
|
+
filename: manifestFilename,
|
|
2217
|
+
verified: false,
|
|
2218
|
+
error: "Artifact referenced in manifest but missing from disk"
|
|
2219
|
+
});
|
|
2220
|
+
hasFailure = true;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
const verifiedCount = results.filter((r) => r.verified).length;
|
|
2224
|
+
const failedCount = results.filter((r) => !r.verified).length;
|
|
2225
|
+
return {
|
|
2226
|
+
content: [
|
|
2227
|
+
{
|
|
2228
|
+
type: "text",
|
|
2229
|
+
text: JSON.stringify({
|
|
2230
|
+
totalArtifacts: artifactFiles.length,
|
|
2231
|
+
verifiedCount,
|
|
2232
|
+
failedCount,
|
|
2233
|
+
results,
|
|
2234
|
+
...hasFailure ? {
|
|
2235
|
+
error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
|
|
2236
|
+
} : {}
|
|
2237
|
+
})
|
|
2238
|
+
}
|
|
2239
|
+
],
|
|
2240
|
+
isError: hasFailure
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// src/tools/pipeline.ts
|
|
1903
2247
|
var import_types = require("@stackwright-pro/types");
|
|
1904
2248
|
var PHASE_ORDER = [
|
|
1905
2249
|
"designer",
|
|
@@ -1975,11 +2319,11 @@ function createDefaultState() {
|
|
|
1975
2319
|
};
|
|
1976
2320
|
}
|
|
1977
2321
|
function statePath(cwd) {
|
|
1978
|
-
return (0,
|
|
2322
|
+
return (0, import_path5.join)(cwd, ".stackwright", "pipeline-state.json");
|
|
1979
2323
|
}
|
|
1980
2324
|
function readState(cwd) {
|
|
1981
2325
|
const p = statePath(cwd);
|
|
1982
|
-
if (!(0,
|
|
2326
|
+
if (!(0, import_fs5.existsSync)(p)) return createDefaultState();
|
|
1983
2327
|
const raw = JSON.parse(safeReadSync(p));
|
|
1984
2328
|
if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
|
|
1985
2329
|
return createDefaultState();
|
|
@@ -1987,26 +2331,26 @@ function readState(cwd) {
|
|
|
1987
2331
|
return raw;
|
|
1988
2332
|
}
|
|
1989
2333
|
function safeWriteSync(filePath, content) {
|
|
1990
|
-
if ((0,
|
|
1991
|
-
const stat = (0,
|
|
2334
|
+
if ((0, import_fs5.existsSync)(filePath)) {
|
|
2335
|
+
const stat = (0, import_fs5.lstatSync)(filePath);
|
|
1992
2336
|
if (stat.isSymbolicLink()) {
|
|
1993
2337
|
throw new Error(`Refusing to write to symlink: ${filePath}`);
|
|
1994
2338
|
}
|
|
1995
2339
|
}
|
|
1996
|
-
(0,
|
|
2340
|
+
(0, import_fs5.writeFileSync)(filePath, content);
|
|
1997
2341
|
}
|
|
1998
2342
|
function safeReadSync(filePath) {
|
|
1999
|
-
if ((0,
|
|
2000
|
-
const stat = (0,
|
|
2343
|
+
if ((0, import_fs5.existsSync)(filePath)) {
|
|
2344
|
+
const stat = (0, import_fs5.lstatSync)(filePath);
|
|
2001
2345
|
if (stat.isSymbolicLink()) {
|
|
2002
2346
|
throw new Error(`Refusing to read symlink: ${filePath}`);
|
|
2003
2347
|
}
|
|
2004
2348
|
}
|
|
2005
|
-
return (0,
|
|
2349
|
+
return (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
2006
2350
|
}
|
|
2007
2351
|
function writeState(cwd, state) {
|
|
2008
|
-
const dir = (0,
|
|
2009
|
-
(0,
|
|
2352
|
+
const dir = (0, import_path5.join)(cwd, ".stackwright");
|
|
2353
|
+
(0, import_fs5.mkdirSync)(dir, { recursive: true });
|
|
2010
2354
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2011
2355
|
safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
2012
2356
|
}
|
|
@@ -2027,6 +2371,15 @@ function handleGetPipelineState(_cwd) {
|
|
|
2027
2371
|
const cwd = _cwd ?? process.cwd();
|
|
2028
2372
|
try {
|
|
2029
2373
|
const state = readState(cwd);
|
|
2374
|
+
const keyPath = (0, import_path5.join)(cwd, ".stackwright", "pipeline-keys.json");
|
|
2375
|
+
if (!(0, import_fs5.existsSync)(keyPath)) {
|
|
2376
|
+
try {
|
|
2377
|
+
const { fingerprint } = initPipelineKeys(cwd);
|
|
2378
|
+
state["signingKeyFingerprint"] = fingerprint;
|
|
2379
|
+
writeState(cwd, state);
|
|
2380
|
+
} catch {
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2030
2383
|
return { text: JSON.stringify(state), isError: false };
|
|
2031
2384
|
} catch (err) {
|
|
2032
2385
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
@@ -2090,8 +2443,8 @@ function handleCheckExecutionReady(_cwd, phase) {
|
|
|
2090
2443
|
isError: true
|
|
2091
2444
|
};
|
|
2092
2445
|
}
|
|
2093
|
-
const answerFile = (0,
|
|
2094
|
-
if (!(0,
|
|
2446
|
+
const answerFile = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2447
|
+
if (!(0, import_fs5.existsSync)(answerFile)) {
|
|
2095
2448
|
return {
|
|
2096
2449
|
text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
|
|
2097
2450
|
isError: false
|
|
@@ -2115,12 +2468,12 @@ function handleCheckExecutionReady(_cwd, phase) {
|
|
|
2115
2468
|
}
|
|
2116
2469
|
}
|
|
2117
2470
|
try {
|
|
2118
|
-
const answersDir = (0,
|
|
2471
|
+
const answersDir = (0, import_path5.join)(cwd, ".stackwright", "answers");
|
|
2119
2472
|
const answeredPhases = [];
|
|
2120
2473
|
const missingPhases = [];
|
|
2121
2474
|
for (const phase2 of PHASE_ORDER) {
|
|
2122
|
-
const answerFile = (0,
|
|
2123
|
-
if ((0,
|
|
2475
|
+
const answerFile = (0, import_path5.join)(answersDir, `${phase2}.json`);
|
|
2476
|
+
if ((0, import_fs5.existsSync)(answerFile)) {
|
|
2124
2477
|
try {
|
|
2125
2478
|
const raw = safeReadSync(answerFile);
|
|
2126
2479
|
const parsed = JSON.parse(raw);
|
|
@@ -2152,15 +2505,35 @@ function handleCheckExecutionReady(_cwd, phase) {
|
|
|
2152
2505
|
function handleListArtifacts(_cwd) {
|
|
2153
2506
|
const cwd = _cwd ?? process.cwd();
|
|
2154
2507
|
try {
|
|
2155
|
-
const artifactsDir = (0,
|
|
2508
|
+
const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
|
|
2509
|
+
let manifest = null;
|
|
2510
|
+
try {
|
|
2511
|
+
manifest = loadSignatureManifest(cwd);
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2156
2514
|
const artifacts = [];
|
|
2157
2515
|
let completedCount = 0;
|
|
2158
2516
|
for (const phase of PHASE_ORDER) {
|
|
2159
2517
|
const expectedFile = PHASE_ARTIFACT[phase];
|
|
2160
|
-
const fullPath = (0,
|
|
2161
|
-
const exists = (0,
|
|
2518
|
+
const fullPath = (0, import_path5.join)(artifactsDir, expectedFile);
|
|
2519
|
+
const exists = (0, import_fs5.existsSync)(fullPath);
|
|
2162
2520
|
if (exists) completedCount++;
|
|
2163
|
-
|
|
2521
|
+
let signed = false;
|
|
2522
|
+
let signatureValid = null;
|
|
2523
|
+
if (exists && manifest) {
|
|
2524
|
+
const entry = manifest.signatures[expectedFile];
|
|
2525
|
+
if (entry) {
|
|
2526
|
+
signed = true;
|
|
2527
|
+
try {
|
|
2528
|
+
const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
|
|
2529
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
2530
|
+
signatureValid = verifyArtifact(rawBytes, entry, publicKey);
|
|
2531
|
+
} catch {
|
|
2532
|
+
signatureValid = null;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
|
|
2164
2537
|
}
|
|
2165
2538
|
return {
|
|
2166
2539
|
text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
|
|
@@ -2196,9 +2569,9 @@ function handleWritePhaseQuestions(input) {
|
|
|
2196
2569
|
}
|
|
2197
2570
|
} catch {
|
|
2198
2571
|
}
|
|
2199
|
-
const questionsDir = (0,
|
|
2200
|
-
(0,
|
|
2201
|
-
const filePath = (0,
|
|
2572
|
+
const questionsDir = (0, import_path5.join)(cwd, ".stackwright", "questions");
|
|
2573
|
+
(0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
|
|
2574
|
+
const filePath = (0, import_path5.join)(questionsDir, `${phase}.json`);
|
|
2202
2575
|
const payload = {
|
|
2203
2576
|
version: "1.0",
|
|
2204
2577
|
phase,
|
|
@@ -2238,14 +2611,14 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
2238
2611
|
};
|
|
2239
2612
|
}
|
|
2240
2613
|
try {
|
|
2241
|
-
const answersPath = (0,
|
|
2614
|
+
const answersPath = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2242
2615
|
let answers = {};
|
|
2243
|
-
if ((0,
|
|
2616
|
+
if ((0, import_fs5.existsSync)(answersPath)) {
|
|
2244
2617
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
2245
2618
|
}
|
|
2246
2619
|
let buildContextText = "";
|
|
2247
|
-
const buildContextPath = (0,
|
|
2248
|
-
if ((0,
|
|
2620
|
+
const buildContextPath = (0, import_path5.join)(cwd, ".stackwright", "build-context.json");
|
|
2621
|
+
if ((0, import_fs5.existsSync)(buildContextPath)) {
|
|
2249
2622
|
try {
|
|
2250
2623
|
const bcRaw = JSON.parse(safeReadSync(buildContextPath));
|
|
2251
2624
|
if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
|
|
@@ -2259,9 +2632,39 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
2259
2632
|
const missingDependencies = [];
|
|
2260
2633
|
for (const dep of deps) {
|
|
2261
2634
|
const artifactFile = PHASE_ARTIFACT[dep];
|
|
2262
|
-
const artifactPath = (0,
|
|
2263
|
-
if ((0,
|
|
2264
|
-
const
|
|
2635
|
+
const artifactPath = (0, import_path5.join)(cwd, ".stackwright", "artifacts", artifactFile);
|
|
2636
|
+
if ((0, import_fs5.existsSync)(artifactPath)) {
|
|
2637
|
+
const rawContent = safeReadSync(artifactPath);
|
|
2638
|
+
const rawBytes = Buffer.from(rawContent, "utf-8");
|
|
2639
|
+
const content = JSON.parse(rawContent);
|
|
2640
|
+
let signatureVerified = false;
|
|
2641
|
+
let signatureAvailable = false;
|
|
2642
|
+
try {
|
|
2643
|
+
const sig = getArtifactSignature(cwd, artifactFile);
|
|
2644
|
+
if (sig) {
|
|
2645
|
+
signatureAvailable = true;
|
|
2646
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
2647
|
+
signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
|
|
2648
|
+
if (!signatureVerified) {
|
|
2649
|
+
const actualDigest = (0, import_crypto3.createHash)("sha384").update(rawBytes).digest("hex");
|
|
2650
|
+
emitSignatureAuditEvent({
|
|
2651
|
+
artifactFilename: artifactFile,
|
|
2652
|
+
expectedDigest: sig.digest,
|
|
2653
|
+
actualDigest,
|
|
2654
|
+
phase: dep,
|
|
2655
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2656
|
+
source: "stackwright_pro_build_specialist_prompt"
|
|
2657
|
+
});
|
|
2658
|
+
missingDependencies.push(dep);
|
|
2659
|
+
artifactSections.push(
|
|
2660
|
+
`[${artifactFile}]:
|
|
2661
|
+
(integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
|
|
2662
|
+
);
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
} catch {
|
|
2667
|
+
}
|
|
2265
2668
|
const expectedOtter = PHASE_TO_OTTER2[dep];
|
|
2266
2669
|
const artifactOtter = content["generatedBy"];
|
|
2267
2670
|
const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
|
|
@@ -2278,8 +2681,11 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
2278
2681
|
(integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
|
|
2279
2682
|
);
|
|
2280
2683
|
} else {
|
|
2281
|
-
|
|
2282
|
-
|
|
2684
|
+
const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
|
|
2685
|
+
artifactSections.push(
|
|
2686
|
+
`[${artifactFile}]${sigStatus}:
|
|
2687
|
+
${JSON.stringify(content, null, 2)}`
|
|
2688
|
+
);
|
|
2283
2689
|
}
|
|
2284
2690
|
} else {
|
|
2285
2691
|
missingDependencies.push(dep);
|
|
@@ -2295,6 +2701,9 @@ ${JSON.stringify(content, null, 2)}`);
|
|
|
2295
2701
|
if (artifactSections.length > 0) {
|
|
2296
2702
|
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
2297
2703
|
}
|
|
2704
|
+
const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
|
|
2705
|
+
parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
|
|
2706
|
+
parts.push(artifactSchema);
|
|
2298
2707
|
parts.push("", "Execute using these answers and the upstream artifacts provided.");
|
|
2299
2708
|
const prompt = parts.join("\n");
|
|
2300
2709
|
const dependenciesSatisfied = missingDependencies.length === 0;
|
|
@@ -2336,6 +2745,187 @@ var PHASE_REQUIRED_KEYS = {
|
|
|
2336
2745
|
dashboard: ["version", "generatedBy"],
|
|
2337
2746
|
workflow: ["version", "generatedBy"]
|
|
2338
2747
|
};
|
|
2748
|
+
var PHASE_ARTIFACT_SCHEMA = {
|
|
2749
|
+
designer: JSON.stringify(
|
|
2750
|
+
{
|
|
2751
|
+
version: "1.0",
|
|
2752
|
+
generatedBy: "stackwright-pro-designer-otter",
|
|
2753
|
+
application: {
|
|
2754
|
+
type: "<operational|data-explorer|admin|logistics|general>",
|
|
2755
|
+
environment: "<workstation|field|control-room|mixed>",
|
|
2756
|
+
density: "<compact|balanced|spacious>",
|
|
2757
|
+
accessibility: "<wcag-aa|section-508|none>",
|
|
2758
|
+
colorScheme: "<light|dark|both>"
|
|
2759
|
+
},
|
|
2760
|
+
designLanguage: {
|
|
2761
|
+
rationale: "<design rationale>",
|
|
2762
|
+
spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
|
|
2763
|
+
colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
|
|
2764
|
+
typography: {
|
|
2765
|
+
dataFont: "Inter",
|
|
2766
|
+
headingFont: "Inter",
|
|
2767
|
+
monoFont: "monospace",
|
|
2768
|
+
dataSizePx: 12,
|
|
2769
|
+
bodySizePx: 14
|
|
2770
|
+
},
|
|
2771
|
+
contrastRatio: "4.5",
|
|
2772
|
+
borderRadius: "4",
|
|
2773
|
+
shadowElevation: "standard"
|
|
2774
|
+
},
|
|
2775
|
+
themeTokenSeeds: {
|
|
2776
|
+
light: {
|
|
2777
|
+
background: "#ffffff",
|
|
2778
|
+
foreground: "#1a1a1a",
|
|
2779
|
+
primary: "#1a365d",
|
|
2780
|
+
surface: "#f7f7f7",
|
|
2781
|
+
border: "#e2e8f0"
|
|
2782
|
+
},
|
|
2783
|
+
dark: {
|
|
2784
|
+
background: "#1a1a1a",
|
|
2785
|
+
foreground: "#ffffff",
|
|
2786
|
+
primary: "#90cdf4",
|
|
2787
|
+
surface: "#2d2d2d",
|
|
2788
|
+
border: "#4a5568"
|
|
2789
|
+
}
|
|
2790
|
+
},
|
|
2791
|
+
conformsTo: null,
|
|
2792
|
+
operationalNotes: []
|
|
2793
|
+
},
|
|
2794
|
+
null,
|
|
2795
|
+
2
|
|
2796
|
+
),
|
|
2797
|
+
theme: JSON.stringify(
|
|
2798
|
+
{
|
|
2799
|
+
version: "1.0",
|
|
2800
|
+
generatedBy: "stackwright-pro-theme-otter",
|
|
2801
|
+
componentLibrary: "shadcn",
|
|
2802
|
+
colorScheme: "<light|dark|both>",
|
|
2803
|
+
tokens: {
|
|
2804
|
+
colors: { "primary-500": "#1a365d", background: "#ffffff" },
|
|
2805
|
+
spacing: { "spacing-1": "8px", "spacing-2": "16px" },
|
|
2806
|
+
typography: { "font-data": "Inter", "text-sm": "12px" },
|
|
2807
|
+
shape: { "radius-sm": "4px", "radius-md": "8px" },
|
|
2808
|
+
shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
|
|
2809
|
+
},
|
|
2810
|
+
cssVariables: {
|
|
2811
|
+
"--background": "0 0% 100%",
|
|
2812
|
+
"--foreground": "222.2 84% 4.9%",
|
|
2813
|
+
"--primary": "222.2 47.4% 11.2%",
|
|
2814
|
+
"--primary-foreground": "210 40% 98%",
|
|
2815
|
+
"--surface": "210 40% 98%",
|
|
2816
|
+
"--border": "214.3 31.8% 91.4%"
|
|
2817
|
+
},
|
|
2818
|
+
dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
|
|
2819
|
+
},
|
|
2820
|
+
null,
|
|
2821
|
+
2
|
|
2822
|
+
),
|
|
2823
|
+
api: JSON.stringify(
|
|
2824
|
+
{
|
|
2825
|
+
version: "1.0",
|
|
2826
|
+
generatedBy: "stackwright-pro-api-otter",
|
|
2827
|
+
entities: [
|
|
2828
|
+
{
|
|
2829
|
+
name: "Shipment",
|
|
2830
|
+
endpoint: "/shipments",
|
|
2831
|
+
method: "GET",
|
|
2832
|
+
revalidate: 60,
|
|
2833
|
+
mutationType: null
|
|
2834
|
+
}
|
|
2835
|
+
],
|
|
2836
|
+
auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
|
|
2837
|
+
baseUrl: "https://api.example.mil/v2",
|
|
2838
|
+
specPath: "./specs/api.yaml"
|
|
2839
|
+
},
|
|
2840
|
+
null,
|
|
2841
|
+
2
|
|
2842
|
+
),
|
|
2843
|
+
data: JSON.stringify(
|
|
2844
|
+
{
|
|
2845
|
+
version: "1.0",
|
|
2846
|
+
generatedBy: "stackwright-pro-data-otter",
|
|
2847
|
+
strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
|
|
2848
|
+
pulseMode: false,
|
|
2849
|
+
collections: [{ name: "equipment", revalidate: 60, pulse: false }],
|
|
2850
|
+
endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
|
|
2851
|
+
requiredPackages: { dependencies: {}, devPackages: {} }
|
|
2852
|
+
},
|
|
2853
|
+
null,
|
|
2854
|
+
2
|
|
2855
|
+
),
|
|
2856
|
+
workflow: JSON.stringify(
|
|
2857
|
+
{
|
|
2858
|
+
version: "1.0",
|
|
2859
|
+
generatedBy: "stackwright-pro-workflow-otter",
|
|
2860
|
+
workflowConfig: {
|
|
2861
|
+
id: "procurement-approval",
|
|
2862
|
+
route: "/procurement",
|
|
2863
|
+
files: ["workflows/procurement-approval.yml"],
|
|
2864
|
+
serviceDependencies: ["service:workflow-state"],
|
|
2865
|
+
warnings: []
|
|
2866
|
+
}
|
|
2867
|
+
},
|
|
2868
|
+
null,
|
|
2869
|
+
2
|
|
2870
|
+
),
|
|
2871
|
+
pages: JSON.stringify(
|
|
2872
|
+
{
|
|
2873
|
+
version: "1.0",
|
|
2874
|
+
generatedBy: "stackwright-pro-page-otter",
|
|
2875
|
+
pages: [
|
|
2876
|
+
{
|
|
2877
|
+
slug: "catalog",
|
|
2878
|
+
type: "collection_listing",
|
|
2879
|
+
collection: "products",
|
|
2880
|
+
themeApplied: true,
|
|
2881
|
+
authRequired: false
|
|
2882
|
+
},
|
|
2883
|
+
{
|
|
2884
|
+
slug: "admin",
|
|
2885
|
+
type: "protected",
|
|
2886
|
+
collection: null,
|
|
2887
|
+
themeApplied: true,
|
|
2888
|
+
authRequired: true
|
|
2889
|
+
}
|
|
2890
|
+
]
|
|
2891
|
+
},
|
|
2892
|
+
null,
|
|
2893
|
+
2
|
|
2894
|
+
),
|
|
2895
|
+
dashboard: JSON.stringify(
|
|
2896
|
+
{
|
|
2897
|
+
version: "1.0",
|
|
2898
|
+
generatedBy: "stackwright-pro-dashboard-otter",
|
|
2899
|
+
pages: [
|
|
2900
|
+
{
|
|
2901
|
+
slug: "dashboard",
|
|
2902
|
+
layout: "<grid|table|mixed>",
|
|
2903
|
+
collections: ["equipment", "supplies"],
|
|
2904
|
+
mode: "<ISR|Pulse>"
|
|
2905
|
+
}
|
|
2906
|
+
]
|
|
2907
|
+
},
|
|
2908
|
+
null,
|
|
2909
|
+
2
|
|
2910
|
+
),
|
|
2911
|
+
auth: JSON.stringify(
|
|
2912
|
+
{
|
|
2913
|
+
version: "1.0",
|
|
2914
|
+
generatedBy: "stackwright-pro-auth-otter",
|
|
2915
|
+
authConfig: {
|
|
2916
|
+
method: "<cac|oidc|oauth2|none>",
|
|
2917
|
+
provider: "<azure-ad|okta|ping|cognito \u2014 if OIDC, else null>",
|
|
2918
|
+
rbacRoles: ["ADMIN", "ANALYST"],
|
|
2919
|
+
rbacDefaultRole: "ANALYST",
|
|
2920
|
+
protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
|
|
2921
|
+
auditEnabled: true,
|
|
2922
|
+
auditRetentionDays: 90
|
|
2923
|
+
}
|
|
2924
|
+
},
|
|
2925
|
+
null,
|
|
2926
|
+
2
|
|
2927
|
+
)
|
|
2928
|
+
};
|
|
2339
2929
|
function handleValidateArtifact(input) {
|
|
2340
2930
|
const cwd = input._cwd ?? process.cwd();
|
|
2341
2931
|
const { phase, responseText, artifact: directArtifact } = input;
|
|
@@ -2429,11 +3019,22 @@ function handleValidateArtifact(input) {
|
|
|
2429
3019
|
}
|
|
2430
3020
|
}
|
|
2431
3021
|
try {
|
|
2432
|
-
const artifactsDir = (0,
|
|
2433
|
-
(0,
|
|
3022
|
+
const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
|
|
3023
|
+
(0, import_fs5.mkdirSync)(artifactsDir, { recursive: true });
|
|
2434
3024
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2435
|
-
const artifactPath = (0,
|
|
2436
|
-
|
|
3025
|
+
const artifactPath = (0, import_path5.join)(artifactsDir, artifactFile);
|
|
3026
|
+
const serialized = JSON.stringify(artifact, null, 2) + "\n";
|
|
3027
|
+
const artifactBytes = Buffer.from(serialized, "utf-8");
|
|
3028
|
+
safeWriteSync(artifactPath, serialized);
|
|
3029
|
+
let signed = false;
|
|
3030
|
+
try {
|
|
3031
|
+
const { privateKey } = loadPipelineKeys(cwd);
|
|
3032
|
+
const sig = signArtifact(artifactBytes, privateKey);
|
|
3033
|
+
const signerOtter = PHASE_TO_OTTER2[phase];
|
|
3034
|
+
saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
|
|
3035
|
+
signed = true;
|
|
3036
|
+
} catch {
|
|
3037
|
+
}
|
|
2437
3038
|
const state = readState(cwd);
|
|
2438
3039
|
if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
|
|
2439
3040
|
const ps = state.phases[phase];
|
|
@@ -2444,7 +3045,7 @@ function handleValidateArtifact(input) {
|
|
|
2444
3045
|
valid: true,
|
|
2445
3046
|
phase,
|
|
2446
3047
|
artifactPath,
|
|
2447
|
-
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
|
|
3048
|
+
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
|
|
2448
3049
|
};
|
|
2449
3050
|
return { text: JSON.stringify(result), isError: false };
|
|
2450
3051
|
} catch (err) {
|
|
@@ -2468,13 +3069,13 @@ function registerPipelineTools(server2) {
|
|
|
2468
3069
|
"stackwright_pro_set_pipeline_state",
|
|
2469
3070
|
`Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
|
|
2470
3071
|
{
|
|
2471
|
-
phase:
|
|
2472
|
-
field:
|
|
2473
|
-
value: boolCoerce(
|
|
3072
|
+
phase: import_zod11.z.string().optional().describe('Phase to update, e.g. "designer"'),
|
|
3073
|
+
field: import_zod11.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
|
|
3074
|
+
value: boolCoerce(import_zod11.z.boolean().optional()).describe(
|
|
2474
3075
|
'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
|
|
2475
3076
|
),
|
|
2476
|
-
status:
|
|
2477
|
-
incrementRetry: boolCoerce(
|
|
3077
|
+
status: import_zod11.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
|
|
3078
|
+
incrementRetry: boolCoerce(import_zod11.z.boolean().optional()).describe(
|
|
2478
3079
|
"Bump retryCount by 1 \u2014 must be a JSON boolean"
|
|
2479
3080
|
)
|
|
2480
3081
|
},
|
|
@@ -2492,7 +3093,7 @@ function registerPipelineTools(server2) {
|
|
|
2492
3093
|
"stackwright_pro_check_execution_ready",
|
|
2493
3094
|
`Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
|
|
2494
3095
|
{
|
|
2495
|
-
phase:
|
|
3096
|
+
phase: import_zod11.z.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
|
|
2496
3097
|
},
|
|
2497
3098
|
async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
|
|
2498
3099
|
);
|
|
@@ -2506,9 +3107,9 @@ function registerPipelineTools(server2) {
|
|
|
2506
3107
|
"stackwright_pro_write_phase_questions",
|
|
2507
3108
|
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
|
|
2508
3109
|
{
|
|
2509
|
-
phase:
|
|
2510
|
-
responseText:
|
|
2511
|
-
questions: jsonCoerce(
|
|
3110
|
+
phase: import_zod11.z.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
|
|
3111
|
+
responseText: import_zod11.z.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
|
|
3112
|
+
questions: jsonCoerce(import_zod11.z.array(import_zod11.z.any()).optional()).describe(
|
|
2512
3113
|
"Questions array for direct specialist write"
|
|
2513
3114
|
)
|
|
2514
3115
|
},
|
|
@@ -2523,10 +3124,10 @@ function registerPipelineTools(server2) {
|
|
|
2523
3124
|
isError: true
|
|
2524
3125
|
};
|
|
2525
3126
|
}
|
|
2526
|
-
const questionsDir = (0,
|
|
2527
|
-
(0,
|
|
2528
|
-
const outPath = (0,
|
|
2529
|
-
(0,
|
|
3127
|
+
const questionsDir = (0, import_path5.join)(process.cwd(), ".stackwright", "questions");
|
|
3128
|
+
(0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
|
|
3129
|
+
const outPath = (0, import_path5.join)(questionsDir, `${phase}.json`);
|
|
3130
|
+
(0, import_fs5.writeFileSync)(
|
|
2530
3131
|
outPath,
|
|
2531
3132
|
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
2532
3133
|
);
|
|
@@ -2551,16 +3152,16 @@ function registerPipelineTools(server2) {
|
|
|
2551
3152
|
server2.tool(
|
|
2552
3153
|
"stackwright_pro_build_specialist_prompt",
|
|
2553
3154
|
`Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
|
|
2554
|
-
{ phase:
|
|
3155
|
+
{ phase: import_zod11.z.string().describe('Phase to build prompt for, e.g. "pages"') },
|
|
2555
3156
|
async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
|
|
2556
3157
|
);
|
|
2557
3158
|
server2.tool(
|
|
2558
3159
|
"stackwright_pro_validate_artifact",
|
|
2559
3160
|
`Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
|
|
2560
3161
|
{
|
|
2561
|
-
phase:
|
|
2562
|
-
responseText:
|
|
2563
|
-
artifact:
|
|
3162
|
+
phase: import_zod11.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
|
|
3163
|
+
responseText: import_zod11.z.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
|
|
3164
|
+
artifact: jsonCoerce(import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown()).optional()).describe(
|
|
2564
3165
|
"Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
|
|
2565
3166
|
)
|
|
2566
3167
|
},
|
|
@@ -2576,9 +3177,9 @@ function registerPipelineTools(server2) {
|
|
|
2576
3177
|
}
|
|
2577
3178
|
|
|
2578
3179
|
// src/tools/safe-write.ts
|
|
2579
|
-
var
|
|
2580
|
-
var
|
|
2581
|
-
var
|
|
3180
|
+
var import_zod12 = require("zod");
|
|
3181
|
+
var import_fs6 = require("fs");
|
|
3182
|
+
var import_path6 = require("path");
|
|
2582
3183
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
2583
3184
|
"stackwright-pro-designer-otter": [
|
|
2584
3185
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
|
|
@@ -2621,15 +3222,31 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
2621
3222
|
};
|
|
2622
3223
|
var PROTECTED_PATH_PREFIXES = [
|
|
2623
3224
|
".stackwright/pipeline-state.json",
|
|
3225
|
+
".stackwright/pipeline-keys.json",
|
|
3226
|
+
// ephemeral signing keys
|
|
3227
|
+
".stackwright/artifacts/signatures.json",
|
|
3228
|
+
// artifact signature manifest
|
|
2624
3229
|
".stackwright/questions/",
|
|
2625
3230
|
".stackwright/answers/"
|
|
2626
3231
|
];
|
|
3232
|
+
var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
|
|
3233
|
+
var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
|
|
3234
|
+
var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
|
|
3235
|
+
var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
|
|
3236
|
+
function getMaxBytesForPath(filePath) {
|
|
3237
|
+
if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
|
|
3238
|
+
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
|
|
3239
|
+
return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
|
|
3240
|
+
if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
|
|
3241
|
+
return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
|
|
3242
|
+
return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
|
|
3243
|
+
}
|
|
2627
3244
|
function checkPathAllowed(callerOtter, filePath) {
|
|
2628
|
-
const normalized = (0,
|
|
3245
|
+
const normalized = (0, import_path6.normalize)(filePath);
|
|
2629
3246
|
if (normalized.includes("..")) {
|
|
2630
3247
|
return { allowed: false, error: 'Path traversal detected: ".." segments are not allowed' };
|
|
2631
3248
|
}
|
|
2632
|
-
if ((0,
|
|
3249
|
+
if ((0, import_path6.isAbsolute)(normalized)) {
|
|
2633
3250
|
return {
|
|
2634
3251
|
allowed: false,
|
|
2635
3252
|
error: "Absolute paths are not allowed \u2014 use paths relative to project root"
|
|
@@ -2749,11 +3366,23 @@ function handleSafeWrite(input) {
|
|
|
2749
3366
|
};
|
|
2750
3367
|
return { text: JSON.stringify(result), isError: true };
|
|
2751
3368
|
}
|
|
2752
|
-
const
|
|
2753
|
-
const
|
|
2754
|
-
if (
|
|
3369
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
3370
|
+
const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
|
|
3371
|
+
if (contentBytes > maxBytes) {
|
|
3372
|
+
const result = {
|
|
3373
|
+
success: false,
|
|
3374
|
+
error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
|
|
3375
|
+
callerOtter,
|
|
3376
|
+
attemptedPath: filePath,
|
|
3377
|
+
allowedPaths: []
|
|
3378
|
+
};
|
|
3379
|
+
return { text: JSON.stringify(result), isError: true };
|
|
3380
|
+
}
|
|
3381
|
+
const normalized = (0, import_path6.normalize)(filePath);
|
|
3382
|
+
const fullPath = (0, import_path6.join)(cwd, normalized);
|
|
3383
|
+
if ((0, import_fs6.existsSync)(fullPath)) {
|
|
2755
3384
|
try {
|
|
2756
|
-
const stat = (0,
|
|
3385
|
+
const stat = (0, import_fs6.lstatSync)(fullPath);
|
|
2757
3386
|
if (stat.isSymbolicLink()) {
|
|
2758
3387
|
const result = {
|
|
2759
3388
|
success: false,
|
|
@@ -2780,9 +3409,9 @@ function handleSafeWrite(input) {
|
|
|
2780
3409
|
}
|
|
2781
3410
|
try {
|
|
2782
3411
|
if (createDirectories) {
|
|
2783
|
-
(0,
|
|
3412
|
+
(0, import_fs6.mkdirSync)((0, import_path6.dirname)(fullPath), { recursive: true });
|
|
2784
3413
|
}
|
|
2785
|
-
(0,
|
|
3414
|
+
(0, import_fs6.writeFileSync)(fullPath, content, { encoding: "utf-8" });
|
|
2786
3415
|
const result = {
|
|
2787
3416
|
success: true,
|
|
2788
3417
|
path: normalized,
|
|
@@ -2808,10 +3437,10 @@ function registerSafeWriteTools(server2) {
|
|
|
2808
3437
|
"stackwright_pro_safe_write",
|
|
2809
3438
|
DESC,
|
|
2810
3439
|
{
|
|
2811
|
-
callerOtter:
|
|
2812
|
-
filePath:
|
|
2813
|
-
content:
|
|
2814
|
-
createDirectories: boolCoerce(
|
|
3440
|
+
callerOtter: import_zod12.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
|
|
3441
|
+
filePath: import_zod12.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
|
|
3442
|
+
content: import_zod12.z.string().describe("File content to write"),
|
|
3443
|
+
createDirectories: boolCoerce(import_zod12.z.boolean().optional().default(true)).describe(
|
|
2815
3444
|
"Create parent directories if they don't exist. Default: true"
|
|
2816
3445
|
)
|
|
2817
3446
|
},
|
|
@@ -2828,9 +3457,9 @@ function registerSafeWriteTools(server2) {
|
|
|
2828
3457
|
}
|
|
2829
3458
|
|
|
2830
3459
|
// src/tools/auth.ts
|
|
2831
|
-
var
|
|
2832
|
-
var
|
|
2833
|
-
var
|
|
3460
|
+
var import_zod13 = require("zod");
|
|
3461
|
+
var import_fs7 = require("fs");
|
|
3462
|
+
var import_path7 = require("path");
|
|
2834
3463
|
function buildHierarchy(roles) {
|
|
2835
3464
|
const h = {};
|
|
2836
3465
|
for (let i = 0; i < roles.length - 1; i++) {
|
|
@@ -3092,7 +3721,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
3092
3721
|
auditRetentionDays,
|
|
3093
3722
|
protectedRoutes
|
|
3094
3723
|
);
|
|
3095
|
-
(0,
|
|
3724
|
+
(0, import_fs7.writeFileSync)((0, import_path7.join)(cwd, "middleware.ts"), middlewareContent, "utf8");
|
|
3096
3725
|
filesWritten.push("middleware.ts");
|
|
3097
3726
|
} catch (err) {
|
|
3098
3727
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3108,12 +3737,12 @@ async function configureAuthHandler(params, cwd) {
|
|
|
3108
3737
|
}
|
|
3109
3738
|
try {
|
|
3110
3739
|
const envBlock = generateEnvBlock(method, params);
|
|
3111
|
-
const envPath = (0,
|
|
3112
|
-
if ((0,
|
|
3113
|
-
const existing = (0,
|
|
3114
|
-
(0,
|
|
3740
|
+
const envPath = (0, import_path7.join)(cwd, ".env.example");
|
|
3741
|
+
if ((0, import_fs7.existsSync)(envPath)) {
|
|
3742
|
+
const existing = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
3743
|
+
(0, import_fs7.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
3115
3744
|
} else {
|
|
3116
|
-
(0,
|
|
3745
|
+
(0, import_fs7.writeFileSync)(envPath, envBlock, "utf8");
|
|
3117
3746
|
}
|
|
3118
3747
|
filesWritten.push(".env.example");
|
|
3119
3748
|
} catch (err) {
|
|
@@ -3139,12 +3768,12 @@ async function configureAuthHandler(params, cwd) {
|
|
|
3139
3768
|
auditRetentionDays,
|
|
3140
3769
|
protectedRoutes
|
|
3141
3770
|
);
|
|
3142
|
-
const ymlPath = (0,
|
|
3143
|
-
if (!(0,
|
|
3144
|
-
(0,
|
|
3771
|
+
const ymlPath = (0, import_path7.join)(cwd, "stackwright.yml");
|
|
3772
|
+
if (!(0, import_fs7.existsSync)(ymlPath)) {
|
|
3773
|
+
(0, import_fs7.writeFileSync)(ymlPath, authYaml, "utf8");
|
|
3145
3774
|
} else {
|
|
3146
|
-
const existing = (0,
|
|
3147
|
-
(0,
|
|
3775
|
+
const existing = (0, import_fs7.readFileSync)(ymlPath, "utf8");
|
|
3776
|
+
(0, import_fs7.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
|
|
3148
3777
|
}
|
|
3149
3778
|
filesWritten.push("stackwright.yml");
|
|
3150
3779
|
} catch (err) {
|
|
@@ -3183,35 +3812,35 @@ function registerAuthTools(server2) {
|
|
|
3183
3812
|
"stackwright_pro_configure_auth",
|
|
3184
3813
|
"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.",
|
|
3185
3814
|
{
|
|
3186
|
-
method:
|
|
3187
|
-
provider:
|
|
3815
|
+
method: import_zod13.z.enum(["cac", "oidc", "oauth2", "none"]),
|
|
3816
|
+
provider: import_zod13.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
|
|
3188
3817
|
// CAC
|
|
3189
|
-
cacCaBundle:
|
|
3190
|
-
cacEdipiLookup:
|
|
3191
|
-
cacOcspEndpoint:
|
|
3192
|
-
cacCertHeader:
|
|
3818
|
+
cacCaBundle: import_zod13.z.string().optional(),
|
|
3819
|
+
cacEdipiLookup: import_zod13.z.string().optional(),
|
|
3820
|
+
cacOcspEndpoint: import_zod13.z.string().optional(),
|
|
3821
|
+
cacCertHeader: import_zod13.z.string().optional(),
|
|
3193
3822
|
// OIDC
|
|
3194
|
-
oidcDiscoveryUrl:
|
|
3195
|
-
oidcClientId:
|
|
3196
|
-
oidcClientSecret:
|
|
3197
|
-
oidcScopes:
|
|
3198
|
-
oidcRoleClaim:
|
|
3823
|
+
oidcDiscoveryUrl: import_zod13.z.string().optional(),
|
|
3824
|
+
oidcClientId: import_zod13.z.string().optional(),
|
|
3825
|
+
oidcClientSecret: import_zod13.z.string().optional(),
|
|
3826
|
+
oidcScopes: import_zod13.z.string().optional(),
|
|
3827
|
+
oidcRoleClaim: import_zod13.z.string().optional(),
|
|
3199
3828
|
// OAuth2
|
|
3200
|
-
oauth2AuthUrl:
|
|
3201
|
-
oauth2TokenUrl:
|
|
3202
|
-
oauth2ClientId:
|
|
3203
|
-
oauth2ClientSecret:
|
|
3204
|
-
oauth2Scopes:
|
|
3829
|
+
oauth2AuthUrl: import_zod13.z.string().optional(),
|
|
3830
|
+
oauth2TokenUrl: import_zod13.z.string().optional(),
|
|
3831
|
+
oauth2ClientId: import_zod13.z.string().optional(),
|
|
3832
|
+
oauth2ClientSecret: import_zod13.z.string().optional(),
|
|
3833
|
+
oauth2Scopes: import_zod13.z.string().optional(),
|
|
3205
3834
|
// RBAC
|
|
3206
|
-
rbacRoles: jsonCoerce(
|
|
3207
|
-
rbacDefaultRole:
|
|
3835
|
+
rbacRoles: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
|
|
3836
|
+
rbacDefaultRole: import_zod13.z.string().optional(),
|
|
3208
3837
|
// Audit
|
|
3209
|
-
auditEnabled: boolCoerce(
|
|
3210
|
-
auditRetentionDays: numCoerce(
|
|
3838
|
+
auditEnabled: boolCoerce(import_zod13.z.boolean().optional()),
|
|
3839
|
+
auditRetentionDays: numCoerce(import_zod13.z.number().int().positive().optional()),
|
|
3211
3840
|
// Routes
|
|
3212
|
-
protectedRoutes: jsonCoerce(
|
|
3841
|
+
protectedRoutes: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
|
|
3213
3842
|
// Injection for tests
|
|
3214
|
-
_cwd:
|
|
3843
|
+
_cwd: import_zod13.z.string().optional()
|
|
3215
3844
|
},
|
|
3216
3845
|
async (params) => {
|
|
3217
3846
|
const cwd = params._cwd ?? process.cwd();
|
|
@@ -3221,13 +3850,13 @@ function registerAuthTools(server2) {
|
|
|
3221
3850
|
}
|
|
3222
3851
|
|
|
3223
3852
|
// src/integrity.ts
|
|
3224
|
-
var
|
|
3225
|
-
var
|
|
3226
|
-
var
|
|
3853
|
+
var import_crypto4 = require("crypto");
|
|
3854
|
+
var import_fs8 = require("fs");
|
|
3855
|
+
var import_path8 = require("path");
|
|
3227
3856
|
var _checksums = /* @__PURE__ */ new Map([
|
|
3228
3857
|
[
|
|
3229
3858
|
"stackwright-pro-api-otter.json",
|
|
3230
|
-
"
|
|
3859
|
+
"ed667124af3f025e090c0e65d0a86f0ef08fea06c0029b8fd0edf6df33df9f9c"
|
|
3231
3860
|
],
|
|
3232
3861
|
[
|
|
3233
3862
|
"stackwright-pro-auth-otter.json",
|
|
@@ -3235,7 +3864,7 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
3235
3864
|
],
|
|
3236
3865
|
[
|
|
3237
3866
|
"stackwright-pro-dashboard-otter.json",
|
|
3238
|
-
"
|
|
3867
|
+
"5e930b4092b9002e3c1a413b36418e49c865199af12a546890ccf7f9e56a5593"
|
|
3239
3868
|
],
|
|
3240
3869
|
[
|
|
3241
3870
|
"stackwright-pro-data-otter.json",
|
|
@@ -3243,11 +3872,11 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
3243
3872
|
],
|
|
3244
3873
|
[
|
|
3245
3874
|
"stackwright-pro-designer-otter.json",
|
|
3246
|
-
"
|
|
3875
|
+
"e80d4e7bab87d8647debadb238a58aac498ec5074ff25b21abd3b13ff778bf71"
|
|
3247
3876
|
],
|
|
3248
3877
|
[
|
|
3249
3878
|
"stackwright-pro-foreman-otter.json",
|
|
3250
|
-
"
|
|
3879
|
+
"02dd2485562361f2f3cfd998981349020d7599dcd2d969bb022f9f6d537f3517"
|
|
3251
3880
|
],
|
|
3252
3881
|
[
|
|
3253
3882
|
"stackwright-pro-page-otter.json",
|
|
@@ -3255,11 +3884,11 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
3255
3884
|
],
|
|
3256
3885
|
[
|
|
3257
3886
|
"stackwright-pro-theme-otter.json",
|
|
3258
|
-
"
|
|
3887
|
+
"d3a15871b71a466c12c4711fe37cbb018c768cb99eff15c40dbbc7061d4e966b"
|
|
3259
3888
|
],
|
|
3260
3889
|
[
|
|
3261
3890
|
"stackwright-pro-workflow-otter.json",
|
|
3262
|
-
"
|
|
3891
|
+
"2ce1bcbb5c45dbb214499ea08c7175f8b743b51b8cb2ad539faf7df11edcf88a"
|
|
3263
3892
|
]
|
|
3264
3893
|
]);
|
|
3265
3894
|
Object.freeze(_checksums);
|
|
@@ -3274,21 +3903,21 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
|
|
|
3274
3903
|
}
|
|
3275
3904
|
var MAX_OTTER_BYTES = 1 * 1024 * 1024;
|
|
3276
3905
|
function computeSha256(data) {
|
|
3277
|
-
return (0,
|
|
3906
|
+
return (0, import_crypto4.createHash)("sha256").update(data).digest("hex");
|
|
3278
3907
|
}
|
|
3279
3908
|
function safeEqual(a, b) {
|
|
3280
3909
|
if (a.length !== b.length) return false;
|
|
3281
|
-
return (0,
|
|
3910
|
+
return (0, import_crypto4.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
3282
3911
|
}
|
|
3283
3912
|
function verifyOtterFile(filePath) {
|
|
3284
|
-
const filename = (0,
|
|
3913
|
+
const filename = (0, import_path8.basename)(filePath);
|
|
3285
3914
|
const expected = CANONICAL_CHECKSUMS.get(filename);
|
|
3286
3915
|
if (expected === void 0) {
|
|
3287
3916
|
return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
|
|
3288
3917
|
}
|
|
3289
3918
|
let stat;
|
|
3290
3919
|
try {
|
|
3291
|
-
stat = (0,
|
|
3920
|
+
stat = (0, import_fs8.lstatSync)(filePath);
|
|
3292
3921
|
} catch (err) {
|
|
3293
3922
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3294
3923
|
return { verified: false, filename, error: `Cannot stat file: ${msg}` };
|
|
@@ -3306,7 +3935,7 @@ function verifyOtterFile(filePath) {
|
|
|
3306
3935
|
}
|
|
3307
3936
|
let raw;
|
|
3308
3937
|
try {
|
|
3309
|
-
raw = (0,
|
|
3938
|
+
raw = (0, import_fs8.readFileSync)(filePath);
|
|
3310
3939
|
} catch (err) {
|
|
3311
3940
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3312
3941
|
return { verified: false, filename, error: `Cannot read file: ${msg}` };
|
|
@@ -3339,12 +3968,24 @@ function verifyOtterFile(filePath) {
|
|
|
3339
3968
|
return { verified: true, filename };
|
|
3340
3969
|
}
|
|
3341
3970
|
function verifyAllOtters(otterDir) {
|
|
3971
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
|
|
3972
|
+
return {
|
|
3973
|
+
verified: [],
|
|
3974
|
+
failed: [
|
|
3975
|
+
{
|
|
3976
|
+
filename: "<directory>",
|
|
3977
|
+
error: `Security: path traversal sequence detected in otter directory parameter`
|
|
3978
|
+
}
|
|
3979
|
+
],
|
|
3980
|
+
unknown: []
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3342
3983
|
const verified = [];
|
|
3343
3984
|
const failed = [];
|
|
3344
3985
|
const unknown = [];
|
|
3345
3986
|
let entries;
|
|
3346
3987
|
try {
|
|
3347
|
-
entries = (0,
|
|
3988
|
+
entries = (0, import_fs8.readdirSync)(otterDir);
|
|
3348
3989
|
} catch (err) {
|
|
3349
3990
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3350
3991
|
return {
|
|
@@ -3355,9 +3996,9 @@ function verifyAllOtters(otterDir) {
|
|
|
3355
3996
|
}
|
|
3356
3997
|
const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
|
|
3357
3998
|
for (const filename of otterFiles) {
|
|
3358
|
-
const filePath = (0,
|
|
3999
|
+
const filePath = (0, import_path8.join)(otterDir, filename);
|
|
3359
4000
|
try {
|
|
3360
|
-
if ((0,
|
|
4001
|
+
if ((0, import_fs8.lstatSync)(filePath).isSymbolicLink()) {
|
|
3361
4002
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
3362
4003
|
continue;
|
|
3363
4004
|
}
|
|
@@ -3383,15 +4024,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
|
|
|
3383
4024
|
function resolveOtterDir() {
|
|
3384
4025
|
const cwd = process.cwd();
|
|
3385
4026
|
for (const relative of DEFAULT_SEARCH_PATHS) {
|
|
3386
|
-
const candidate = (0,
|
|
4027
|
+
const candidate = (0, import_path8.join)(cwd, relative);
|
|
3387
4028
|
try {
|
|
3388
|
-
(0,
|
|
4029
|
+
(0, import_fs8.lstatSync)(candidate);
|
|
3389
4030
|
return candidate;
|
|
3390
4031
|
} catch {
|
|
3391
4032
|
}
|
|
3392
4033
|
}
|
|
3393
4034
|
return null;
|
|
3394
4035
|
}
|
|
4036
|
+
function emitIntegrityAuditEvent(params) {
|
|
4037
|
+
const record = JSON.stringify({
|
|
4038
|
+
level: "AUDIT",
|
|
4039
|
+
event: "INTEGRITY_FAIL",
|
|
4040
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4041
|
+
source: "stackwright_pro_verify_otter_integrity",
|
|
4042
|
+
otterDir: params.otterDir,
|
|
4043
|
+
failedCount: params.failed.length,
|
|
4044
|
+
unknownCount: params.unknown.length,
|
|
4045
|
+
failures: params.failed,
|
|
4046
|
+
unknown: params.unknown
|
|
4047
|
+
});
|
|
4048
|
+
process.stderr.write(`INTEGRITY_FAIL ${record}
|
|
4049
|
+
`);
|
|
4050
|
+
}
|
|
3395
4051
|
function registerIntegrityTools(server2) {
|
|
3396
4052
|
server2.tool(
|
|
3397
4053
|
"stackwright_pro_verify_otter_integrity",
|
|
@@ -3415,6 +4071,13 @@ function registerIntegrityTools(server2) {
|
|
|
3415
4071
|
}
|
|
3416
4072
|
const result = verifyAllOtters(resolved);
|
|
3417
4073
|
const allGood = result.failed.length === 0 && result.unknown.length === 0;
|
|
4074
|
+
if (!allGood) {
|
|
4075
|
+
emitIntegrityAuditEvent({
|
|
4076
|
+
otterDir: resolved,
|
|
4077
|
+
failed: result.failed,
|
|
4078
|
+
unknown: result.unknown
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
3418
4081
|
return {
|
|
3419
4082
|
content: [
|
|
3420
4083
|
{
|
|
@@ -3428,25 +4091,27 @@ function registerIntegrityTools(server2) {
|
|
|
3428
4091
|
verified: result.verified,
|
|
3429
4092
|
failed: result.failed,
|
|
3430
4093
|
unknown: result.unknown,
|
|
3431
|
-
|
|
4094
|
+
...allGood ? {} : {
|
|
4095
|
+
error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
|
|
4096
|
+
}
|
|
3432
4097
|
})
|
|
3433
4098
|
}
|
|
3434
4099
|
],
|
|
3435
|
-
isError:
|
|
4100
|
+
isError: !allGood
|
|
3436
4101
|
};
|
|
3437
4102
|
}
|
|
3438
4103
|
);
|
|
3439
4104
|
}
|
|
3440
4105
|
|
|
3441
4106
|
// src/tools/domain.ts
|
|
3442
|
-
var
|
|
3443
|
-
var
|
|
3444
|
-
var
|
|
4107
|
+
var import_zod14 = require("zod");
|
|
4108
|
+
var import_fs9 = require("fs");
|
|
4109
|
+
var import_path9 = require("path");
|
|
3445
4110
|
function handleListCollections(input) {
|
|
3446
4111
|
const cwd = input._cwd ?? process.cwd();
|
|
3447
4112
|
const sources = [
|
|
3448
4113
|
{
|
|
3449
|
-
path: (0,
|
|
4114
|
+
path: (0, import_path9.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
|
|
3450
4115
|
source: "data-config.json",
|
|
3451
4116
|
parse: (raw) => {
|
|
3452
4117
|
const parsed = JSON.parse(raw);
|
|
@@ -3457,15 +4122,15 @@ function handleListCollections(input) {
|
|
|
3457
4122
|
}
|
|
3458
4123
|
},
|
|
3459
4124
|
{
|
|
3460
|
-
path: (0,
|
|
4125
|
+
path: (0, import_path9.join)(cwd, "stackwright.yml"),
|
|
3461
4126
|
source: "stackwright.yml",
|
|
3462
4127
|
parse: extractCollectionsFromYaml
|
|
3463
4128
|
}
|
|
3464
4129
|
];
|
|
3465
4130
|
for (const { path: path3, source, parse } of sources) {
|
|
3466
|
-
if (!(0,
|
|
4131
|
+
if (!(0, import_fs9.existsSync)(path3)) continue;
|
|
3467
4132
|
try {
|
|
3468
|
-
const collections = parse((0,
|
|
4133
|
+
const collections = parse((0, import_fs9.readFileSync)(path3, "utf8"));
|
|
3469
4134
|
return {
|
|
3470
4135
|
text: JSON.stringify({ collections, source, collectionCount: collections.length }),
|
|
3471
4136
|
isError: false
|
|
@@ -3616,8 +4281,8 @@ function handleValidateWorkflow(input) {
|
|
|
3616
4281
|
if (input.workflow && Object.keys(input.workflow).length > 0) {
|
|
3617
4282
|
raw = input.workflow;
|
|
3618
4283
|
} else {
|
|
3619
|
-
const artifactPath = (0,
|
|
3620
|
-
if (!(0,
|
|
4284
|
+
const artifactPath = (0, import_path9.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
4285
|
+
if (!(0, import_fs9.existsSync)(artifactPath)) {
|
|
3621
4286
|
return fail([
|
|
3622
4287
|
{
|
|
3623
4288
|
code: "NO_WORKFLOW",
|
|
@@ -3626,7 +4291,7 @@ function handleValidateWorkflow(input) {
|
|
|
3626
4291
|
]);
|
|
3627
4292
|
}
|
|
3628
4293
|
try {
|
|
3629
|
-
raw = JSON.parse((0,
|
|
4294
|
+
raw = JSON.parse((0, import_fs9.readFileSync)(artifactPath, "utf8"));
|
|
3630
4295
|
} catch (err) {
|
|
3631
4296
|
return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
|
|
3632
4297
|
}
|
|
@@ -3825,7 +4490,7 @@ function registerDomainTools(server2) {
|
|
|
3825
4490
|
"stackwright_pro_resolve_data_strategy",
|
|
3826
4491
|
"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.",
|
|
3827
4492
|
{
|
|
3828
|
-
strategy:
|
|
4493
|
+
strategy: import_zod14.z.string().describe(
|
|
3829
4494
|
'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
|
|
3830
4495
|
)
|
|
3831
4496
|
},
|
|
@@ -3835,7 +4500,7 @@ function registerDomainTools(server2) {
|
|
|
3835
4500
|
"stackwright_pro_validate_workflow",
|
|
3836
4501
|
"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.",
|
|
3837
4502
|
{
|
|
3838
|
-
workflow: jsonCoerce(
|
|
4503
|
+
workflow: jsonCoerce(import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).optional()).describe(
|
|
3839
4504
|
"Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
|
|
3840
4505
|
)
|
|
3841
4506
|
},
|
|
@@ -3844,7 +4509,7 @@ function registerDomainTools(server2) {
|
|
|
3844
4509
|
}
|
|
3845
4510
|
|
|
3846
4511
|
// src/tools/type-schemas.ts
|
|
3847
|
-
var
|
|
4512
|
+
var import_zod15 = require("zod");
|
|
3848
4513
|
function buildTypeSchemaSummary() {
|
|
3849
4514
|
return {
|
|
3850
4515
|
version: "1.0",
|
|
@@ -3921,7 +4586,7 @@ function registerTypeSchemasTool(server2) {
|
|
|
3921
4586
|
"stackwright_pro_get_type_schemas",
|
|
3922
4587
|
"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.",
|
|
3923
4588
|
{
|
|
3924
|
-
format:
|
|
4589
|
+
format: import_zod15.z.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
|
|
3925
4590
|
},
|
|
3926
4591
|
async ({ format }) => {
|
|
3927
4592
|
const summary = buildTypeSchemaSummary();
|
|
@@ -3939,13 +4604,13 @@ var package_default = {
|
|
|
3939
4604
|
"@stackwright-pro/types": "workspace:*",
|
|
3940
4605
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
3941
4606
|
"@stackwright-pro/cli-data-explorer": "workspace:*",
|
|
3942
|
-
zod: "^4.3
|
|
4607
|
+
zod: "^4.4.3"
|
|
3943
4608
|
},
|
|
3944
4609
|
devDependencies: {
|
|
3945
|
-
"@types/node": "
|
|
3946
|
-
tsup: "
|
|
3947
|
-
typescript: "
|
|
3948
|
-
vitest: "
|
|
4610
|
+
"@types/node": "catalog:",
|
|
4611
|
+
tsup: "catalog:",
|
|
4612
|
+
typescript: "catalog:",
|
|
4613
|
+
vitest: "catalog:"
|
|
3949
4614
|
},
|
|
3950
4615
|
scripts: {
|
|
3951
4616
|
prepublishOnly: "node scripts/verify-integrity-sync.js",
|
|
@@ -3956,9 +4621,9 @@ var package_default = {
|
|
|
3956
4621
|
"test:coverage": "vitest run --coverage"
|
|
3957
4622
|
},
|
|
3958
4623
|
name: "@stackwright-pro/mcp",
|
|
3959
|
-
version: "0.2.0-alpha.
|
|
4624
|
+
version: "0.2.0-alpha.48",
|
|
3960
4625
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3961
|
-
license: "
|
|
4626
|
+
license: "SEE LICENSE IN LICENSE",
|
|
3962
4627
|
main: "./dist/server.js",
|
|
3963
4628
|
bin: {
|
|
3964
4629
|
"stackwright-pro-mcp": "./dist/server.js"
|
|
@@ -4007,6 +4672,7 @@ registerPipelineTools(server);
|
|
|
4007
4672
|
registerSafeWriteTools(server);
|
|
4008
4673
|
registerAuthTools(server);
|
|
4009
4674
|
registerIntegrityTools(server);
|
|
4675
|
+
registerArtifactSigningTools(server);
|
|
4010
4676
|
registerDomainTools(server);
|
|
4011
4677
|
registerTypeSchemasTool(server);
|
|
4012
4678
|
async function main() {
|