multicorn-shield 0.8.0 → 0.10.0
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/CHANGELOG.md +365 -0
- package/LICENSE +1 -1
- package/README.md +29 -1
- package/dist/index.cjs +295 -27
- package/dist/index.d.cts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +293 -28
- package/dist/multicorn-proxy.js +190 -6
- package/dist/multicorn-shield.js +1 -0
- package/dist/shield-extension.js +2 -1
- package/package.json +9 -2
- package/plugins/windsurf/README.md +54 -0
- package/plugins/windsurf/hooks/scripts/post-action.cjs +245 -0
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +646 -0
package/dist/index.js
CHANGED
|
@@ -285,6 +285,39 @@ function hasScope(grantedScopes, requested) {
|
|
|
285
285
|
);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
// src/scopes/content-review-detector.ts
|
|
289
|
+
function requiresContentReview(scope) {
|
|
290
|
+
if (scope.service === "web" && scope.permissionLevel === "publish") {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (scope.service === "public_content" && scope.permissionLevel === "create") {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
function isPublicContentAction(toolName, service) {
|
|
299
|
+
const lowerToolName = toolName.toLowerCase();
|
|
300
|
+
const lowerService = service.toLowerCase();
|
|
301
|
+
if (lowerService === "web" || lowerService === "public_content") {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
const publicContentIndicators = [
|
|
305
|
+
"publish",
|
|
306
|
+
"public",
|
|
307
|
+
"web",
|
|
308
|
+
"blog",
|
|
309
|
+
"post",
|
|
310
|
+
"article",
|
|
311
|
+
"social",
|
|
312
|
+
"twitter",
|
|
313
|
+
"facebook",
|
|
314
|
+
"linkedin",
|
|
315
|
+
"github_pages",
|
|
316
|
+
"deploy"
|
|
317
|
+
];
|
|
318
|
+
return publicContentIndicators.some((indicator) => lowerToolName.includes(indicator));
|
|
319
|
+
}
|
|
320
|
+
|
|
288
321
|
// src/consent/scope-labels.ts
|
|
289
322
|
var SERVICE_DISPLAY_NAMES = {
|
|
290
323
|
gmail: "Gmail",
|
|
@@ -1876,37 +1909,215 @@ function centsToDollars(cents) {
|
|
|
1876
1909
|
})}`;
|
|
1877
1910
|
}
|
|
1878
1911
|
|
|
1879
|
-
// src/
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1912
|
+
// src/openclaw/shield-client.ts
|
|
1913
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
1914
|
+
var AUTH_HEADER = "X-Multicorn-Key";
|
|
1915
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1916
|
+
var MAX_POLLS = 100;
|
|
1917
|
+
var POLL_TIMEOUT_MS = POLL_INTERVAL_MS * MAX_POLLS;
|
|
1918
|
+
var authErrorLogged = false;
|
|
1919
|
+
function isApiSuccess(value) {
|
|
1920
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1921
|
+
const obj = value;
|
|
1922
|
+
return obj["success"] === true;
|
|
1923
|
+
}
|
|
1924
|
+
function isContentReviewStatusResponse(value) {
|
|
1925
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1926
|
+
const obj = value;
|
|
1927
|
+
return typeof obj["id"] === "string" && typeof obj["status"] === "string" && ["pending", "approved", "blocked", "timeout"].includes(obj["status"]);
|
|
1928
|
+
}
|
|
1929
|
+
function readApiErrorCode(body) {
|
|
1930
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
1931
|
+
const err = body["error"];
|
|
1932
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
1933
|
+
const code = err["code"];
|
|
1934
|
+
return typeof code === "string" ? code : void 0;
|
|
1935
|
+
}
|
|
1936
|
+
function isPlanTierInsufficientError(status, body) {
|
|
1937
|
+
return status === 403 && readApiErrorCode(body) === "PLAN_TIER_INSUFFICIENT";
|
|
1938
|
+
}
|
|
1939
|
+
function handleHttpError(status, logger, retryDelaySeconds) {
|
|
1940
|
+
if (status === 401 || status === 403) {
|
|
1941
|
+
if (!authErrorLogged) {
|
|
1942
|
+
authErrorLogged = true;
|
|
1943
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in your OpenClaw config (~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.env.MULTICORN_API_KEY). Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
1944
|
+
logger?.error(errorMsg);
|
|
1945
|
+
process.stderr.write(`${errorMsg}
|
|
1946
|
+
`);
|
|
1947
|
+
}
|
|
1948
|
+
return { shouldBlock: true };
|
|
1949
|
+
}
|
|
1950
|
+
if (status === 429) {
|
|
1951
|
+
if (retryDelaySeconds !== void 0) {
|
|
1952
|
+
const rateLimitMsg = `[multicorn-shield] Rate limited by Shield API. Retrying in ${String(retryDelaySeconds)}s.`;
|
|
1953
|
+
logger?.warn(rateLimitMsg);
|
|
1954
|
+
process.stderr.write(`${rateLimitMsg}
|
|
1955
|
+
`);
|
|
1956
|
+
} else {
|
|
1957
|
+
const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action blocked: Shield cannot verify permissions.";
|
|
1958
|
+
logger?.warn(rateLimitMsg);
|
|
1959
|
+
process.stderr.write(`${rateLimitMsg}
|
|
1960
|
+
`);
|
|
1961
|
+
}
|
|
1962
|
+
return { shouldBlock: true };
|
|
1883
1963
|
}
|
|
1884
|
-
if (
|
|
1885
|
-
|
|
1964
|
+
if (status >= 500 && status < 600) {
|
|
1965
|
+
const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action blocked: Shield cannot verify permissions.`;
|
|
1966
|
+
logger?.warn(serverErrorMsg);
|
|
1967
|
+
process.stderr.write(`${serverErrorMsg}
|
|
1968
|
+
`);
|
|
1969
|
+
return { shouldBlock: true };
|
|
1886
1970
|
}
|
|
1887
|
-
return
|
|
1971
|
+
return { shouldBlock: true };
|
|
1888
1972
|
}
|
|
1889
|
-
function
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1973
|
+
async function pollContentReviewStatus(reviewId, apiKey, baseUrl, logger) {
|
|
1974
|
+
const startTime = Date.now();
|
|
1975
|
+
const logDebug = logger?.debug?.bind(logger);
|
|
1976
|
+
for (let pollCount = 0; pollCount < MAX_POLLS; pollCount++) {
|
|
1977
|
+
if (Date.now() - startTime >= POLL_TIMEOUT_MS) {
|
|
1978
|
+
return { status: "timeout", reason: "decision_window_exceeded", reviewId };
|
|
1979
|
+
}
|
|
1980
|
+
let row = null;
|
|
1981
|
+
for (let retry = 0; retry < 3; retry++) {
|
|
1982
|
+
try {
|
|
1983
|
+
const response = await fetch(`${baseUrl}/api/v1/content-reviews/${reviewId}/status`, {
|
|
1984
|
+
headers: { [AUTH_HEADER]: apiKey },
|
|
1985
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1986
|
+
});
|
|
1987
|
+
if (!response.ok) {
|
|
1988
|
+
if (response.status === 404) {
|
|
1989
|
+
return { status: "blocked", reason: "review_not_found", reviewId };
|
|
1990
|
+
}
|
|
1991
|
+
const errBody = await response.json().catch(() => null);
|
|
1992
|
+
if (isPlanTierInsufficientError(response.status, errBody)) {
|
|
1993
|
+
return { status: "blocked", reason: "plan_tier_insufficient", reviewId };
|
|
1994
|
+
}
|
|
1995
|
+
if (response.status === 401 || response.status === 403) {
|
|
1996
|
+
handleHttpError(response.status, logger);
|
|
1997
|
+
return { status: "blocked", reason: "auth_error", reviewId };
|
|
1998
|
+
}
|
|
1999
|
+
if (response.status === 429 || response.status >= 500 && response.status < 600) {
|
|
2000
|
+
const retryDelay = retry < 2 ? Math.pow(2, retry) : void 0;
|
|
2001
|
+
handleHttpError(response.status, logger, retryDelay);
|
|
2002
|
+
}
|
|
2003
|
+
logDebug?.(
|
|
2004
|
+
`Content review poll ${String(pollCount + 1)} failed: HTTP ${String(response.status)}. Retrying...`
|
|
2005
|
+
);
|
|
2006
|
+
if (retry < 2) {
|
|
2007
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2008
|
+
}
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
const body = await response.json();
|
|
2012
|
+
if (!isApiSuccess(body)) {
|
|
2013
|
+
logDebug?.(
|
|
2014
|
+
`Content review poll ${String(pollCount + 1)} failed: invalid response format. Retrying...`
|
|
2015
|
+
);
|
|
2016
|
+
if (retry < 2) {
|
|
2017
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2018
|
+
}
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
const statusData = body.data;
|
|
2022
|
+
if (!isContentReviewStatusResponse(statusData)) {
|
|
2023
|
+
logDebug?.(
|
|
2024
|
+
`Content review poll ${String(pollCount + 1)} failed: invalid status data. Retrying...`
|
|
2025
|
+
);
|
|
2026
|
+
if (retry < 2) {
|
|
2027
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2028
|
+
}
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
row = statusData;
|
|
2032
|
+
break;
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2035
|
+
logDebug?.(
|
|
2036
|
+
`Content review poll ${String(pollCount + 1)} failed: ${errorMessage}. Retrying...`
|
|
2037
|
+
);
|
|
2038
|
+
if (retry < 2) {
|
|
2039
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (row !== null) {
|
|
2044
|
+
if (row.status === "approved") {
|
|
2045
|
+
return { status: "approved", reviewId };
|
|
2046
|
+
}
|
|
2047
|
+
if (row.status === "blocked") {
|
|
2048
|
+
return { status: "blocked", reason: "blocked_by_reviewer", reviewId };
|
|
2049
|
+
}
|
|
2050
|
+
if (row.status === "timeout") {
|
|
2051
|
+
return { status: "timeout", reason: "decision_window_exceeded", reviewId };
|
|
2052
|
+
}
|
|
2053
|
+
if (pollCount < MAX_POLLS - 1) {
|
|
2054
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2055
|
+
}
|
|
2056
|
+
} else {
|
|
2057
|
+
logDebug?.(
|
|
2058
|
+
`All retries failed for content review poll ${String(pollCount + 1)}. Continuing...`
|
|
2059
|
+
);
|
|
2060
|
+
if (pollCount < MAX_POLLS - 1) {
|
|
2061
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return { status: "timeout", reason: "decision_window_exceeded", reviewId };
|
|
2066
|
+
}
|
|
2067
|
+
async function requestContentReview(payload, apiKey, baseUrl, logger) {
|
|
2068
|
+
try {
|
|
2069
|
+
const response = await fetch(`${baseUrl}/api/v1/actions`, {
|
|
2070
|
+
method: "POST",
|
|
2071
|
+
headers: {
|
|
2072
|
+
"Content-Type": "application/json",
|
|
2073
|
+
[AUTH_HEADER]: apiKey
|
|
2074
|
+
},
|
|
2075
|
+
body: JSON.stringify({
|
|
2076
|
+
agent: payload.agent,
|
|
2077
|
+
service: payload.service,
|
|
2078
|
+
actionType: payload.actionType,
|
|
2079
|
+
status: "requires_approval",
|
|
2080
|
+
cost: payload.cost ?? 0,
|
|
2081
|
+
metadata: payload.metadata
|
|
2082
|
+
}),
|
|
2083
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
2084
|
+
});
|
|
2085
|
+
const body = await response.json().catch(() => null);
|
|
2086
|
+
if (isPlanTierInsufficientError(response.status, body)) {
|
|
2087
|
+
return { status: "blocked", reason: "plan_tier_insufficient" };
|
|
2088
|
+
}
|
|
2089
|
+
if (response.status === 401 || response.status === 403) {
|
|
2090
|
+
handleHttpError(response.status, logger);
|
|
2091
|
+
return { status: "blocked", reason: "auth_error" };
|
|
2092
|
+
}
|
|
2093
|
+
if (response.status === 429 || response.status >= 500 && response.status < 600) {
|
|
2094
|
+
handleHttpError(response.status, logger);
|
|
2095
|
+
return { status: "blocked", reason: "service_unavailable" };
|
|
2096
|
+
}
|
|
2097
|
+
if (response.status === 202) {
|
|
2098
|
+
if (!isApiSuccess(body)) {
|
|
2099
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2100
|
+
}
|
|
2101
|
+
const data = body.data;
|
|
2102
|
+
if (typeof data !== "object" || data === null) {
|
|
2103
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2104
|
+
}
|
|
2105
|
+
const record = data;
|
|
2106
|
+
const rid = record["content_review_id"];
|
|
2107
|
+
const reviewId = typeof rid === "string" ? rid : void 0;
|
|
2108
|
+
if (reviewId === void 0) {
|
|
2109
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2110
|
+
}
|
|
2111
|
+
const polled = await pollContentReviewStatus(reviewId, apiKey, baseUrl, logger);
|
|
2112
|
+
return { ...polled, reviewId };
|
|
2113
|
+
}
|
|
2114
|
+
if (response.status === 201) {
|
|
2115
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2116
|
+
}
|
|
2117
|
+
return { status: "blocked", reason: "service_unavailable" };
|
|
2118
|
+
} catch {
|
|
2119
|
+
return { status: "blocked", reason: "network_error" };
|
|
1894
2120
|
}
|
|
1895
|
-
const publicContentIndicators = [
|
|
1896
|
-
"publish",
|
|
1897
|
-
"public",
|
|
1898
|
-
"web",
|
|
1899
|
-
"blog",
|
|
1900
|
-
"post",
|
|
1901
|
-
"article",
|
|
1902
|
-
"social",
|
|
1903
|
-
"twitter",
|
|
1904
|
-
"facebook",
|
|
1905
|
-
"linkedin",
|
|
1906
|
-
"github_pages",
|
|
1907
|
-
"deploy"
|
|
1908
|
-
];
|
|
1909
|
-
return publicContentIndicators.some((indicator) => lowerToolName.includes(indicator));
|
|
1910
2121
|
}
|
|
1911
2122
|
|
|
1912
2123
|
// src/mcp/mcp-adapter.ts
|
|
@@ -1983,6 +2194,28 @@ function createMcpAdapter(config) {
|
|
|
1983
2194
|
status
|
|
1984
2195
|
});
|
|
1985
2196
|
}
|
|
2197
|
+
function mapContentReviewReasonToUserMessage(reason) {
|
|
2198
|
+
switch (reason) {
|
|
2199
|
+
case "plan_tier_insufficient":
|
|
2200
|
+
return "Content review requires an Enterprise plan. Upgrade at app.multicorn.ai/settings.";
|
|
2201
|
+
case "review_not_found":
|
|
2202
|
+
return "Content review no longer exists. It may have been deleted or timed out.";
|
|
2203
|
+
case "decision_window_exceeded":
|
|
2204
|
+
return "Content review timed out before a decision was made.";
|
|
2205
|
+
case "blocked_by_reviewer":
|
|
2206
|
+
return "Content review was blocked by a reviewer.";
|
|
2207
|
+
case "auth_error":
|
|
2208
|
+
return "Content review failed: please verify your Multicorn API key.";
|
|
2209
|
+
case "service_unavailable":
|
|
2210
|
+
return "Content review failed: service unavailable.";
|
|
2211
|
+
case "network_error":
|
|
2212
|
+
return "Content review failed: network error.";
|
|
2213
|
+
case "no_review_id":
|
|
2214
|
+
return "Content review failed: could not start review.";
|
|
2215
|
+
default:
|
|
2216
|
+
return "Content review failed.";
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
1986
2219
|
async function checkAutoApproveStatus() {
|
|
1987
2220
|
if (config.checkAutoApprove !== void 0) {
|
|
1988
2221
|
const result = config.checkAutoApprove(config.agentId);
|
|
@@ -2052,6 +2285,38 @@ function createMcpAdapter(config) {
|
|
|
2052
2285
|
arguments: JSON.stringify(toolCall.arguments),
|
|
2053
2286
|
requiresReview: true
|
|
2054
2287
|
};
|
|
2288
|
+
if (config.waitForReviewDecision === true) {
|
|
2289
|
+
if (config.baseUrl === void 0 || config.apiKey === void 0) {
|
|
2290
|
+
return {
|
|
2291
|
+
blocked: true,
|
|
2292
|
+
reason: "waitForReviewDecision requires baseUrl and apiKey to poll the content review.",
|
|
2293
|
+
toolName: toolCall.toolName,
|
|
2294
|
+
service: mappedService,
|
|
2295
|
+
action
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const review = await requestContentReview(
|
|
2299
|
+
{
|
|
2300
|
+
agent: config.agentId,
|
|
2301
|
+
service: mappedService,
|
|
2302
|
+
actionType: action,
|
|
2303
|
+
metadata
|
|
2304
|
+
},
|
|
2305
|
+
config.apiKey,
|
|
2306
|
+
config.baseUrl
|
|
2307
|
+
);
|
|
2308
|
+
if (review.status === "approved") {
|
|
2309
|
+
await recordAction(mappedService, action, ACTION_STATUSES.Approved);
|
|
2310
|
+
return handler(toolCall);
|
|
2311
|
+
}
|
|
2312
|
+
return {
|
|
2313
|
+
blocked: true,
|
|
2314
|
+
reason: mapContentReviewReasonToUserMessage(review.reason),
|
|
2315
|
+
toolName: toolCall.toolName,
|
|
2316
|
+
service: mappedService,
|
|
2317
|
+
action
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2055
2320
|
if (config.logger) {
|
|
2056
2321
|
await config.logger.logAction({
|
|
2057
2322
|
agent: config.agentId,
|
|
@@ -2478,4 +2743,4 @@ function validateApiKey(apiKey) {
|
|
|
2478
2743
|
}
|
|
2479
2744
|
}
|
|
2480
2745
|
|
|
2481
|
-
export { ACTION_STATUSES, AGENT_STATUSES, BUILT_IN_SERVICES, CONSENT_ELEMENT_TAG, MulticornConsent, MulticornShield, PERMISSION_LEVELS, SERVICE_NAME_PATTERN, ScopeParseError, centsToDollars, createActionLogger, createFocusTrap, createMcpAdapter, createScopeRegistry, createSpendingChecker, dollarsToCents, formatScope, getPermissionLabel, getScopeLabel, getScopeShortLabel, getServiceDisplayName, getServiceIcon, hasScope, isBlockedResult, isValidScopeString, parseScope, parseScopes, tryParseScope, validateAllScopesAccess, validateScopeAccess };
|
|
2746
|
+
export { ACTION_STATUSES, AGENT_STATUSES, BUILT_IN_SERVICES, CONSENT_ELEMENT_TAG, MulticornConsent, MulticornShield, PERMISSION_LEVELS, SERVICE_NAME_PATTERN, ScopeParseError, centsToDollars, createActionLogger, createFocusTrap, createMcpAdapter, createScopeRegistry, createSpendingChecker, dollarsToCents, formatScope, getPermissionLabel, getScopeLabel, getScopeShortLabel, getServiceDisplayName, getServiceIcon, hasScope, isBlockedResult, isPublicContentAction, isValidScopeString, parseScope, parseScopes, requestContentReview, requiresContentReview, tryParseScope, validateAllScopesAccess, validateScopeAccess };
|
package/dist/multicorn-proxy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { mkdir, writeFile, readFile, unlink } from 'fs/promises';
|
|
4
|
-
import { join } from 'path';
|
|
3
|
+
import { mkdir, writeFile, readFile, copyFile, unlink } from 'fs/promises';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
6
7
|
import { createInterface } from 'readline';
|
|
7
8
|
import { spawn } from 'child_process';
|
|
8
9
|
import { createHash } from 'crypto';
|
|
@@ -386,6 +387,92 @@ async function isWindsurfConnected() {
|
|
|
386
387
|
return false;
|
|
387
388
|
}
|
|
388
389
|
}
|
|
390
|
+
function multicornShieldPackageRoot() {
|
|
391
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
392
|
+
}
|
|
393
|
+
function getWindsurfHooksInstallDir() {
|
|
394
|
+
return join(homedir(), ".multicorn", "windsurf-hooks");
|
|
395
|
+
}
|
|
396
|
+
function getWindsurfCascadeHooksJsonPath() {
|
|
397
|
+
return join(homedir(), ".codeium", "windsurf", "hooks.json");
|
|
398
|
+
}
|
|
399
|
+
function isShieldWindsurfHookCommand(cmd) {
|
|
400
|
+
return cmd.includes("windsurf-hooks/pre-action.cjs") || cmd.includes("windsurf-hooks\\pre-action.cjs") || cmd.includes("windsurf-hooks/post-action.cjs") || cmd.includes("windsurf-hooks\\post-action.cjs");
|
|
401
|
+
}
|
|
402
|
+
function filterOutShieldWindsurfHooks(entries) {
|
|
403
|
+
if (!Array.isArray(entries)) return [];
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const e of entries) {
|
|
406
|
+
if (typeof e !== "object" || e === null) continue;
|
|
407
|
+
const rec = e;
|
|
408
|
+
const cmd = rec["command"];
|
|
409
|
+
if (typeof cmd !== "string" || isShieldWindsurfHookCommand(cmd)) continue;
|
|
410
|
+
const powershell = rec["powershell"];
|
|
411
|
+
const show_output = rec["show_output"];
|
|
412
|
+
out.push({
|
|
413
|
+
command: cmd,
|
|
414
|
+
...typeof powershell === "string" ? { powershell } : {},
|
|
415
|
+
...show_output === true ? { show_output: true } : {}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
async function installWindsurfNativeHooks() {
|
|
421
|
+
const root = multicornShieldPackageRoot();
|
|
422
|
+
const srcPre = join(root, "plugins", "windsurf", "hooks", "scripts", "pre-action.cjs");
|
|
423
|
+
const srcPost = join(root, "plugins", "windsurf", "hooks", "scripts", "post-action.cjs");
|
|
424
|
+
if (!existsSync(srcPre) || !existsSync(srcPost)) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const installDir = getWindsurfHooksInstallDir();
|
|
430
|
+
await mkdir(installDir, { recursive: true });
|
|
431
|
+
const destPre = join(installDir, "pre-action.cjs");
|
|
432
|
+
const destPost = join(installDir, "post-action.cjs");
|
|
433
|
+
await copyFile(srcPre, destPre);
|
|
434
|
+
await copyFile(srcPost, destPost);
|
|
435
|
+
const preCmd = `node ${JSON.stringify(destPre)}`;
|
|
436
|
+
const postCmd = `node ${JSON.stringify(destPost)}`;
|
|
437
|
+
const preEntry = { command: preCmd, powershell: preCmd, show_output: true };
|
|
438
|
+
const postEntry = { command: postCmd, powershell: postCmd };
|
|
439
|
+
const hooksPath = getWindsurfCascadeHooksJsonPath();
|
|
440
|
+
let base = { hooks: {} };
|
|
441
|
+
try {
|
|
442
|
+
const raw = await readFile(hooksPath, "utf8");
|
|
443
|
+
base = JSON.parse(raw);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
if (!isErrnoException(err) || err.code !== "ENOENT") {
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const hooks = base["hooks"] ?? {};
|
|
450
|
+
const preKeys = [
|
|
451
|
+
"pre_read_code",
|
|
452
|
+
"pre_write_code",
|
|
453
|
+
"pre_run_command",
|
|
454
|
+
"pre_mcp_tool_use"
|
|
455
|
+
];
|
|
456
|
+
const postKeys = [
|
|
457
|
+
"post_read_code",
|
|
458
|
+
"post_write_code",
|
|
459
|
+
"post_run_command",
|
|
460
|
+
"post_mcp_tool_use"
|
|
461
|
+
];
|
|
462
|
+
const nextHooks = { ...hooks };
|
|
463
|
+
for (const k of preKeys) {
|
|
464
|
+
const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
|
|
465
|
+
nextHooks[k] = [...merged, preEntry];
|
|
466
|
+
}
|
|
467
|
+
for (const k of postKeys) {
|
|
468
|
+
const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
|
|
469
|
+
nextHooks[k] = [...merged, postEntry];
|
|
470
|
+
}
|
|
471
|
+
base["hooks"] = nextHooks;
|
|
472
|
+
const hooksDir = dirname(hooksPath);
|
|
473
|
+
await mkdir(hooksDir, { recursive: true });
|
|
474
|
+
await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
|
|
475
|
+
}
|
|
389
476
|
var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor", "Windsurf", "Local MCP / Other"];
|
|
390
477
|
var PLATFORM_BY_SELECTION = {
|
|
391
478
|
1: "openclaw",
|
|
@@ -430,6 +517,23 @@ async function promptPlatformSelection(ask) {
|
|
|
430
517
|
}
|
|
431
518
|
return selection;
|
|
432
519
|
}
|
|
520
|
+
async function promptWindsurfIntegrationMode(ask) {
|
|
521
|
+
process.stderr.write("\n" + style.bold("Windsurf integration") + "\n");
|
|
522
|
+
process.stderr.write(
|
|
523
|
+
" " + style.violet("1") + ". Native plugin (recommended) \u2014 Cascade Hooks see every file, terminal, and MCP action\n"
|
|
524
|
+
);
|
|
525
|
+
process.stderr.write(
|
|
526
|
+
" " + style.violet("2") + ". Hosted proxy \u2014 govern MCP traffic only (paste proxy URL into mcp_config)\n"
|
|
527
|
+
);
|
|
528
|
+
let choice = 0;
|
|
529
|
+
while (choice === 0) {
|
|
530
|
+
const input = await ask("Choose integration (1-2): ");
|
|
531
|
+
const num = parseInt(input.trim(), 10);
|
|
532
|
+
if (num === 1) choice = 1;
|
|
533
|
+
if (num === 2) choice = 2;
|
|
534
|
+
}
|
|
535
|
+
return choice === 1 ? "native" : "hosted";
|
|
536
|
+
}
|
|
433
537
|
async function promptAgentName(ask, platform) {
|
|
434
538
|
const defaultAgentName = DEFAULT_AGENT_NAMES[platform] ?? "my-agent";
|
|
435
539
|
let agentName = "";
|
|
@@ -823,6 +927,80 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
823
927
|
agentName
|
|
824
928
|
});
|
|
825
929
|
setupSucceeded = true;
|
|
930
|
+
} else if (selection === 4) {
|
|
931
|
+
const windsurfMode = await promptWindsurfIntegrationMode(ask);
|
|
932
|
+
if (windsurfMode === "native") {
|
|
933
|
+
try {
|
|
934
|
+
await installWindsurfNativeHooks();
|
|
935
|
+
process.stderr.write("\n" + style.bold("Shield Windsurf hooks installed") + "\n");
|
|
936
|
+
process.stderr.write(
|
|
937
|
+
style.dim("Scripts: ") + style.cyan(getWindsurfHooksInstallDir()) + "\n"
|
|
938
|
+
);
|
|
939
|
+
process.stderr.write(
|
|
940
|
+
style.dim("Hooks config: ") + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n"
|
|
941
|
+
);
|
|
942
|
+
process.stderr.write(
|
|
943
|
+
"\n" + style.dim(
|
|
944
|
+
"The Shield hook runs with your user permissions. It intercepts Cascade actions to check permissions and log activity. Review the scripts under "
|
|
945
|
+
) + style.cyan("~/.multicorn/windsurf-hooks") + style.dim(" if that is a concern.") + "\n\n"
|
|
946
|
+
);
|
|
947
|
+
process.stderr.write(
|
|
948
|
+
style.dim("Restart Windsurf (quit fully, then reopen) so hooks load.") + "\n"
|
|
949
|
+
);
|
|
950
|
+
configuredAgents.push({
|
|
951
|
+
selection,
|
|
952
|
+
platform: selectedPlatform,
|
|
953
|
+
platformLabel: selectedLabel,
|
|
954
|
+
agentName,
|
|
955
|
+
windsurfIntegration: "native"
|
|
956
|
+
});
|
|
957
|
+
setupSucceeded = true;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
960
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
961
|
+
}
|
|
962
|
+
} else {
|
|
963
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
964
|
+
let proxyUrl = "";
|
|
965
|
+
let created = false;
|
|
966
|
+
while (!created) {
|
|
967
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
968
|
+
try {
|
|
969
|
+
proxyUrl = await createProxyConfig(
|
|
970
|
+
resolvedBaseUrl,
|
|
971
|
+
apiKey,
|
|
972
|
+
agentName,
|
|
973
|
+
targetUrl,
|
|
974
|
+
shortName,
|
|
975
|
+
selectedPlatform
|
|
976
|
+
);
|
|
977
|
+
spinner.stop(true, "Proxy config created!");
|
|
978
|
+
created = true;
|
|
979
|
+
} catch (error) {
|
|
980
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
981
|
+
spinner.stop(false, detail);
|
|
982
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
983
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (created && proxyUrl.length > 0) {
|
|
989
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
990
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
991
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
|
|
992
|
+
configuredAgents.push({
|
|
993
|
+
selection,
|
|
994
|
+
platform: selectedPlatform,
|
|
995
|
+
platformLabel: selectedLabel,
|
|
996
|
+
agentName,
|
|
997
|
+
shortName,
|
|
998
|
+
proxyUrl,
|
|
999
|
+
windsurfIntegration: "hosted"
|
|
1000
|
+
});
|
|
1001
|
+
setupSucceeded = true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
826
1004
|
} else {
|
|
827
1005
|
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
828
1006
|
let proxyUrl = "";
|
|
@@ -925,14 +1103,20 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
925
1103
|
"\n" + style.bold("To complete your Cursor setup:") + "\n 1. If you don't have Cursor yet, download it from " + style.cyan("https://cursor.com/downloads") + "\n 2. Open " + style.cyan("~/.cursor/mcp.json") + " and paste the config snippet shown above\n 3. Restart Cursor (or launch it for the first time) to load the new MCP server\n"
|
|
926
1104
|
);
|
|
927
1105
|
}
|
|
928
|
-
|
|
1106
|
+
const windsurfNativeConfigured = configuredAgents.some(
|
|
1107
|
+
(a) => a.platform === "windsurf" && a.windsurfIntegration === "native"
|
|
1108
|
+
);
|
|
1109
|
+
const windsurfHostedConfigured = configuredAgents.some(
|
|
1110
|
+
(a) => a.platform === "windsurf" && a.windsurfIntegration === "hosted"
|
|
1111
|
+
);
|
|
1112
|
+
if (windsurfNativeConfigured) {
|
|
929
1113
|
blocks.push(
|
|
930
|
-
"\n" + style.bold("To complete
|
|
1114
|
+
"\n" + style.bold("To complete native Windsurf (Shield) setup:") + "\n 1. Hook scripts: " + style.cyan(getWindsurfHooksInstallDir()) + "\n 2. Hooks config: " + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n 3. Restart Windsurf (quit fully, then reopen)\n"
|
|
931
1115
|
);
|
|
932
1116
|
}
|
|
933
|
-
if (
|
|
1117
|
+
if (windsurfHostedConfigured) {
|
|
934
1118
|
blocks.push(
|
|
935
|
-
"\n" + style.bold("To complete your Windsurf setup:") + "\n
|
|
1119
|
+
"\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
|
|
936
1120
|
);
|
|
937
1121
|
}
|
|
938
1122
|
if (blocks.length > 0) {
|
package/dist/multicorn-shield.js
CHANGED
package/dist/shield-extension.js
CHANGED
|
@@ -7,6 +7,7 @@ import process3 from 'process';
|
|
|
7
7
|
import 'stream';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
|
+
import 'url';
|
|
10
11
|
import 'readline';
|
|
11
12
|
|
|
12
13
|
// Multicorn Shield Claude Desktop Extension - https://multicorn.ai
|
|
@@ -22358,7 +22359,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22358
22359
|
|
|
22359
22360
|
// package.json
|
|
22360
22361
|
var package_default = {
|
|
22361
|
-
version: "0.
|
|
22362
|
+
version: "0.10.0"};
|
|
22362
22363
|
|
|
22363
22364
|
// src/package-meta.ts
|
|
22364
22365
|
var PACKAGE_VERSION = package_default.version;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multicorn-shield",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "Multicorn AI Pty Ltd",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "./dist/index.cjs",
|
|
8
9
|
"module": "./dist/index.js",
|
|
@@ -35,9 +36,15 @@
|
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"dist",
|
|
39
|
+
"plugins/windsurf",
|
|
38
40
|
"LICENSE",
|
|
39
|
-
"README.md"
|
|
41
|
+
"README.md",
|
|
42
|
+
"CHANGELOG.md"
|
|
40
43
|
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"provenance": true
|
|
47
|
+
},
|
|
41
48
|
"sideEffects": false,
|
|
42
49
|
"engines": {
|
|
43
50
|
"node": ">=20"
|