@muggleai/works 5.12.0-staging.73 → 5.12.0-staging.75
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/{chunk-FCDHJD6I.js → chunk-DJ6W5PZA.js} +47 -6
- package/dist/{chunk-O7LFABOC.js → chunk-VTSS6TGQ.js} +121 -23
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/hooks/README.md +2 -0
- package/dist/plugin/scripts/guardrails.mjs +36 -2
- package/dist/release-manifest.json +3 -3
- package/dist/{src-TFNPZD5X.js → src-FRYBTZ6J.js} +1 -1
- package/package.json +6 -2
- package/plugin/hooks/README.md +2 -0
- package/plugin/scripts/guardrails.mjs +36 -2
- package/scripts/postinstall.mjs +35 -4
- package/scripts/release-integrity/compareVersions.mjs +24 -0
- package/scripts/release-integrity/constants.mjs +8 -0
- package/scripts/release-integrity/index.d.mts +21 -0
- package/scripts/release-integrity/index.mjs +4 -0
- package/scripts/release-integrity/resolveIntegrityPolicy.mjs +37 -0
- package/scripts/release-integrity/verifyReleaseSignature.mjs +51 -0
|
@@ -69,12 +69,14 @@ __export(src_exports, {
|
|
|
69
69
|
getElectronAppChecksums: () => getElectronAppChecksums,
|
|
70
70
|
getElectronAppDir: () => getElectronAppDir,
|
|
71
71
|
getElectronAppReleaseTagPrefix: () => getElectronAppReleaseTagPrefix,
|
|
72
|
+
getElectronAppSignedFromVersion: () => getElectronAppSignedFromVersion,
|
|
72
73
|
getElectronAppVersion: () => getElectronAppVersion,
|
|
73
74
|
getElectronAppVersionSource: () => getElectronAppVersionSource,
|
|
74
75
|
getLocalQaTools: () => getLocalQaTools,
|
|
75
76
|
getLogger: () => getLogger,
|
|
76
77
|
getPlatformKey: () => getPlatformKey,
|
|
77
78
|
getQaTools: () => getQaTools,
|
|
79
|
+
getReleaseSignerIdentityUri: () => getReleaseSignerIdentityUri,
|
|
78
80
|
getValidApiKeyData: () => getValidApiKeyData,
|
|
79
81
|
getValidCredentials: () => getValidCredentials,
|
|
80
82
|
hasApiKey: () => hasApiKey,
|
|
@@ -271,6 +273,7 @@ function readReleaseManifest() {
|
|
|
271
273
|
// packages/mcps/src/shared/config.ts
|
|
272
274
|
var ELECTRON_APP_DIR = "electron-app";
|
|
273
275
|
var API_KEY_FILE = "api-key.json";
|
|
276
|
+
var OAUTH_SESSION_FILE = "oauth-session.json";
|
|
274
277
|
var configInstance = null;
|
|
275
278
|
var muggleConfigCache = null;
|
|
276
279
|
function getPackageRoot2() {
|
|
@@ -359,6 +362,12 @@ function buildElectronAppDirName(params) {
|
|
|
359
362
|
}
|
|
360
363
|
return `${params.version}-${params.releaseStream}`;
|
|
361
364
|
}
|
|
365
|
+
function buildOAuthSessionFileName(params) {
|
|
366
|
+
if (params.runtimeTarget === "production" /* Production */) {
|
|
367
|
+
return OAUTH_SESSION_FILE;
|
|
368
|
+
}
|
|
369
|
+
return `oauth-session-${params.runtimeTarget}.json`;
|
|
370
|
+
}
|
|
362
371
|
function getDownloadedElectronAppPath() {
|
|
363
372
|
const platformName = os4.platform();
|
|
364
373
|
const version = getElectronAppVersion();
|
|
@@ -479,7 +488,10 @@ function buildLocalQaConfig() {
|
|
|
479
488
|
projectsDir: path11.join(dataDir, "projects"),
|
|
480
489
|
tempDir: path11.join(dataDir, "temp"),
|
|
481
490
|
apiKeyFilePath: path11.join(dataDir, API_KEY_FILE),
|
|
482
|
-
oauthSessionFilePath: path11.join(
|
|
491
|
+
oauthSessionFilePath: path11.join(
|
|
492
|
+
dataDir,
|
|
493
|
+
buildOAuthSessionFileName({ runtimeTarget: getActiveRuntimeTarget() })
|
|
494
|
+
),
|
|
483
495
|
webServicePath: resolveWebServicePath(),
|
|
484
496
|
webServicePidFile: path11.join(dataDir, "web-service.pid"),
|
|
485
497
|
auth0: {
|
|
@@ -569,6 +581,12 @@ function buildElectronAppChecksumsUrl(version) {
|
|
|
569
581
|
function getElectronAppChecksums() {
|
|
570
582
|
return getMuggleConfig().checksumsByStream?.[getActiveElectronAppReleaseStream()];
|
|
571
583
|
}
|
|
584
|
+
function getElectronAppSignedFromVersion() {
|
|
585
|
+
return getMuggleConfig().electronAppSignedFromVersion ?? "";
|
|
586
|
+
}
|
|
587
|
+
function getReleaseSignerIdentityUri() {
|
|
588
|
+
return getMuggleConfig().signerIdentityUri ?? "";
|
|
589
|
+
}
|
|
572
590
|
function isElectronAppInstalled() {
|
|
573
591
|
return getDownloadedElectronAppPath() !== null;
|
|
574
592
|
}
|
|
@@ -933,6 +951,14 @@ var TestResultStatus = /* @__PURE__ */ ((TestResultStatus2) => {
|
|
|
933
951
|
return TestResultStatus2;
|
|
934
952
|
})(TestResultStatus || {});
|
|
935
953
|
|
|
954
|
+
// packages/mcps/src/mcp/local/services/stored-auth-target.ts
|
|
955
|
+
function isStoredAuthForRuntimeTarget(params) {
|
|
956
|
+
if (!params.storedRuntimeTarget) {
|
|
957
|
+
return params.activeRuntimeTarget === "production" /* Production */;
|
|
958
|
+
}
|
|
959
|
+
return params.storedRuntimeTarget === params.activeRuntimeTarget;
|
|
960
|
+
}
|
|
961
|
+
|
|
936
962
|
// packages/mcps/src/mcp/local/services/auth-service.ts
|
|
937
963
|
var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
|
|
938
964
|
var AuthService = class {
|
|
@@ -1267,7 +1293,8 @@ var AuthService = class {
|
|
|
1267
1293
|
refreshToken: tokenResponse.refreshToken,
|
|
1268
1294
|
expiresAt,
|
|
1269
1295
|
email,
|
|
1270
|
-
userId
|
|
1296
|
+
userId,
|
|
1297
|
+
runtimeTarget: getActiveRuntimeTarget()
|
|
1271
1298
|
};
|
|
1272
1299
|
const dir = path11.dirname(this.oauthSessionFilePath);
|
|
1273
1300
|
if (!fs6.existsSync(dir)) {
|
|
@@ -1280,7 +1307,8 @@ var AuthService = class {
|
|
|
1280
1307
|
logger6.info("Auth stored successfully", { email, expiresAt });
|
|
1281
1308
|
}
|
|
1282
1309
|
/**
|
|
1283
|
-
* Load stored authentication.
|
|
1310
|
+
* Load stored authentication for the active runtime target.
|
|
1311
|
+
* @returns Stored auth, or null when it is absent, unreadable, or was issued for another target.
|
|
1284
1312
|
*/
|
|
1285
1313
|
loadStoredAuth() {
|
|
1286
1314
|
const logger6 = getLogger();
|
|
@@ -1289,7 +1317,19 @@ var AuthService = class {
|
|
|
1289
1317
|
}
|
|
1290
1318
|
try {
|
|
1291
1319
|
const content = fs6.readFileSync(this.oauthSessionFilePath, "utf-8");
|
|
1292
|
-
|
|
1320
|
+
const storedAuth = JSON.parse(content);
|
|
1321
|
+
const activeRuntimeTarget = getActiveRuntimeTarget();
|
|
1322
|
+
if (!isStoredAuthForRuntimeTarget({
|
|
1323
|
+
storedRuntimeTarget: storedAuth.runtimeTarget,
|
|
1324
|
+
activeRuntimeTarget
|
|
1325
|
+
})) {
|
|
1326
|
+
logger6.warn("Ignoring stored auth issued for a different runtime target", {
|
|
1327
|
+
storedRuntimeTarget: storedAuth.runtimeTarget ?? "unrecorded",
|
|
1328
|
+
activeRuntimeTarget
|
|
1329
|
+
});
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
return storedAuth;
|
|
1293
1333
|
} catch (error) {
|
|
1294
1334
|
logger6.error("Failed to load stored auth", {
|
|
1295
1335
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -1370,7 +1410,8 @@ var AuthService = class {
|
|
|
1370
1410
|
refreshToken: tokenData.refresh_token ?? storedAuth.refreshToken,
|
|
1371
1411
|
expiresAt: newExpiresAt,
|
|
1372
1412
|
email: storedAuth.email,
|
|
1373
|
-
userId: storedAuth.userId
|
|
1413
|
+
userId: storedAuth.userId,
|
|
1414
|
+
runtimeTarget: storedAuth.runtimeTarget ?? getActiveRuntimeTarget()
|
|
1374
1415
|
};
|
|
1375
1416
|
const dir = path11.dirname(this.oauthSessionFilePath);
|
|
1376
1417
|
if (!fs6.existsSync(dir)) {
|
|
@@ -7727,4 +7768,4 @@ var WATCHER_LIFETIME_SECONDS = {
|
|
|
7727
7768
|
["never" /* Never */]: WATCHER_LIFETIME_UNBOUNDED_SECONDS
|
|
7728
7769
|
};
|
|
7729
7770
|
|
|
7730
|
-
export { DEFAULT_PREFERENCES, ElectronAppReleaseStream, EventName, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RuntimeTarget, ServiceName, Surface, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, __export, __require, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports2 as e2e_exports, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir2 as getDataDir, getDisclosureCopy, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, hasShownDisclosure, initTelemetry, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports2 as local_exports, markDisclosureShown, mcp_exports, openBrowserUrl, performLogin, performLogout, pollDeviceCode, reconcileProjectPreferences, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveElectronAppPathOrNull, resolvePreferences, resolveRuntimeTarget, saveApiKey, saveApiKeyData, saveCredentials, src_exports, startDeviceCodeFlow, toolRequiresAuth, track, validatePreference, verifyFileChecksum, writePreferences2 as writePreferences };
|
|
7771
|
+
export { DEFAULT_PREFERENCES, ElectronAppReleaseStream, EventName, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RuntimeTarget, ServiceName, Surface, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, __export, __require, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports2 as e2e_exports, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir2 as getDataDir, getDisclosureCopy, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppSignedFromVersion, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getReleaseSignerIdentityUri, getValidApiKeyData, getValidCredentials, hasApiKey, hasShownDisclosure, initTelemetry, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports2 as local_exports, markDisclosureShown, mcp_exports, openBrowserUrl, performLogin, performLogout, pollDeviceCode, reconcileProjectPreferences, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveElectronAppPathOrNull, resolvePreferences, resolveRuntimeTarget, saveApiKey, saveApiKeyData, saveCredentials, src_exports, startDeviceCodeFlow, toolRequiresAuth, track, validatePreference, verifyFileChecksum, writePreferences2 as writePreferences };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, getDisclosureCopy, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-
|
|
1
|
+
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, getDisclosureCopy, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-DJ6W5PZA.js';
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
@@ -14,6 +14,8 @@ import axios from 'axios';
|
|
|
14
14
|
import { platform, homedir, arch } from 'os';
|
|
15
15
|
import { execFile } from 'child_process';
|
|
16
16
|
import { pipeline } from 'stream/promises';
|
|
17
|
+
import { readFile } from 'fs/promises';
|
|
18
|
+
import { verify } from 'sigstore';
|
|
17
19
|
|
|
18
20
|
var registeredTools = [];
|
|
19
21
|
function registerTools(tools) {
|
|
@@ -751,7 +753,7 @@ async function resolveGsScreenshotUrls(report, opts) {
|
|
|
751
753
|
if (gsUrls.length === 0) {
|
|
752
754
|
return report;
|
|
753
755
|
}
|
|
754
|
-
const mcps = await import('./src-
|
|
756
|
+
const mcps = await import('./src-FRYBTZ6J.js');
|
|
755
757
|
const credentials = await mcps.getCallerCredentialsAsync();
|
|
756
758
|
if (!credentials.bearerToken && !credentials.apiKey) {
|
|
757
759
|
stderrWrite(
|
|
@@ -1731,6 +1733,79 @@ async function serveCommand(options) {
|
|
|
1731
1733
|
process.exit(1);
|
|
1732
1734
|
}
|
|
1733
1735
|
}
|
|
1736
|
+
|
|
1737
|
+
// scripts/release-integrity/constants.mjs
|
|
1738
|
+
var SIGNATURE_BUNDLE_SUFFIX = ".sigstore.json";
|
|
1739
|
+
var SIGNER_CERTIFICATE_ISSUER = "https://token.actions.githubusercontent.com";
|
|
1740
|
+
var SIGNATURE_FETCH_TIMEOUT_MS = 3e4;
|
|
1741
|
+
|
|
1742
|
+
// scripts/release-integrity/compareVersions.mjs
|
|
1743
|
+
function compareVersions2(a, b) {
|
|
1744
|
+
const partsA = a.split(".").map(Number);
|
|
1745
|
+
const partsB = b.split(".").map(Number);
|
|
1746
|
+
for (let index = 0; index < 3; index++) {
|
|
1747
|
+
const partA = partsA[index] || 0;
|
|
1748
|
+
const partB = partsB[index] || 0;
|
|
1749
|
+
if (partA > partB) {
|
|
1750
|
+
return 1;
|
|
1751
|
+
}
|
|
1752
|
+
if (partA < partB) {
|
|
1753
|
+
return -1;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return 0;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// scripts/release-integrity/resolveIntegrityPolicy.mjs
|
|
1760
|
+
function resolveIntegrityPolicy({ version, signedFromVersion, expectedChecksum }) {
|
|
1761
|
+
const requiresSignature = Boolean(signedFromVersion) && compareVersions2(version, signedFromVersion) >= 0;
|
|
1762
|
+
const hasChecksum = Boolean(expectedChecksum && expectedChecksum.trim());
|
|
1763
|
+
if (!requiresSignature && !hasChecksum) {
|
|
1764
|
+
return {
|
|
1765
|
+
requiresSignature: false,
|
|
1766
|
+
requiresChecksum: false,
|
|
1767
|
+
unverifiableReason: `no integrity evidence is available for v${version}: it predates release signing (first signed version: ${signedFromVersion || "none configured"}) and no checksum is configured for this release stream`
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
return {
|
|
1771
|
+
requiresSignature,
|
|
1772
|
+
requiresChecksum: !requiresSignature,
|
|
1773
|
+
unverifiableReason: ""
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
async function verifyReleaseSignature({ artifactPath, bundleUrl, signerIdentityUri }) {
|
|
1777
|
+
if (!signerIdentityUri) {
|
|
1778
|
+
return { valid: false, reason: "no signer identity is configured to verify against" };
|
|
1779
|
+
}
|
|
1780
|
+
const controller = new AbortController();
|
|
1781
|
+
const timer = setTimeout(() => controller.abort(), SIGNATURE_FETCH_TIMEOUT_MS);
|
|
1782
|
+
let bundle;
|
|
1783
|
+
try {
|
|
1784
|
+
const response = await fetch(bundleUrl, { signal: controller.signal });
|
|
1785
|
+
if (!response.ok) {
|
|
1786
|
+
return {
|
|
1787
|
+
valid: false,
|
|
1788
|
+
reason: `signature bundle unavailable (HTTP ${response.status}) at ${bundleUrl}`
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
bundle = await response.json();
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
return { valid: false, reason: `could not fetch signature bundle: ${error.message}` };
|
|
1794
|
+
} finally {
|
|
1795
|
+
clearTimeout(timer);
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
await verify(bundle, await readFile(artifactPath), {
|
|
1799
|
+
certificateIssuer: SIGNER_CERTIFICATE_ISSUER,
|
|
1800
|
+
certificateIdentityURI: signerIdentityUri
|
|
1801
|
+
});
|
|
1802
|
+
return { valid: true, reason: "" };
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
return { valid: false, reason: error.message };
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/cli/setup.ts
|
|
1734
1809
|
var logger6 = getLogger();
|
|
1735
1810
|
var MAX_RETRY_ATTEMPTS = 3;
|
|
1736
1811
|
var RETRY_BASE_DELAY_MS = 2e3;
|
|
@@ -1922,9 +1997,30 @@ async function setupCommand(options) {
|
|
|
1922
1997
|
mkdirSync(versionDir, { recursive: true });
|
|
1923
1998
|
const tempFile = path.join(versionDir, binaryName);
|
|
1924
1999
|
await downloadWithRetry(downloadUrl, tempFile);
|
|
1925
|
-
console.log("Download complete, verifying
|
|
2000
|
+
console.log("Download complete, verifying integrity...");
|
|
1926
2001
|
const checksums = getElectronAppChecksums();
|
|
1927
2002
|
const expectedChecksum = getChecksumForPlatform(checksums);
|
|
2003
|
+
const integrityPolicy = resolveIntegrityPolicy({
|
|
2004
|
+
version,
|
|
2005
|
+
signedFromVersion: getElectronAppSignedFromVersion(),
|
|
2006
|
+
expectedChecksum: expectedChecksum ?? ""
|
|
2007
|
+
});
|
|
2008
|
+
if (integrityPolicy.unverifiableReason) {
|
|
2009
|
+
cleanupFailedInstall(versionDir);
|
|
2010
|
+
throw new Error("Refusing to install an unverifiable download: " + integrityPolicy.unverifiableReason);
|
|
2011
|
+
}
|
|
2012
|
+
if (integrityPolicy.requiresSignature) {
|
|
2013
|
+
const signatureResult = await verifyReleaseSignature({
|
|
2014
|
+
artifactPath: tempFile,
|
|
2015
|
+
bundleUrl: downloadUrl + SIGNATURE_BUNDLE_SUFFIX,
|
|
2016
|
+
signerIdentityUri: getReleaseSignerIdentityUri()
|
|
2017
|
+
});
|
|
2018
|
+
if (!signatureResult.valid) {
|
|
2019
|
+
cleanupFailedInstall(versionDir);
|
|
2020
|
+
throw new Error("Signature verification failed, refusing to install: " + signatureResult.reason);
|
|
2021
|
+
}
|
|
2022
|
+
console.log("Signature verified successfully.");
|
|
2023
|
+
}
|
|
1928
2024
|
const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum);
|
|
1929
2025
|
if (!checksumResult.valid && expectedChecksum) {
|
|
1930
2026
|
cleanupFailedInstall(versionDir);
|
|
@@ -1937,8 +2033,6 @@ The downloaded file may be corrupted or tampered with.`
|
|
|
1937
2033
|
}
|
|
1938
2034
|
if (expectedChecksum) {
|
|
1939
2035
|
console.log("Checksum verified successfully.");
|
|
1940
|
-
} else {
|
|
1941
|
-
console.log("Warning: No checksum configured, skipping verification.");
|
|
1942
2036
|
}
|
|
1943
2037
|
console.log("Extracting...");
|
|
1944
2038
|
if (binaryName.endsWith(".zip")) {
|
|
@@ -2067,21 +2161,6 @@ async function checkForUpdates() {
|
|
|
2067
2161
|
throw new Error(`Failed to check for updates: ${errorMessage}`, { cause: error });
|
|
2068
2162
|
}
|
|
2069
2163
|
}
|
|
2070
|
-
function compareVersions2(a, b) {
|
|
2071
|
-
const partsA = a.split(".").map(Number);
|
|
2072
|
-
const partsB = b.split(".").map(Number);
|
|
2073
|
-
for (let i = 0; i < 3; i++) {
|
|
2074
|
-
const partA = partsA[i] || 0;
|
|
2075
|
-
const partB = partsB[i] || 0;
|
|
2076
|
-
if (partA > partB) {
|
|
2077
|
-
return 1;
|
|
2078
|
-
}
|
|
2079
|
-
if (partA < partB) {
|
|
2080
|
-
return -1;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
return 0;
|
|
2084
|
-
}
|
|
2085
2164
|
function getExpectedExecutablePath3(versionDir) {
|
|
2086
2165
|
const os = platform();
|
|
2087
2166
|
switch (os) {
|
|
@@ -2208,11 +2287,32 @@ async function downloadAndInstall(version, downloadUrl, checksum) {
|
|
|
2208
2287
|
throw new Error("No response body");
|
|
2209
2288
|
}
|
|
2210
2289
|
await pipeline(response.body, fileStream);
|
|
2211
|
-
console.log("Download complete, verifying
|
|
2290
|
+
console.log("Download complete, verifying integrity...");
|
|
2212
2291
|
let expectedChecksum = checksum;
|
|
2213
2292
|
if (!expectedChecksum) {
|
|
2214
2293
|
expectedChecksum = await fetchChecksumFromRelease(version);
|
|
2215
2294
|
}
|
|
2295
|
+
const integrityPolicy = resolveIntegrityPolicy({
|
|
2296
|
+
version,
|
|
2297
|
+
signedFromVersion: getElectronAppSignedFromVersion(),
|
|
2298
|
+
expectedChecksum: expectedChecksum ?? ""
|
|
2299
|
+
});
|
|
2300
|
+
if (integrityPolicy.unverifiableReason) {
|
|
2301
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
2302
|
+
throw new Error("Refusing to install an unverifiable download: " + integrityPolicy.unverifiableReason);
|
|
2303
|
+
}
|
|
2304
|
+
if (integrityPolicy.requiresSignature) {
|
|
2305
|
+
const signatureResult = await verifyReleaseSignature({
|
|
2306
|
+
artifactPath: tempFile,
|
|
2307
|
+
bundleUrl: downloadUrl + SIGNATURE_BUNDLE_SUFFIX,
|
|
2308
|
+
signerIdentityUri: getReleaseSignerIdentityUri()
|
|
2309
|
+
});
|
|
2310
|
+
if (!signatureResult.valid) {
|
|
2311
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
2312
|
+
throw new Error("Signature verification failed, refusing to install: " + signatureResult.reason);
|
|
2313
|
+
}
|
|
2314
|
+
console.log("Signature verified successfully.");
|
|
2315
|
+
}
|
|
2216
2316
|
const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum || "");
|
|
2217
2317
|
if (!checksumResult.valid && expectedChecksum) {
|
|
2218
2318
|
rmSync(versionDir, { recursive: true, force: true });
|
|
@@ -2225,8 +2325,6 @@ The downloaded file may be corrupted or tampered with.`
|
|
|
2225
2325
|
}
|
|
2226
2326
|
if (expectedChecksum) {
|
|
2227
2327
|
console.log("Checksum verified successfully.");
|
|
2228
|
-
} else {
|
|
2229
|
-
console.log("Warning: No checksum available, skipping verification.");
|
|
2230
2328
|
}
|
|
2231
2329
|
console.log("Extracting...");
|
|
2232
2330
|
if (binaryName.endsWith(".zip")) {
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
2
|
-
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-
|
|
1
|
+
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-VTSS6TGQ.js';
|
|
2
|
+
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-DJ6W5PZA.js';
|
|
@@ -24,6 +24,8 @@ Enforcement is reserved for the handoffs that were being skipped: the E2E accept
|
|
|
24
24
|
|
|
25
25
|
## Mechanism
|
|
26
26
|
|
|
27
|
+
A recorder that *clears* an obligation — a walkthrough posted, a failure diagnosed, a test case classified — records only when the call did not visibly fail (`callOutcome.ts`). Recording from the request alone let a rejected `gh pr comment` mark the walkthrough posted, so the gate went quiet on a PR that never received it.
|
|
28
|
+
|
|
27
29
|
Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json`. The wrapper pipes the event payload (stdin JSON) to the bundled `../scripts/guardrails.mjs <subcommand>`, which holds the decision logic (built from `src/guardrails/`, vitest-covered). Per-session state in `~/.muggle-ai/guardrails/<session_id>.json` tracks what fired. Any *failure* degrades to `{}` (allow) — a gate blocks only by an explicit, tested decision, never by accident.
|
|
28
30
|
|
|
29
31
|
Each wrapper short-circuits in shell first, so the common case never pays Node cold-start. A gate that has spent its block budget stamps a `<gate>Released` flag, which its wrapper then pre-filters on — without it a released gate keeps cold-starting Node on every remaining turn end to answer `{}`, and the walkthrough gate keeps making provider calls to do it. That pre-filter is a second, looser copy of what `guardrails.mjs` matches, and it is the one place a guardrail can fail *silently*: a payload it drops — a skip marker, a reopen line, a comment edit — reaches no recorder, and the gate keeps demanding an action the user already took. Over-matching is free; under-matching is a dead escape hatch. `src/test/guardrails/hook-prefilter.test.ts` pins every payload each subcommand acts on against the wrapper guarding it, and derives the skip-marker tokens from source so a new marker is covered the moment it exists.
|
|
@@ -40,6 +40,14 @@ var MAX_REPLY_BLOCKS = 3;
|
|
|
40
40
|
var CAPABILITY_CLAIM_TRANSCRIPT_TAIL_BYTES = 64e3;
|
|
41
41
|
var SESSION_STATE_LOCK_WAIT_MS = 250;
|
|
42
42
|
var LOCK_POLL_INTERVAL_MS = 10;
|
|
43
|
+
var CALL_FAILURE_SIGNALS = [
|
|
44
|
+
/\bgh:\s/,
|
|
45
|
+
/\bglab:\s/,
|
|
46
|
+
/\bHTTP\s(?:4|5)\d\d\b/,
|
|
47
|
+
/"status"\s*:\s*"?(?:4|5)\d\d/,
|
|
48
|
+
/"isError"\s*:\s*true/
|
|
49
|
+
];
|
|
50
|
+
var FORGE_TERMINAL_CMD = /\b(?:gh\s+pr\s+(?:merge|close|reopen)|glab\s+mr\s+(?:merge|close|reopen))\b/;
|
|
43
51
|
|
|
44
52
|
// src/guardrails/store/fileLock.ts
|
|
45
53
|
function isProcessAlive(pid) {
|
|
@@ -159,15 +167,21 @@ ${input2.tool_response?.output ?? ""}`;
|
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
// src/guardrails/prTerminal.ts
|
|
170
|
+
function terminalProvenance(input2) {
|
|
171
|
+
const command = input2.tool_input?.command;
|
|
172
|
+
if (command === void 0) return { acceptsForgeLine: true };
|
|
173
|
+
return { acceptsForgeLine: FORGE_TERMINAL_CMD.test(command) };
|
|
174
|
+
}
|
|
162
175
|
function detectPrTerminal(input2) {
|
|
163
176
|
if (input2.tool_name !== "Bash" && input2.tool_name !== "Monitor") return null;
|
|
164
177
|
const response = input2.tool_response;
|
|
178
|
+
const provenance = terminalProvenance(input2);
|
|
165
179
|
const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
|
|
166
|
-
const mergedMatch = haystack.match(GH_PR_MERGED_LINE);
|
|
180
|
+
const mergedMatch = provenance.acceptsForgeLine ? haystack.match(GH_PR_MERGED_LINE) : null;
|
|
167
181
|
if (mergedMatch) {
|
|
168
182
|
return { prNumber: Number(mergedMatch[1]), verdict: "merged" /* Merged */ };
|
|
169
183
|
}
|
|
170
|
-
const closedMatch = haystack.match(GH_PR_CLOSED_LINE);
|
|
184
|
+
const closedMatch = provenance.acceptsForgeLine ? haystack.match(GH_PR_CLOSED_LINE) : null;
|
|
171
185
|
if (closedMatch) {
|
|
172
186
|
return { prNumber: Number(closedMatch[1]), verdict: "closed" /* Closed */ };
|
|
173
187
|
}
|
|
@@ -182,6 +196,7 @@ function detectPrTerminal(input2) {
|
|
|
182
196
|
}
|
|
183
197
|
function detectPrReopened(input2) {
|
|
184
198
|
if (input2.tool_name !== "Bash") return null;
|
|
199
|
+
if (!terminalProvenance(input2).acceptsForgeLine) return null;
|
|
185
200
|
const response = input2.tool_response;
|
|
186
201
|
const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
|
|
187
202
|
const reopenedMatch = haystack.match(GH_PR_REOPENED_LINE);
|
|
@@ -362,11 +377,28 @@ function stageGateDecision(state, unreadStagePaths, maxBlocks = MAX_STAGE_BLOCKS
|
|
|
362
377
|
return { action: "block" /* Block */, blockCount: blockCount + 1, unread: unreadStagePaths };
|
|
363
378
|
}
|
|
364
379
|
|
|
380
|
+
// src/guardrails/callOutcome.ts
|
|
381
|
+
function renderedResponse(toolResponse) {
|
|
382
|
+
const rendered = [];
|
|
383
|
+
const collect = (value) => {
|
|
384
|
+
if (typeof value === "string") rendered.push(value);
|
|
385
|
+
else if (Array.isArray(value)) value.forEach(collect);
|
|
386
|
+
else if (value && typeof value === "object") Object.values(value).forEach(collect);
|
|
387
|
+
};
|
|
388
|
+
collect(toolResponse);
|
|
389
|
+
return rendered.join("\n");
|
|
390
|
+
}
|
|
391
|
+
function callFailed(input2) {
|
|
392
|
+
const rendered = renderedResponse(input2.tool_response);
|
|
393
|
+
return CALL_FAILURE_SIGNALS.some((signal) => signal.test(rendered));
|
|
394
|
+
}
|
|
395
|
+
|
|
365
396
|
// src/guardrails/preExecutionClassification.ts
|
|
366
397
|
var CLASSIFICATION_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_CLASSIFY_SKIP\b/;
|
|
367
398
|
function detectClassifiedTestCaseId(input2) {
|
|
368
399
|
if (!MUGGLE_EVENT_EMIT_TOOL.test(input2.tool_name ?? "")) return void 0;
|
|
369
400
|
if (input2.tool_input?.eventType !== PRE_EXECUTION_CLASSIFICATION_EVENT) return void 0;
|
|
401
|
+
if (callFailed(input2)) return void 0;
|
|
370
402
|
return input2.tool_input?.testCaseId ?? ANY_TEST_CASE;
|
|
371
403
|
}
|
|
372
404
|
function applyClassifiedTestCase(state, testCaseId) {
|
|
@@ -423,6 +455,7 @@ function detectDebugEvidenceRunIds(input2, owedRunIds) {
|
|
|
423
455
|
const toolName = input2.tool_name ?? "";
|
|
424
456
|
const isDiagnosisEmit = MUGGLE_EVENT_EMIT_TOOL.test(toolName) && FAILURE_DIAGNOSIS_EVENT.test(input2.tool_input?.eventType ?? "");
|
|
425
457
|
if (!isDiagnosisEmit && !MUGGLE_FEEDBACK_CREATE_TOOL.test(toolName)) return [];
|
|
458
|
+
if (callFailed(input2)) return [];
|
|
426
459
|
const payload = serialize(input2);
|
|
427
460
|
return owedRunIds.filter((runId) => payload.includes(runId));
|
|
428
461
|
}
|
|
@@ -598,6 +631,7 @@ function detectWalkthroughPost(input2, read = defaultFileReader) {
|
|
|
598
631
|
if (input2.tool_name !== "Bash") return false;
|
|
599
632
|
const cmd = input2.tool_input?.command ?? "";
|
|
600
633
|
if (!isPrReportPostCommand(cmd)) return false;
|
|
634
|
+
if (callFailed(input2)) return false;
|
|
601
635
|
return collectPrPostText(cmd, input2.cwd, read).includes(REPORT_SENTINEL);
|
|
602
636
|
}
|
|
603
637
|
function applyWalkthroughPosted(state, posted) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"release": "5.11.0",
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-08-
|
|
3
|
+
"buildId": "run-75-1",
|
|
4
|
+
"commitSha": "d2d28543763361dcae7b5bef1c31ca4ae75df64c",
|
|
5
|
+
"buildTime": "2026-08-26T20:14:09Z",
|
|
6
6
|
"serviceName": "muggle-ai-works-mcp"
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { DEFAULT_PREFERENCES, ElectronAppReleaseStream, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RuntimeTarget, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports as e2e, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports as localQa, mcp_exports as mcp, openBrowserUrl, performLogin, performLogout, pollDeviceCode, e2e_exports as qa, reconcileProjectPreferences, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveElectronAppPathOrNull, resolvePreferences, resolveRuntimeTarget, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, toolRequiresAuth, validatePreference, verifyFileChecksum, writePreferences } from './chunk-
|
|
1
|
+
export { DEFAULT_PREFERENCES, ElectronAppReleaseStream, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RuntimeTarget, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports as e2e, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppSignedFromVersion, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getReleaseSignerIdentityUri, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports as localQa, mcp_exports as mcp, openBrowserUrl, performLogin, performLogout, pollDeviceCode, e2e_exports as qa, reconcileProjectPreferences, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveElectronAppPathOrNull, resolvePreferences, resolveRuntimeTarget, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, toolRequiresAuth, validatePreference, verifyFileChecksum, writePreferences } from './chunk-DJ6W5PZA.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muggleai/works",
|
|
3
3
|
"mcpName": "io.github.multiplex-ai/muggle",
|
|
4
|
-
"version": "5.12.0-staging.
|
|
4
|
+
"version": "5.12.0-staging.75",
|
|
5
5
|
"description": "Ship quality products with AI-powered E2E acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"plugin",
|
|
14
14
|
"bin/muggle.js",
|
|
15
15
|
"scripts/postinstall.mjs",
|
|
16
|
+
"scripts/release-integrity",
|
|
16
17
|
"config/runtime-targets.json"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
@@ -65,7 +66,9 @@
|
|
|
65
66
|
"linux-x64": "929396f628a05f001782d48524cd9f76fdbb59216658982a5a3909dfe413cbea",
|
|
66
67
|
"win32-x64": "a8b5b46b97ed788030be84982a3e8d463abbfa7f3e8bc795692ccbffc82bfa92"
|
|
67
68
|
}
|
|
68
|
-
}
|
|
69
|
+
},
|
|
70
|
+
"electronAppSignedFromVersion": "1.10.0",
|
|
71
|
+
"signerIdentityUri": "https://github.com/multiplex-ai/muggle-ai-teaching-service/.github/workflows/release-electron-app-reusable.yml@refs/heads/master"
|
|
69
72
|
},
|
|
70
73
|
"dependencies": {
|
|
71
74
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
@@ -73,6 +76,7 @@
|
|
|
73
76
|
"axios": "^1.7.9",
|
|
74
77
|
"commander": "^14.0.3",
|
|
75
78
|
"open": "^11.0.0",
|
|
79
|
+
"sigstore": "^5.0.0",
|
|
76
80
|
"ulid": "^3.0.2",
|
|
77
81
|
"uuid": "^14.0.0",
|
|
78
82
|
"winston": "^3.17.0",
|
package/plugin/hooks/README.md
CHANGED
|
@@ -24,6 +24,8 @@ Enforcement is reserved for the handoffs that were being skipped: the E2E accept
|
|
|
24
24
|
|
|
25
25
|
## Mechanism
|
|
26
26
|
|
|
27
|
+
A recorder that *clears* an obligation — a walkthrough posted, a failure diagnosed, a test case classified — records only when the call did not visibly fail (`callOutcome.ts`). Recording from the request alone let a rejected `gh pr comment` mark the walkthrough posted, so the gate went quiet on a PR that never received it.
|
|
28
|
+
|
|
27
29
|
Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json`. The wrapper pipes the event payload (stdin JSON) to the bundled `../scripts/guardrails.mjs <subcommand>`, which holds the decision logic (built from `src/guardrails/`, vitest-covered). Per-session state in `~/.muggle-ai/guardrails/<session_id>.json` tracks what fired. Any *failure* degrades to `{}` (allow) — a gate blocks only by an explicit, tested decision, never by accident.
|
|
28
30
|
|
|
29
31
|
Each wrapper short-circuits in shell first, so the common case never pays Node cold-start. A gate that has spent its block budget stamps a `<gate>Released` flag, which its wrapper then pre-filters on — without it a released gate keeps cold-starting Node on every remaining turn end to answer `{}`, and the walkthrough gate keeps making provider calls to do it. That pre-filter is a second, looser copy of what `guardrails.mjs` matches, and it is the one place a guardrail can fail *silently*: a payload it drops — a skip marker, a reopen line, a comment edit — reaches no recorder, and the gate keeps demanding an action the user already took. Over-matching is free; under-matching is a dead escape hatch. `src/test/guardrails/hook-prefilter.test.ts` pins every payload each subcommand acts on against the wrapper guarding it, and derives the skip-marker tokens from source so a new marker is covered the moment it exists.
|
|
@@ -40,6 +40,14 @@ var MAX_REPLY_BLOCKS = 3;
|
|
|
40
40
|
var CAPABILITY_CLAIM_TRANSCRIPT_TAIL_BYTES = 64e3;
|
|
41
41
|
var SESSION_STATE_LOCK_WAIT_MS = 250;
|
|
42
42
|
var LOCK_POLL_INTERVAL_MS = 10;
|
|
43
|
+
var CALL_FAILURE_SIGNALS = [
|
|
44
|
+
/\bgh:\s/,
|
|
45
|
+
/\bglab:\s/,
|
|
46
|
+
/\bHTTP\s(?:4|5)\d\d\b/,
|
|
47
|
+
/"status"\s*:\s*"?(?:4|5)\d\d/,
|
|
48
|
+
/"isError"\s*:\s*true/
|
|
49
|
+
];
|
|
50
|
+
var FORGE_TERMINAL_CMD = /\b(?:gh\s+pr\s+(?:merge|close|reopen)|glab\s+mr\s+(?:merge|close|reopen))\b/;
|
|
43
51
|
|
|
44
52
|
// src/guardrails/store/fileLock.ts
|
|
45
53
|
function isProcessAlive(pid) {
|
|
@@ -159,15 +167,21 @@ ${input2.tool_response?.output ?? ""}`;
|
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
// src/guardrails/prTerminal.ts
|
|
170
|
+
function terminalProvenance(input2) {
|
|
171
|
+
const command = input2.tool_input?.command;
|
|
172
|
+
if (command === void 0) return { acceptsForgeLine: true };
|
|
173
|
+
return { acceptsForgeLine: FORGE_TERMINAL_CMD.test(command) };
|
|
174
|
+
}
|
|
162
175
|
function detectPrTerminal(input2) {
|
|
163
176
|
if (input2.tool_name !== "Bash" && input2.tool_name !== "Monitor") return null;
|
|
164
177
|
const response = input2.tool_response;
|
|
178
|
+
const provenance = terminalProvenance(input2);
|
|
165
179
|
const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
|
|
166
|
-
const mergedMatch = haystack.match(GH_PR_MERGED_LINE);
|
|
180
|
+
const mergedMatch = provenance.acceptsForgeLine ? haystack.match(GH_PR_MERGED_LINE) : null;
|
|
167
181
|
if (mergedMatch) {
|
|
168
182
|
return { prNumber: Number(mergedMatch[1]), verdict: "merged" /* Merged */ };
|
|
169
183
|
}
|
|
170
|
-
const closedMatch = haystack.match(GH_PR_CLOSED_LINE);
|
|
184
|
+
const closedMatch = provenance.acceptsForgeLine ? haystack.match(GH_PR_CLOSED_LINE) : null;
|
|
171
185
|
if (closedMatch) {
|
|
172
186
|
return { prNumber: Number(closedMatch[1]), verdict: "closed" /* Closed */ };
|
|
173
187
|
}
|
|
@@ -182,6 +196,7 @@ function detectPrTerminal(input2) {
|
|
|
182
196
|
}
|
|
183
197
|
function detectPrReopened(input2) {
|
|
184
198
|
if (input2.tool_name !== "Bash") return null;
|
|
199
|
+
if (!terminalProvenance(input2).acceptsForgeLine) return null;
|
|
185
200
|
const response = input2.tool_response;
|
|
186
201
|
const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
|
|
187
202
|
const reopenedMatch = haystack.match(GH_PR_REOPENED_LINE);
|
|
@@ -362,11 +377,28 @@ function stageGateDecision(state, unreadStagePaths, maxBlocks = MAX_STAGE_BLOCKS
|
|
|
362
377
|
return { action: "block" /* Block */, blockCount: blockCount + 1, unread: unreadStagePaths };
|
|
363
378
|
}
|
|
364
379
|
|
|
380
|
+
// src/guardrails/callOutcome.ts
|
|
381
|
+
function renderedResponse(toolResponse) {
|
|
382
|
+
const rendered = [];
|
|
383
|
+
const collect = (value) => {
|
|
384
|
+
if (typeof value === "string") rendered.push(value);
|
|
385
|
+
else if (Array.isArray(value)) value.forEach(collect);
|
|
386
|
+
else if (value && typeof value === "object") Object.values(value).forEach(collect);
|
|
387
|
+
};
|
|
388
|
+
collect(toolResponse);
|
|
389
|
+
return rendered.join("\n");
|
|
390
|
+
}
|
|
391
|
+
function callFailed(input2) {
|
|
392
|
+
const rendered = renderedResponse(input2.tool_response);
|
|
393
|
+
return CALL_FAILURE_SIGNALS.some((signal) => signal.test(rendered));
|
|
394
|
+
}
|
|
395
|
+
|
|
365
396
|
// src/guardrails/preExecutionClassification.ts
|
|
366
397
|
var CLASSIFICATION_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_CLASSIFY_SKIP\b/;
|
|
367
398
|
function detectClassifiedTestCaseId(input2) {
|
|
368
399
|
if (!MUGGLE_EVENT_EMIT_TOOL.test(input2.tool_name ?? "")) return void 0;
|
|
369
400
|
if (input2.tool_input?.eventType !== PRE_EXECUTION_CLASSIFICATION_EVENT) return void 0;
|
|
401
|
+
if (callFailed(input2)) return void 0;
|
|
370
402
|
return input2.tool_input?.testCaseId ?? ANY_TEST_CASE;
|
|
371
403
|
}
|
|
372
404
|
function applyClassifiedTestCase(state, testCaseId) {
|
|
@@ -423,6 +455,7 @@ function detectDebugEvidenceRunIds(input2, owedRunIds) {
|
|
|
423
455
|
const toolName = input2.tool_name ?? "";
|
|
424
456
|
const isDiagnosisEmit = MUGGLE_EVENT_EMIT_TOOL.test(toolName) && FAILURE_DIAGNOSIS_EVENT.test(input2.tool_input?.eventType ?? "");
|
|
425
457
|
if (!isDiagnosisEmit && !MUGGLE_FEEDBACK_CREATE_TOOL.test(toolName)) return [];
|
|
458
|
+
if (callFailed(input2)) return [];
|
|
426
459
|
const payload = serialize(input2);
|
|
427
460
|
return owedRunIds.filter((runId) => payload.includes(runId));
|
|
428
461
|
}
|
|
@@ -598,6 +631,7 @@ function detectWalkthroughPost(input2, read = defaultFileReader) {
|
|
|
598
631
|
if (input2.tool_name !== "Bash") return false;
|
|
599
632
|
const cmd = input2.tool_input?.command ?? "";
|
|
600
633
|
if (!isPrReportPostCommand(cmd)) return false;
|
|
634
|
+
if (callFailed(input2)) return false;
|
|
601
635
|
return collectPrPostText(cmd, input2.cwd, read).includes(REPORT_SENTINEL);
|
|
602
636
|
}
|
|
603
637
|
function applyWalkthroughPosted(state, posted) {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -25,6 +25,11 @@ import { dirname, join } from "path";
|
|
|
25
25
|
import { pipeline } from "stream/promises";
|
|
26
26
|
import { createRequire } from "module";
|
|
27
27
|
import { fileURLToPath } from "url";
|
|
28
|
+
import {
|
|
29
|
+
SIGNATURE_BUNDLE_SUFFIX,
|
|
30
|
+
resolveIntegrityPolicy,
|
|
31
|
+
verifyReleaseSignature,
|
|
32
|
+
} from "./release-integrity/index.mjs";
|
|
28
33
|
|
|
29
34
|
const require = createRequire(import.meta.url);
|
|
30
35
|
const VERSION_DIRECTORY_NAME_PATTERN = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
|
|
@@ -728,7 +733,35 @@ async function downloadElectronApp() {
|
|
|
728
733
|
clearTimeout(streamTimer);
|
|
729
734
|
}
|
|
730
735
|
|
|
731
|
-
log("Download complete, verifying
|
|
736
|
+
log("Download complete, verifying integrity...");
|
|
737
|
+
|
|
738
|
+
const integrityPolicy = resolveIntegrityPolicy({
|
|
739
|
+
version: version,
|
|
740
|
+
signedFromVersion: config.electronAppSignedFromVersion || "",
|
|
741
|
+
expectedChecksum: expectedChecksum,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
if (integrityPolicy.unverifiableReason) {
|
|
745
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
746
|
+
throw new Error("Refusing to install an unverifiable download: " + integrityPolicy.unverifiableReason);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (integrityPolicy.requiresSignature) {
|
|
750
|
+
const signatureResult = await verifyReleaseSignature({
|
|
751
|
+
artifactPath: tempFile,
|
|
752
|
+
bundleUrl: downloadUrl + SIGNATURE_BUNDLE_SUFFIX,
|
|
753
|
+
signerIdentityUri: config.signerIdentityUri || "",
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
if (!signatureResult.valid) {
|
|
757
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
758
|
+
throw new Error(
|
|
759
|
+
"Signature verification failed, refusing to install: " + signatureResult.reason,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
log("Signature verified successfully.");
|
|
764
|
+
}
|
|
732
765
|
|
|
733
766
|
// Verify checksum
|
|
734
767
|
const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum);
|
|
@@ -743,9 +776,7 @@ async function downloadElectronApp() {
|
|
|
743
776
|
);
|
|
744
777
|
}
|
|
745
778
|
|
|
746
|
-
if (checksumResult.skipped) {
|
|
747
|
-
log("Warning: No checksum configured, skipping verification.");
|
|
748
|
-
} else {
|
|
779
|
+
if (!checksumResult.skipped) {
|
|
749
780
|
log("Checksum verified successfully.");
|
|
750
781
|
}
|
|
751
782
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two semver versions.
|
|
3
|
+
* @param {string} a - First version.
|
|
4
|
+
* @param {string} b - Second version.
|
|
5
|
+
* @returns {number} 1 if a > b, -1 if a < b, 0 if equal.
|
|
6
|
+
*/
|
|
7
|
+
export function compareVersions(a, b) {
|
|
8
|
+
const partsA = a.split(".").map(Number);
|
|
9
|
+
const partsB = b.split(".").map(Number);
|
|
10
|
+
|
|
11
|
+
for (let index = 0; index < 3; index++) {
|
|
12
|
+
const partA = partsA[index] || 0;
|
|
13
|
+
const partB = partsB[index] || 0;
|
|
14
|
+
|
|
15
|
+
if (partA > partB) {
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
if (partA < partB) {
|
|
19
|
+
return -1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Suffix appended to a release asset's name to locate its Sigstore bundle. */
|
|
2
|
+
export const SIGNATURE_BUNDLE_SUFFIX = ".sigstore.json";
|
|
3
|
+
|
|
4
|
+
/** OIDC issuer that must have issued the signing certificate. */
|
|
5
|
+
export const SIGNER_CERTIFICATE_ISSUER = "https://token.actions.githubusercontent.com";
|
|
6
|
+
|
|
7
|
+
/** Abort budget for fetching a signature bundle. */
|
|
8
|
+
export const SIGNATURE_FETCH_TIMEOUT_MS = 30_000;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const SIGNATURE_BUNDLE_SUFFIX: string;
|
|
2
|
+
export declare const SIGNATURE_FETCH_TIMEOUT_MS: number;
|
|
3
|
+
export declare const SIGNER_CERTIFICATE_ISSUER: string;
|
|
4
|
+
|
|
5
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
6
|
+
|
|
7
|
+
export declare function resolveIntegrityPolicy(params: {
|
|
8
|
+
version: string;
|
|
9
|
+
signedFromVersion: string;
|
|
10
|
+
expectedChecksum: string;
|
|
11
|
+
}): {
|
|
12
|
+
requiresSignature: boolean;
|
|
13
|
+
requiresChecksum: boolean;
|
|
14
|
+
unverifiableReason: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export declare function verifyReleaseSignature(params: {
|
|
18
|
+
artifactPath: string;
|
|
19
|
+
bundleUrl: string;
|
|
20
|
+
signerIdentityUri: string;
|
|
21
|
+
}): Promise<{ valid: boolean; reason: string }>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { SIGNATURE_BUNDLE_SUFFIX, SIGNATURE_FETCH_TIMEOUT_MS, SIGNER_CERTIFICATE_ISSUER } from "./constants.mjs";
|
|
2
|
+
export { compareVersions } from "./compareVersions.mjs";
|
|
3
|
+
export { resolveIntegrityPolicy } from "./resolveIntegrityPolicy.mjs";
|
|
4
|
+
export { verifyReleaseSignature } from "./verifyReleaseSignature.mjs";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { compareVersions } from "./compareVersions.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decide which integrity evidence a download must produce before it is trusted.
|
|
5
|
+
*
|
|
6
|
+
* Releases cut before signing existed carry only a checksum, so the signature
|
|
7
|
+
* requirement is pinned to the first version that ships one. Below that pin the
|
|
8
|
+
* checksum is mandatory rather than advisory: a download with neither signature
|
|
9
|
+
* nor checksum is refused instead of being installed behind a warning.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} params - Policy inputs.
|
|
12
|
+
* @param {string} params.version - Electron app version being installed.
|
|
13
|
+
* @param {string} params.signedFromVersion - First version published with a signature.
|
|
14
|
+
* @param {string} params.expectedChecksum - Configured SHA256, empty when none.
|
|
15
|
+
* @returns {{requiresSignature: boolean, requiresChecksum: boolean, unverifiableReason: string}} Which checks apply, and why none can.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveIntegrityPolicy({ version, signedFromVersion, expectedChecksum }) {
|
|
18
|
+
const requiresSignature = Boolean(signedFromVersion) && compareVersions(version, signedFromVersion) >= 0;
|
|
19
|
+
const hasChecksum = Boolean(expectedChecksum && expectedChecksum.trim());
|
|
20
|
+
|
|
21
|
+
if (!requiresSignature && !hasChecksum) {
|
|
22
|
+
return {
|
|
23
|
+
requiresSignature: false,
|
|
24
|
+
requiresChecksum: false,
|
|
25
|
+
unverifiableReason:
|
|
26
|
+
`no integrity evidence is available for v${version}: it predates release signing ` +
|
|
27
|
+
`(first signed version: ${signedFromVersion || "none configured"}) ` +
|
|
28
|
+
`and no checksum is configured for this release stream`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
requiresSignature: requiresSignature,
|
|
34
|
+
requiresChecksum: !requiresSignature,
|
|
35
|
+
unverifiableReason: "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* global AbortController */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { verify } from "sigstore";
|
|
4
|
+
import { SIGNATURE_FETCH_TIMEOUT_MS, SIGNER_CERTIFICATE_ISSUER } from "./constants.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verify a downloaded release asset against the Sigstore bundle published beside it.
|
|
8
|
+
*
|
|
9
|
+
* The bundle proves the asset was produced by a specific workflow in the studio
|
|
10
|
+
* source repository, so the signer identity is pinned exactly: a bundle that is
|
|
11
|
+
* otherwise valid but was issued to any other workflow is rejected.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} params - Verification parameters.
|
|
14
|
+
* @param {string} params.artifactPath - Path to the downloaded asset.
|
|
15
|
+
* @param {string} params.bundleUrl - URL of the asset's Sigstore bundle.
|
|
16
|
+
* @param {string} params.signerIdentityUri - Certificate subject the signer must carry.
|
|
17
|
+
* @returns {Promise<{valid: boolean, reason: string}>} Verification outcome, with the rejection cause when invalid.
|
|
18
|
+
*/
|
|
19
|
+
export async function verifyReleaseSignature({ artifactPath, bundleUrl, signerIdentityUri }) {
|
|
20
|
+
if (!signerIdentityUri) {
|
|
21
|
+
return { valid: false, reason: "no signer identity is configured to verify against" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), SIGNATURE_FETCH_TIMEOUT_MS);
|
|
26
|
+
let bundle;
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(bundleUrl, { signal: controller.signal });
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
reason: `signature bundle unavailable (HTTP ${response.status}) at ${bundleUrl}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
bundle = await response.json();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { valid: false, reason: `could not fetch signature bundle: ${error.message}` };
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await verify(bundle, await readFile(artifactPath), {
|
|
44
|
+
certificateIssuer: SIGNER_CERTIFICATE_ISSUER,
|
|
45
|
+
certificateIdentityURI: signerIdentityUri,
|
|
46
|
+
});
|
|
47
|
+
return { valid: true, reason: "" };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { valid: false, reason: error.message };
|
|
50
|
+
}
|
|
51
|
+
}
|