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.cjs
CHANGED
|
@@ -287,6 +287,39 @@ function hasScope(grantedScopes, requested) {
|
|
|
287
287
|
);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// src/scopes/content-review-detector.ts
|
|
291
|
+
function requiresContentReview(scope) {
|
|
292
|
+
if (scope.service === "web" && scope.permissionLevel === "publish") {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (scope.service === "public_content" && scope.permissionLevel === "create") {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
function isPublicContentAction(toolName, service) {
|
|
301
|
+
const lowerToolName = toolName.toLowerCase();
|
|
302
|
+
const lowerService = service.toLowerCase();
|
|
303
|
+
if (lowerService === "web" || lowerService === "public_content") {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
const publicContentIndicators = [
|
|
307
|
+
"publish",
|
|
308
|
+
"public",
|
|
309
|
+
"web",
|
|
310
|
+
"blog",
|
|
311
|
+
"post",
|
|
312
|
+
"article",
|
|
313
|
+
"social",
|
|
314
|
+
"twitter",
|
|
315
|
+
"facebook",
|
|
316
|
+
"linkedin",
|
|
317
|
+
"github_pages",
|
|
318
|
+
"deploy"
|
|
319
|
+
];
|
|
320
|
+
return publicContentIndicators.some((indicator) => lowerToolName.includes(indicator));
|
|
321
|
+
}
|
|
322
|
+
|
|
290
323
|
// src/consent/scope-labels.ts
|
|
291
324
|
var SERVICE_DISPLAY_NAMES = {
|
|
292
325
|
gmail: "Gmail",
|
|
@@ -1878,37 +1911,215 @@ function centsToDollars(cents) {
|
|
|
1878
1911
|
})}`;
|
|
1879
1912
|
}
|
|
1880
1913
|
|
|
1881
|
-
// src/
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1914
|
+
// src/openclaw/shield-client.ts
|
|
1915
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
1916
|
+
var AUTH_HEADER = "X-Multicorn-Key";
|
|
1917
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1918
|
+
var MAX_POLLS = 100;
|
|
1919
|
+
var POLL_TIMEOUT_MS = POLL_INTERVAL_MS * MAX_POLLS;
|
|
1920
|
+
var authErrorLogged = false;
|
|
1921
|
+
function isApiSuccess(value) {
|
|
1922
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1923
|
+
const obj = value;
|
|
1924
|
+
return obj["success"] === true;
|
|
1925
|
+
}
|
|
1926
|
+
function isContentReviewStatusResponse(value) {
|
|
1927
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1928
|
+
const obj = value;
|
|
1929
|
+
return typeof obj["id"] === "string" && typeof obj["status"] === "string" && ["pending", "approved", "blocked", "timeout"].includes(obj["status"]);
|
|
1930
|
+
}
|
|
1931
|
+
function readApiErrorCode(body) {
|
|
1932
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
1933
|
+
const err = body["error"];
|
|
1934
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
1935
|
+
const code = err["code"];
|
|
1936
|
+
return typeof code === "string" ? code : void 0;
|
|
1937
|
+
}
|
|
1938
|
+
function isPlanTierInsufficientError(status, body) {
|
|
1939
|
+
return status === 403 && readApiErrorCode(body) === "PLAN_TIER_INSUFFICIENT";
|
|
1940
|
+
}
|
|
1941
|
+
function handleHttpError(status, logger, retryDelaySeconds) {
|
|
1942
|
+
if (status === 401 || status === 403) {
|
|
1943
|
+
if (!authErrorLogged) {
|
|
1944
|
+
authErrorLogged = true;
|
|
1945
|
+
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).";
|
|
1946
|
+
logger?.error(errorMsg);
|
|
1947
|
+
process.stderr.write(`${errorMsg}
|
|
1948
|
+
`);
|
|
1949
|
+
}
|
|
1950
|
+
return { shouldBlock: true };
|
|
1951
|
+
}
|
|
1952
|
+
if (status === 429) {
|
|
1953
|
+
if (retryDelaySeconds !== void 0) {
|
|
1954
|
+
const rateLimitMsg = `[multicorn-shield] Rate limited by Shield API. Retrying in ${String(retryDelaySeconds)}s.`;
|
|
1955
|
+
logger?.warn(rateLimitMsg);
|
|
1956
|
+
process.stderr.write(`${rateLimitMsg}
|
|
1957
|
+
`);
|
|
1958
|
+
} else {
|
|
1959
|
+
const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action blocked: Shield cannot verify permissions.";
|
|
1960
|
+
logger?.warn(rateLimitMsg);
|
|
1961
|
+
process.stderr.write(`${rateLimitMsg}
|
|
1962
|
+
`);
|
|
1963
|
+
}
|
|
1964
|
+
return { shouldBlock: true };
|
|
1885
1965
|
}
|
|
1886
|
-
if (
|
|
1887
|
-
|
|
1966
|
+
if (status >= 500 && status < 600) {
|
|
1967
|
+
const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action blocked: Shield cannot verify permissions.`;
|
|
1968
|
+
logger?.warn(serverErrorMsg);
|
|
1969
|
+
process.stderr.write(`${serverErrorMsg}
|
|
1970
|
+
`);
|
|
1971
|
+
return { shouldBlock: true };
|
|
1888
1972
|
}
|
|
1889
|
-
return
|
|
1973
|
+
return { shouldBlock: true };
|
|
1890
1974
|
}
|
|
1891
|
-
function
|
|
1892
|
-
const
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1975
|
+
async function pollContentReviewStatus(reviewId, apiKey, baseUrl, logger) {
|
|
1976
|
+
const startTime = Date.now();
|
|
1977
|
+
const logDebug = logger?.debug?.bind(logger);
|
|
1978
|
+
for (let pollCount = 0; pollCount < MAX_POLLS; pollCount++) {
|
|
1979
|
+
if (Date.now() - startTime >= POLL_TIMEOUT_MS) {
|
|
1980
|
+
return { status: "timeout", reason: "decision_window_exceeded", reviewId };
|
|
1981
|
+
}
|
|
1982
|
+
let row = null;
|
|
1983
|
+
for (let retry = 0; retry < 3; retry++) {
|
|
1984
|
+
try {
|
|
1985
|
+
const response = await fetch(`${baseUrl}/api/v1/content-reviews/${reviewId}/status`, {
|
|
1986
|
+
headers: { [AUTH_HEADER]: apiKey },
|
|
1987
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1988
|
+
});
|
|
1989
|
+
if (!response.ok) {
|
|
1990
|
+
if (response.status === 404) {
|
|
1991
|
+
return { status: "blocked", reason: "review_not_found", reviewId };
|
|
1992
|
+
}
|
|
1993
|
+
const errBody = await response.json().catch(() => null);
|
|
1994
|
+
if (isPlanTierInsufficientError(response.status, errBody)) {
|
|
1995
|
+
return { status: "blocked", reason: "plan_tier_insufficient", reviewId };
|
|
1996
|
+
}
|
|
1997
|
+
if (response.status === 401 || response.status === 403) {
|
|
1998
|
+
handleHttpError(response.status, logger);
|
|
1999
|
+
return { status: "blocked", reason: "auth_error", reviewId };
|
|
2000
|
+
}
|
|
2001
|
+
if (response.status === 429 || response.status >= 500 && response.status < 600) {
|
|
2002
|
+
const retryDelay = retry < 2 ? Math.pow(2, retry) : void 0;
|
|
2003
|
+
handleHttpError(response.status, logger, retryDelay);
|
|
2004
|
+
}
|
|
2005
|
+
logDebug?.(
|
|
2006
|
+
`Content review poll ${String(pollCount + 1)} failed: HTTP ${String(response.status)}. Retrying...`
|
|
2007
|
+
);
|
|
2008
|
+
if (retry < 2) {
|
|
2009
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2010
|
+
}
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
const body = await response.json();
|
|
2014
|
+
if (!isApiSuccess(body)) {
|
|
2015
|
+
logDebug?.(
|
|
2016
|
+
`Content review poll ${String(pollCount + 1)} failed: invalid response format. Retrying...`
|
|
2017
|
+
);
|
|
2018
|
+
if (retry < 2) {
|
|
2019
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2020
|
+
}
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
const statusData = body.data;
|
|
2024
|
+
if (!isContentReviewStatusResponse(statusData)) {
|
|
2025
|
+
logDebug?.(
|
|
2026
|
+
`Content review poll ${String(pollCount + 1)} failed: invalid status data. Retrying...`
|
|
2027
|
+
);
|
|
2028
|
+
if (retry < 2) {
|
|
2029
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2030
|
+
}
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
row = statusData;
|
|
2034
|
+
break;
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2037
|
+
logDebug?.(
|
|
2038
|
+
`Content review poll ${String(pollCount + 1)} failed: ${errorMessage}. Retrying...`
|
|
2039
|
+
);
|
|
2040
|
+
if (retry < 2) {
|
|
2041
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * Math.pow(2, retry)));
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (row !== null) {
|
|
2046
|
+
if (row.status === "approved") {
|
|
2047
|
+
return { status: "approved", reviewId };
|
|
2048
|
+
}
|
|
2049
|
+
if (row.status === "blocked") {
|
|
2050
|
+
return { status: "blocked", reason: "blocked_by_reviewer", reviewId };
|
|
2051
|
+
}
|
|
2052
|
+
if (row.status === "timeout") {
|
|
2053
|
+
return { status: "timeout", reason: "decision_window_exceeded", reviewId };
|
|
2054
|
+
}
|
|
2055
|
+
if (pollCount < MAX_POLLS - 1) {
|
|
2056
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2057
|
+
}
|
|
2058
|
+
} else {
|
|
2059
|
+
logDebug?.(
|
|
2060
|
+
`All retries failed for content review poll ${String(pollCount + 1)}. Continuing...`
|
|
2061
|
+
);
|
|
2062
|
+
if (pollCount < MAX_POLLS - 1) {
|
|
2063
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return { status: "timeout", reason: "decision_window_exceeded", reviewId };
|
|
2068
|
+
}
|
|
2069
|
+
async function requestContentReview(payload, apiKey, baseUrl, logger) {
|
|
2070
|
+
try {
|
|
2071
|
+
const response = await fetch(`${baseUrl}/api/v1/actions`, {
|
|
2072
|
+
method: "POST",
|
|
2073
|
+
headers: {
|
|
2074
|
+
"Content-Type": "application/json",
|
|
2075
|
+
[AUTH_HEADER]: apiKey
|
|
2076
|
+
},
|
|
2077
|
+
body: JSON.stringify({
|
|
2078
|
+
agent: payload.agent,
|
|
2079
|
+
service: payload.service,
|
|
2080
|
+
actionType: payload.actionType,
|
|
2081
|
+
status: "requires_approval",
|
|
2082
|
+
cost: payload.cost ?? 0,
|
|
2083
|
+
metadata: payload.metadata
|
|
2084
|
+
}),
|
|
2085
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
2086
|
+
});
|
|
2087
|
+
const body = await response.json().catch(() => null);
|
|
2088
|
+
if (isPlanTierInsufficientError(response.status, body)) {
|
|
2089
|
+
return { status: "blocked", reason: "plan_tier_insufficient" };
|
|
2090
|
+
}
|
|
2091
|
+
if (response.status === 401 || response.status === 403) {
|
|
2092
|
+
handleHttpError(response.status, logger);
|
|
2093
|
+
return { status: "blocked", reason: "auth_error" };
|
|
2094
|
+
}
|
|
2095
|
+
if (response.status === 429 || response.status >= 500 && response.status < 600) {
|
|
2096
|
+
handleHttpError(response.status, logger);
|
|
2097
|
+
return { status: "blocked", reason: "service_unavailable" };
|
|
2098
|
+
}
|
|
2099
|
+
if (response.status === 202) {
|
|
2100
|
+
if (!isApiSuccess(body)) {
|
|
2101
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2102
|
+
}
|
|
2103
|
+
const data = body.data;
|
|
2104
|
+
if (typeof data !== "object" || data === null) {
|
|
2105
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2106
|
+
}
|
|
2107
|
+
const record = data;
|
|
2108
|
+
const rid = record["content_review_id"];
|
|
2109
|
+
const reviewId = typeof rid === "string" ? rid : void 0;
|
|
2110
|
+
if (reviewId === void 0) {
|
|
2111
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2112
|
+
}
|
|
2113
|
+
const polled = await pollContentReviewStatus(reviewId, apiKey, baseUrl, logger);
|
|
2114
|
+
return { ...polled, reviewId };
|
|
2115
|
+
}
|
|
2116
|
+
if (response.status === 201) {
|
|
2117
|
+
return { status: "blocked", reason: "no_review_id" };
|
|
2118
|
+
}
|
|
2119
|
+
return { status: "blocked", reason: "service_unavailable" };
|
|
2120
|
+
} catch {
|
|
2121
|
+
return { status: "blocked", reason: "network_error" };
|
|
1896
2122
|
}
|
|
1897
|
-
const publicContentIndicators = [
|
|
1898
|
-
"publish",
|
|
1899
|
-
"public",
|
|
1900
|
-
"web",
|
|
1901
|
-
"blog",
|
|
1902
|
-
"post",
|
|
1903
|
-
"article",
|
|
1904
|
-
"social",
|
|
1905
|
-
"twitter",
|
|
1906
|
-
"facebook",
|
|
1907
|
-
"linkedin",
|
|
1908
|
-
"github_pages",
|
|
1909
|
-
"deploy"
|
|
1910
|
-
];
|
|
1911
|
-
return publicContentIndicators.some((indicator) => lowerToolName.includes(indicator));
|
|
1912
2123
|
}
|
|
1913
2124
|
|
|
1914
2125
|
// src/mcp/mcp-adapter.ts
|
|
@@ -1985,6 +2196,28 @@ function createMcpAdapter(config) {
|
|
|
1985
2196
|
status
|
|
1986
2197
|
});
|
|
1987
2198
|
}
|
|
2199
|
+
function mapContentReviewReasonToUserMessage(reason) {
|
|
2200
|
+
switch (reason) {
|
|
2201
|
+
case "plan_tier_insufficient":
|
|
2202
|
+
return "Content review requires an Enterprise plan. Upgrade at app.multicorn.ai/settings.";
|
|
2203
|
+
case "review_not_found":
|
|
2204
|
+
return "Content review no longer exists. It may have been deleted or timed out.";
|
|
2205
|
+
case "decision_window_exceeded":
|
|
2206
|
+
return "Content review timed out before a decision was made.";
|
|
2207
|
+
case "blocked_by_reviewer":
|
|
2208
|
+
return "Content review was blocked by a reviewer.";
|
|
2209
|
+
case "auth_error":
|
|
2210
|
+
return "Content review failed: please verify your Multicorn API key.";
|
|
2211
|
+
case "service_unavailable":
|
|
2212
|
+
return "Content review failed: service unavailable.";
|
|
2213
|
+
case "network_error":
|
|
2214
|
+
return "Content review failed: network error.";
|
|
2215
|
+
case "no_review_id":
|
|
2216
|
+
return "Content review failed: could not start review.";
|
|
2217
|
+
default:
|
|
2218
|
+
return "Content review failed.";
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
1988
2221
|
async function checkAutoApproveStatus() {
|
|
1989
2222
|
if (config.checkAutoApprove !== void 0) {
|
|
1990
2223
|
const result = config.checkAutoApprove(config.agentId);
|
|
@@ -2054,6 +2287,38 @@ function createMcpAdapter(config) {
|
|
|
2054
2287
|
arguments: JSON.stringify(toolCall.arguments),
|
|
2055
2288
|
requiresReview: true
|
|
2056
2289
|
};
|
|
2290
|
+
if (config.waitForReviewDecision === true) {
|
|
2291
|
+
if (config.baseUrl === void 0 || config.apiKey === void 0) {
|
|
2292
|
+
return {
|
|
2293
|
+
blocked: true,
|
|
2294
|
+
reason: "waitForReviewDecision requires baseUrl and apiKey to poll the content review.",
|
|
2295
|
+
toolName: toolCall.toolName,
|
|
2296
|
+
service: mappedService,
|
|
2297
|
+
action
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
const review = await requestContentReview(
|
|
2301
|
+
{
|
|
2302
|
+
agent: config.agentId,
|
|
2303
|
+
service: mappedService,
|
|
2304
|
+
actionType: action,
|
|
2305
|
+
metadata
|
|
2306
|
+
},
|
|
2307
|
+
config.apiKey,
|
|
2308
|
+
config.baseUrl
|
|
2309
|
+
);
|
|
2310
|
+
if (review.status === "approved") {
|
|
2311
|
+
await recordAction(mappedService, action, ACTION_STATUSES.Approved);
|
|
2312
|
+
return handler(toolCall);
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
blocked: true,
|
|
2316
|
+
reason: mapContentReviewReasonToUserMessage(review.reason),
|
|
2317
|
+
toolName: toolCall.toolName,
|
|
2318
|
+
service: mappedService,
|
|
2319
|
+
action
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2057
2322
|
if (config.logger) {
|
|
2058
2323
|
await config.logger.logAction({
|
|
2059
2324
|
agent: config.agentId,
|
|
@@ -2503,9 +2768,12 @@ exports.getServiceDisplayName = getServiceDisplayName;
|
|
|
2503
2768
|
exports.getServiceIcon = getServiceIcon;
|
|
2504
2769
|
exports.hasScope = hasScope;
|
|
2505
2770
|
exports.isBlockedResult = isBlockedResult;
|
|
2771
|
+
exports.isPublicContentAction = isPublicContentAction;
|
|
2506
2772
|
exports.isValidScopeString = isValidScopeString;
|
|
2507
2773
|
exports.parseScope = parseScope;
|
|
2508
2774
|
exports.parseScopes = parseScopes;
|
|
2775
|
+
exports.requestContentReview = requestContentReview;
|
|
2776
|
+
exports.requiresContentReview = requiresContentReview;
|
|
2509
2777
|
exports.tryParseScope = tryParseScope;
|
|
2510
2778
|
exports.validateAllScopesAccess = validateAllScopesAccess;
|
|
2511
2779
|
exports.validateScopeAccess = validateScopeAccess;
|
package/dist/index.d.cts
CHANGED
|
@@ -652,6 +652,42 @@ declare function validateAllScopesAccess(grantedScopes: readonly Scope[], reques
|
|
|
652
652
|
*/
|
|
653
653
|
declare function hasScope(grantedScopes: readonly Scope[], requested: Scope): boolean;
|
|
654
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Detects if a scope requires content review before execution.
|
|
657
|
+
*
|
|
658
|
+
* Public content scopes that require review:
|
|
659
|
+
* - `publish:web` - publishing content to the web
|
|
660
|
+
* - `create:public_content` - creating public-facing content
|
|
661
|
+
*
|
|
662
|
+
* @module scopes/content-review-detector
|
|
663
|
+
*/
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Check if a scope requires content review.
|
|
667
|
+
*
|
|
668
|
+
* @param scope - The scope to check
|
|
669
|
+
* @returns `true` if the scope requires content review
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```ts
|
|
673
|
+
* requiresContentReview({ service: "web", permissionLevel: "publish" }); // true
|
|
674
|
+
* requiresContentReview({ service: "public_content", permissionLevel: "create" }); // true
|
|
675
|
+
* requiresContentReview({ service: "gmail", permissionLevel: "execute" }); // false
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
declare function requiresContentReview(scope: Scope): boolean;
|
|
679
|
+
/**
|
|
680
|
+
* Check if a tool name/action indicates public content creation.
|
|
681
|
+
*
|
|
682
|
+
* This is a helper for cases where the service might not be explicitly
|
|
683
|
+
* "web" or "public_content" but the action name indicates public content.
|
|
684
|
+
*
|
|
685
|
+
* @param toolName - The tool name to check
|
|
686
|
+
* @param service - The service name
|
|
687
|
+
* @returns `true` if the tool/action indicates public content
|
|
688
|
+
*/
|
|
689
|
+
declare function isPublicContentAction(toolName: string, service: string): boolean;
|
|
690
|
+
|
|
655
691
|
/**
|
|
656
692
|
* Custom element tag name for the consent component.
|
|
657
693
|
*/
|
|
@@ -1775,6 +1811,11 @@ interface McpAdapterConfig {
|
|
|
1775
1811
|
* Used with baseUrl to fetch auto-approval status if checkAutoApprove is not provided.
|
|
1776
1812
|
*/
|
|
1777
1813
|
readonly apiKey?: string;
|
|
1814
|
+
/**
|
|
1815
|
+
* When `true`, blocks until the content review completes (or times out) and forwards the tool call if approved.
|
|
1816
|
+
* Requires `baseUrl` and `apiKey`. Default `false` preserves the existing fast block with `requires_approval` logging only.
|
|
1817
|
+
*/
|
|
1818
|
+
readonly waitForReviewDecision?: boolean;
|
|
1778
1819
|
}
|
|
1779
1820
|
/**
|
|
1780
1821
|
* The MCP adapter produced by {@link createMcpAdapter}.
|
|
@@ -2179,4 +2220,67 @@ declare class MulticornShield {
|
|
|
2179
2220
|
destroy(): void;
|
|
2180
2221
|
}
|
|
2181
2222
|
|
|
2182
|
-
|
|
2223
|
+
/**
|
|
2224
|
+
* Local type definitions for the OpenClaw Plugin SDK.
|
|
2225
|
+
*
|
|
2226
|
+
* Extracted from openclaw/dist/plugin-sdk/plugins/types.d.ts (v2026.2.26).
|
|
2227
|
+
* We define only the subset needed for the Shield plugin so there's no
|
|
2228
|
+
* build-time dependency on the OpenClaw package itself.
|
|
2229
|
+
*
|
|
2230
|
+
* @module openclaw/plugin-sdk.types
|
|
2231
|
+
*/
|
|
2232
|
+
interface PluginLogger {
|
|
2233
|
+
debug?: (message: string) => void;
|
|
2234
|
+
info: (message: string) => void;
|
|
2235
|
+
warn: (message: string) => void;
|
|
2236
|
+
error: (message: string) => void;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* HTTP client for communicating with the Multicorn Shield API.
|
|
2241
|
+
*
|
|
2242
|
+
* Handles agent registration, permission fetching, and action logging.
|
|
2243
|
+
* Follows the same patterns as the MCP proxy client but is self-contained
|
|
2244
|
+
* so the hook has no runtime dependency on proxy internals.
|
|
2245
|
+
*
|
|
2246
|
+
* Security: the API key is passed as a parameter and sent only via the
|
|
2247
|
+
* `X-Multicorn-Key` header over HTTPS. It is never logged or written
|
|
2248
|
+
* to disk.
|
|
2249
|
+
*
|
|
2250
|
+
* @module openclaw/shield-client
|
|
2251
|
+
*/
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* `data` shape from GET /api/v1/content-reviews/:id/status when `success` is true.
|
|
2255
|
+
*/
|
|
2256
|
+
interface ContentReviewStatusResponse {
|
|
2257
|
+
readonly id: string;
|
|
2258
|
+
readonly status: "pending" | "approved" | "blocked" | "timeout";
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Result of {@link requestContentReview} or {@link pollContentReviewStatus}.
|
|
2262
|
+
*/
|
|
2263
|
+
interface ContentReviewResult {
|
|
2264
|
+
readonly status: "approved" | "blocked" | "timeout";
|
|
2265
|
+
readonly reviewId?: string;
|
|
2266
|
+
readonly reason?: string;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Payload for {@link requestContentReview}. `cost` is optional; the POST body always includes `cost`, defaulting to `0` when omitted (matches {@link LogActionRequest} in the service).
|
|
2270
|
+
*/
|
|
2271
|
+
interface ContentReviewRequestPayload {
|
|
2272
|
+
readonly agent: string;
|
|
2273
|
+
readonly service: string;
|
|
2274
|
+
readonly actionType: string;
|
|
2275
|
+
/** Defaults to `0` when omitted. Must be >= 0 if set. */
|
|
2276
|
+
readonly cost?: number;
|
|
2277
|
+
readonly metadata?: Readonly<Record<string, string | number | boolean>>;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Create a content-review request via POST /api/v1/actions with `status: "requires_approval"`, then poll until decided.
|
|
2281
|
+
*
|
|
2282
|
+
* Wire format: response `data.content_review_id` (snake_case) per service Jackson naming.
|
|
2283
|
+
*/
|
|
2284
|
+
declare function requestContentReview(payload: ContentReviewRequestPayload, apiKey: string, baseUrl: string, logger?: PluginLogger): Promise<ContentReviewResult>;
|
|
2285
|
+
|
|
2286
|
+
export { ACTION_STATUSES, AGENT_STATUSES, type Action, type ActionInput, type ActionLogger, type ActionLoggerConfig, type ActionPayload, type ActionStatus, type Agent, type AgentStatus, type ApiError, BUILT_IN_SERVICES, type BatchModeConfig, type BuiltInServiceName, CONSENT_ELEMENT_TAG, type ConsentDecision, type ConsentDeniedEventDetail, type ConsentEventDetail, type ConsentEventMap, type ConsentEventName, type ConsentGrantedEventDetail, type ConsentOptions, type ConsentPartialEventDetail, type ContentReviewRequestPayload, type ContentReviewResult, type ContentReviewStatusResponse, type FocusTrap, type McpAdapter, type McpAdapterConfig, type McpAdapterResult, type McpBlockedResult, type McpToolCall, type McpToolHandler, type McpToolResult, MulticornConsent, MulticornShield, type MulticornShieldConfig, PERMISSION_LEVELS, type Permission, type PermissionLevel, type RemainingBudget, SERVICE_NAME_PATTERN, type Scope, ScopeParseError, type ScopeParseResult, type ScopeRegistry, type ScopeRequest, type ServiceDefinition, type SpendCheckResult, type SpendingCheckResult, type SpendingChecker, type SpendingLimit, type SpendingLimits, type SpendingTrackerConfig, type ValidationResult, 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/index.d.ts
CHANGED
|
@@ -652,6 +652,42 @@ declare function validateAllScopesAccess(grantedScopes: readonly Scope[], reques
|
|
|
652
652
|
*/
|
|
653
653
|
declare function hasScope(grantedScopes: readonly Scope[], requested: Scope): boolean;
|
|
654
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Detects if a scope requires content review before execution.
|
|
657
|
+
*
|
|
658
|
+
* Public content scopes that require review:
|
|
659
|
+
* - `publish:web` - publishing content to the web
|
|
660
|
+
* - `create:public_content` - creating public-facing content
|
|
661
|
+
*
|
|
662
|
+
* @module scopes/content-review-detector
|
|
663
|
+
*/
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Check if a scope requires content review.
|
|
667
|
+
*
|
|
668
|
+
* @param scope - The scope to check
|
|
669
|
+
* @returns `true` if the scope requires content review
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```ts
|
|
673
|
+
* requiresContentReview({ service: "web", permissionLevel: "publish" }); // true
|
|
674
|
+
* requiresContentReview({ service: "public_content", permissionLevel: "create" }); // true
|
|
675
|
+
* requiresContentReview({ service: "gmail", permissionLevel: "execute" }); // false
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
declare function requiresContentReview(scope: Scope): boolean;
|
|
679
|
+
/**
|
|
680
|
+
* Check if a tool name/action indicates public content creation.
|
|
681
|
+
*
|
|
682
|
+
* This is a helper for cases where the service might not be explicitly
|
|
683
|
+
* "web" or "public_content" but the action name indicates public content.
|
|
684
|
+
*
|
|
685
|
+
* @param toolName - The tool name to check
|
|
686
|
+
* @param service - The service name
|
|
687
|
+
* @returns `true` if the tool/action indicates public content
|
|
688
|
+
*/
|
|
689
|
+
declare function isPublicContentAction(toolName: string, service: string): boolean;
|
|
690
|
+
|
|
655
691
|
/**
|
|
656
692
|
* Custom element tag name for the consent component.
|
|
657
693
|
*/
|
|
@@ -1775,6 +1811,11 @@ interface McpAdapterConfig {
|
|
|
1775
1811
|
* Used with baseUrl to fetch auto-approval status if checkAutoApprove is not provided.
|
|
1776
1812
|
*/
|
|
1777
1813
|
readonly apiKey?: string;
|
|
1814
|
+
/**
|
|
1815
|
+
* When `true`, blocks until the content review completes (or times out) and forwards the tool call if approved.
|
|
1816
|
+
* Requires `baseUrl` and `apiKey`. Default `false` preserves the existing fast block with `requires_approval` logging only.
|
|
1817
|
+
*/
|
|
1818
|
+
readonly waitForReviewDecision?: boolean;
|
|
1778
1819
|
}
|
|
1779
1820
|
/**
|
|
1780
1821
|
* The MCP adapter produced by {@link createMcpAdapter}.
|
|
@@ -2179,4 +2220,67 @@ declare class MulticornShield {
|
|
|
2179
2220
|
destroy(): void;
|
|
2180
2221
|
}
|
|
2181
2222
|
|
|
2182
|
-
|
|
2223
|
+
/**
|
|
2224
|
+
* Local type definitions for the OpenClaw Plugin SDK.
|
|
2225
|
+
*
|
|
2226
|
+
* Extracted from openclaw/dist/plugin-sdk/plugins/types.d.ts (v2026.2.26).
|
|
2227
|
+
* We define only the subset needed for the Shield plugin so there's no
|
|
2228
|
+
* build-time dependency on the OpenClaw package itself.
|
|
2229
|
+
*
|
|
2230
|
+
* @module openclaw/plugin-sdk.types
|
|
2231
|
+
*/
|
|
2232
|
+
interface PluginLogger {
|
|
2233
|
+
debug?: (message: string) => void;
|
|
2234
|
+
info: (message: string) => void;
|
|
2235
|
+
warn: (message: string) => void;
|
|
2236
|
+
error: (message: string) => void;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* HTTP client for communicating with the Multicorn Shield API.
|
|
2241
|
+
*
|
|
2242
|
+
* Handles agent registration, permission fetching, and action logging.
|
|
2243
|
+
* Follows the same patterns as the MCP proxy client but is self-contained
|
|
2244
|
+
* so the hook has no runtime dependency on proxy internals.
|
|
2245
|
+
*
|
|
2246
|
+
* Security: the API key is passed as a parameter and sent only via the
|
|
2247
|
+
* `X-Multicorn-Key` header over HTTPS. It is never logged or written
|
|
2248
|
+
* to disk.
|
|
2249
|
+
*
|
|
2250
|
+
* @module openclaw/shield-client
|
|
2251
|
+
*/
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* `data` shape from GET /api/v1/content-reviews/:id/status when `success` is true.
|
|
2255
|
+
*/
|
|
2256
|
+
interface ContentReviewStatusResponse {
|
|
2257
|
+
readonly id: string;
|
|
2258
|
+
readonly status: "pending" | "approved" | "blocked" | "timeout";
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Result of {@link requestContentReview} or {@link pollContentReviewStatus}.
|
|
2262
|
+
*/
|
|
2263
|
+
interface ContentReviewResult {
|
|
2264
|
+
readonly status: "approved" | "blocked" | "timeout";
|
|
2265
|
+
readonly reviewId?: string;
|
|
2266
|
+
readonly reason?: string;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Payload for {@link requestContentReview}. `cost` is optional; the POST body always includes `cost`, defaulting to `0` when omitted (matches {@link LogActionRequest} in the service).
|
|
2270
|
+
*/
|
|
2271
|
+
interface ContentReviewRequestPayload {
|
|
2272
|
+
readonly agent: string;
|
|
2273
|
+
readonly service: string;
|
|
2274
|
+
readonly actionType: string;
|
|
2275
|
+
/** Defaults to `0` when omitted. Must be >= 0 if set. */
|
|
2276
|
+
readonly cost?: number;
|
|
2277
|
+
readonly metadata?: Readonly<Record<string, string | number | boolean>>;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Create a content-review request via POST /api/v1/actions with `status: "requires_approval"`, then poll until decided.
|
|
2281
|
+
*
|
|
2282
|
+
* Wire format: response `data.content_review_id` (snake_case) per service Jackson naming.
|
|
2283
|
+
*/
|
|
2284
|
+
declare function requestContentReview(payload: ContentReviewRequestPayload, apiKey: string, baseUrl: string, logger?: PluginLogger): Promise<ContentReviewResult>;
|
|
2285
|
+
|
|
2286
|
+
export { ACTION_STATUSES, AGENT_STATUSES, type Action, type ActionInput, type ActionLogger, type ActionLoggerConfig, type ActionPayload, type ActionStatus, type Agent, type AgentStatus, type ApiError, BUILT_IN_SERVICES, type BatchModeConfig, type BuiltInServiceName, CONSENT_ELEMENT_TAG, type ConsentDecision, type ConsentDeniedEventDetail, type ConsentEventDetail, type ConsentEventMap, type ConsentEventName, type ConsentGrantedEventDetail, type ConsentOptions, type ConsentPartialEventDetail, type ContentReviewRequestPayload, type ContentReviewResult, type ContentReviewStatusResponse, type FocusTrap, type McpAdapter, type McpAdapterConfig, type McpAdapterResult, type McpBlockedResult, type McpToolCall, type McpToolHandler, type McpToolResult, MulticornConsent, MulticornShield, type MulticornShieldConfig, PERMISSION_LEVELS, type Permission, type PermissionLevel, type RemainingBudget, SERVICE_NAME_PATTERN, type Scope, ScopeParseError, type ScopeParseResult, type ScopeRegistry, type ScopeRequest, type ServiceDefinition, type SpendCheckResult, type SpendingCheckResult, type SpendingChecker, type SpendingLimit, type SpendingLimits, type SpendingTrackerConfig, type ValidationResult, centsToDollars, createActionLogger, createFocusTrap, createMcpAdapter, createScopeRegistry, createSpendingChecker, dollarsToCents, formatScope, getPermissionLabel, getScopeLabel, getScopeShortLabel, getServiceDisplayName, getServiceIcon, hasScope, isBlockedResult, isPublicContentAction, isValidScopeString, parseScope, parseScopes, requestContentReview, requiresContentReview, tryParseScope, validateAllScopesAccess, validateScopeAccess };
|