multicorn-shield 1.3.0 → 1.3.2
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 +21 -0
- package/dist/multicorn-proxy.js +30 -3
- package/dist/multicorn-shield.js +30 -3
- package/dist/openclaw-plugin/multicorn-shield.js +9 -0
- package/dist/shield-extension.js +1 -1
- package/package.json +1 -1
- package/plugins/multicorn-shield/hooks/scripts/pre-tool-use.cjs +73 -25
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +69 -25
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
- Bump `version` in `package.json` before publishing to npm.
|
|
11
11
|
|
|
12
|
+
## [1.3.2] - 2026-05-07
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Pass action cost (USD) to the backend when logging approved and spending-blocked actions via the MCP proxy. Previously cost was computed locally for spending-limit checks but never sent to the API, causing all agent spend totals to show $0.
|
|
17
|
+
- Add optional `cost` field to `ActionLogPayload` in `shield-client.ts` so OpenClaw plugin callers can include cost when it becomes available upstream.
|
|
18
|
+
- Sanitise cost values extracted from tool arguments before logging (reject negative, NaN, Infinity, and values exceeding $1M).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Print signup URL and API key instructions before the key prompt in the CLI wizard for new users who don't yet have an account.
|
|
23
|
+
- Print dashboard URL at the end of the CLI wizard setup summary.
|
|
24
|
+
- Log a one-time "First action recorded" message with dashboard link on the first approved action in both the MCP proxy and OpenClaw plugin.
|
|
25
|
+
|
|
26
|
+
## [1.3.1] - 2026-05-07
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Consent screen not opening for re-created agents with the same name (stale consent marker now cleared on polling timeout)
|
|
31
|
+
- One-time approvals not working in Claude Code and Windsurf hooks (hook now polls approval status instead of immediately blocking when consent marker exists)
|
|
32
|
+
|
|
12
33
|
## [1.2.0] - 2026-05-06
|
|
13
34
|
|
|
14
35
|
### Added
|
package/dist/multicorn-proxy.js
CHANGED
|
@@ -1724,6 +1724,16 @@ async function runInit(explicitBaseUrl) {
|
|
|
1724
1724
|
apiKey = existing.apiKey;
|
|
1725
1725
|
}
|
|
1726
1726
|
}
|
|
1727
|
+
if (apiKey.length === 0) {
|
|
1728
|
+
const signupDashboardUrl = deriveDashboardUrl(resolvedBaseUrl).replace(/\/+$/, "");
|
|
1729
|
+
console.log("");
|
|
1730
|
+
console.log(" Multicorn Shield controls what your AI agents can do.");
|
|
1731
|
+
console.log(" You need a free account to get an API key.");
|
|
1732
|
+
console.log("");
|
|
1733
|
+
console.log(` 1. Sign up or log in \u2192 ${signupDashboardUrl}`);
|
|
1734
|
+
console.log(" 2. Go to Settings \u2192 API Keys to create a key");
|
|
1735
|
+
console.log("");
|
|
1736
|
+
}
|
|
1727
1737
|
while (apiKey.length === 0) {
|
|
1728
1738
|
const input = await ask("API key (starts with mcs_): ");
|
|
1729
1739
|
const key = input.trim();
|
|
@@ -2416,6 +2426,13 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2416
2426
|
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
2417
2427
|
process.stderr.write(blocks.join("") + "\n");
|
|
2418
2428
|
}
|
|
2429
|
+
const dashboardUrl = deriveDashboardUrl(resolvedBaseUrl).replace(/\/+$/, "");
|
|
2430
|
+
console.log("");
|
|
2431
|
+
console.log(" Your dashboard");
|
|
2432
|
+
console.log(` \u2192 ${dashboardUrl}/agents`);
|
|
2433
|
+
console.log("");
|
|
2434
|
+
console.log(" Use any tool in your agent to see it appear.");
|
|
2435
|
+
console.log("");
|
|
2419
2436
|
}
|
|
2420
2437
|
return lastConfig;
|
|
2421
2438
|
}
|
|
@@ -3012,6 +3029,7 @@ function createProxyServer(config) {
|
|
|
3012
3029
|
const pendingLines = [];
|
|
3013
3030
|
let draining = false;
|
|
3014
3031
|
let stopped = false;
|
|
3032
|
+
let hasLoggedFirstAction = false;
|
|
3015
3033
|
async function refreshScopes() {
|
|
3016
3034
|
if (stopped) return;
|
|
3017
3035
|
if (agentId.length === 0) return;
|
|
@@ -3117,8 +3135,10 @@ function createProxyServer(config) {
|
|
|
3117
3135
|
);
|
|
3118
3136
|
}
|
|
3119
3137
|
}
|
|
3138
|
+
const rawCostCents = extractCostCents(toolParams.arguments);
|
|
3139
|
+
const costCents = Number.isFinite(rawCostCents) && rawCostCents > 0 ? Math.min(rawCostCents, 1e8) : 0;
|
|
3140
|
+
const costUsd = costCents > 0 ? costCents / 100 : void 0;
|
|
3120
3141
|
if (spendingChecker !== null) {
|
|
3121
|
-
const costCents = extractCostCents(toolParams.arguments);
|
|
3122
3142
|
if (costCents > 0) {
|
|
3123
3143
|
const spendResult = spendingChecker.checkSpend(costCents);
|
|
3124
3144
|
if (!spendResult.allowed) {
|
|
@@ -3137,7 +3157,8 @@ function createProxyServer(config) {
|
|
|
3137
3157
|
agent: config.agentName,
|
|
3138
3158
|
service,
|
|
3139
3159
|
actionType: action,
|
|
3140
|
-
status: "blocked"
|
|
3160
|
+
status: "blocked",
|
|
3161
|
+
...costUsd !== void 0 ? { cost: costUsd } : {}
|
|
3141
3162
|
});
|
|
3142
3163
|
config.logger.debug("Spending-blocked action logged.", { tool: toolParams.name });
|
|
3143
3164
|
}
|
|
@@ -3165,9 +3186,15 @@ function createProxyServer(config) {
|
|
|
3165
3186
|
agent: config.agentName,
|
|
3166
3187
|
service,
|
|
3167
3188
|
actionType: action,
|
|
3168
|
-
status: "approved"
|
|
3189
|
+
status: "approved",
|
|
3190
|
+
...costUsd !== void 0 ? { cost: costUsd } : {}
|
|
3169
3191
|
});
|
|
3170
3192
|
config.logger.debug("Approved action logged.", { tool: toolParams.name });
|
|
3193
|
+
if (!hasLoggedFirstAction) {
|
|
3194
|
+
hasLoggedFirstAction = true;
|
|
3195
|
+
const dashUrl = deriveDashboardUrl(config.baseUrl).replace(/\/+$/, "");
|
|
3196
|
+
config.logger.info(`First action recorded. View activity \u2192 ${dashUrl}/agents`);
|
|
3197
|
+
}
|
|
3171
3198
|
}
|
|
3172
3199
|
}
|
|
3173
3200
|
return null;
|
package/dist/multicorn-shield.js
CHANGED
|
@@ -1795,6 +1795,16 @@ async function runInit(explicitBaseUrl) {
|
|
|
1795
1795
|
apiKey = existing.apiKey;
|
|
1796
1796
|
}
|
|
1797
1797
|
}
|
|
1798
|
+
if (apiKey.length === 0) {
|
|
1799
|
+
const signupDashboardUrl = deriveDashboardUrl(resolvedBaseUrl).replace(/\/+$/, "");
|
|
1800
|
+
console.log("");
|
|
1801
|
+
console.log(" Multicorn Shield controls what your AI agents can do.");
|
|
1802
|
+
console.log(" You need a free account to get an API key.");
|
|
1803
|
+
console.log("");
|
|
1804
|
+
console.log(` 1. Sign up or log in \u2192 ${signupDashboardUrl}`);
|
|
1805
|
+
console.log(" 2. Go to Settings \u2192 API Keys to create a key");
|
|
1806
|
+
console.log("");
|
|
1807
|
+
}
|
|
1798
1808
|
while (apiKey.length === 0) {
|
|
1799
1809
|
const input = await ask("API key (starts with mcs_): ");
|
|
1800
1810
|
const key = input.trim();
|
|
@@ -2487,6 +2497,13 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2487
2497
|
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
2488
2498
|
process.stderr.write(blocks.join("") + "\n");
|
|
2489
2499
|
}
|
|
2500
|
+
const dashboardUrl = deriveDashboardUrl(resolvedBaseUrl).replace(/\/+$/, "");
|
|
2501
|
+
console.log("");
|
|
2502
|
+
console.log(" Your dashboard");
|
|
2503
|
+
console.log(` \u2192 ${dashboardUrl}/agents`);
|
|
2504
|
+
console.log("");
|
|
2505
|
+
console.log(" Use any tool in your agent to see it appear.");
|
|
2506
|
+
console.log("");
|
|
2490
2507
|
}
|
|
2491
2508
|
return lastConfig;
|
|
2492
2509
|
}
|
|
@@ -2964,6 +2981,7 @@ function createProxyServer(config) {
|
|
|
2964
2981
|
const pendingLines = [];
|
|
2965
2982
|
let draining = false;
|
|
2966
2983
|
let stopped = false;
|
|
2984
|
+
let hasLoggedFirstAction = false;
|
|
2967
2985
|
async function refreshScopes() {
|
|
2968
2986
|
if (stopped) return;
|
|
2969
2987
|
if (agentId.length === 0) return;
|
|
@@ -3069,8 +3087,10 @@ function createProxyServer(config) {
|
|
|
3069
3087
|
);
|
|
3070
3088
|
}
|
|
3071
3089
|
}
|
|
3090
|
+
const rawCostCents = extractCostCents(toolParams.arguments);
|
|
3091
|
+
const costCents = Number.isFinite(rawCostCents) && rawCostCents > 0 ? Math.min(rawCostCents, 1e8) : 0;
|
|
3092
|
+
const costUsd = costCents > 0 ? costCents / 100 : void 0;
|
|
3072
3093
|
if (spendingChecker !== null) {
|
|
3073
|
-
const costCents = extractCostCents(toolParams.arguments);
|
|
3074
3094
|
if (costCents > 0) {
|
|
3075
3095
|
const spendResult = spendingChecker.checkSpend(costCents);
|
|
3076
3096
|
if (!spendResult.allowed) {
|
|
@@ -3089,7 +3109,8 @@ function createProxyServer(config) {
|
|
|
3089
3109
|
agent: config.agentName,
|
|
3090
3110
|
service,
|
|
3091
3111
|
actionType: action,
|
|
3092
|
-
status: "blocked"
|
|
3112
|
+
status: "blocked",
|
|
3113
|
+
...costUsd !== void 0 ? { cost: costUsd } : {}
|
|
3093
3114
|
});
|
|
3094
3115
|
config.logger.debug("Spending-blocked action logged.", { tool: toolParams.name });
|
|
3095
3116
|
}
|
|
@@ -3117,9 +3138,15 @@ function createProxyServer(config) {
|
|
|
3117
3138
|
agent: config.agentName,
|
|
3118
3139
|
service,
|
|
3119
3140
|
actionType: action,
|
|
3120
|
-
status: "approved"
|
|
3141
|
+
status: "approved",
|
|
3142
|
+
...costUsd !== void 0 ? { cost: costUsd } : {}
|
|
3121
3143
|
});
|
|
3122
3144
|
config.logger.debug("Approved action logged.", { tool: toolParams.name });
|
|
3145
|
+
if (!hasLoggedFirstAction) {
|
|
3146
|
+
hasLoggedFirstAction = true;
|
|
3147
|
+
const dashUrl = deriveDashboardUrl(config.baseUrl).replace(/\/+$/, "");
|
|
3148
|
+
config.logger.info(`First action recorded. View activity \u2192 ${dashUrl}/agents`);
|
|
3149
|
+
}
|
|
3123
3150
|
}
|
|
3124
3151
|
}
|
|
3125
3152
|
return null;
|
|
@@ -468,6 +468,7 @@ var pluginLogger = null;
|
|
|
468
468
|
var pluginConfig;
|
|
469
469
|
var connectionLogged = false;
|
|
470
470
|
var pinnedAgentName = null;
|
|
471
|
+
var hasLoggedFirstAction = false;
|
|
471
472
|
var SCOPE_REFRESH_INTERVAL_MS = 6e4;
|
|
472
473
|
var cachedMulticornConfig = null;
|
|
473
474
|
function loadMulticornConfig() {
|
|
@@ -863,6 +864,13 @@ function afterToolCall(event, ctx) {
|
|
|
863
864
|
config.baseUrl,
|
|
864
865
|
pluginLogger ?? void 0
|
|
865
866
|
);
|
|
867
|
+
if (!event.error && !hasLoggedFirstAction) {
|
|
868
|
+
hasLoggedFirstAction = true;
|
|
869
|
+
const dashUrl = deriveDashboardUrl(config.baseUrl).replace(/\/+$/, "");
|
|
870
|
+
if (pluginLogger) {
|
|
871
|
+
pluginLogger.info(`First action recorded. View activity \u2192 ${dashUrl}/agents`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
866
874
|
return Promise.resolve();
|
|
867
875
|
}
|
|
868
876
|
var plugin = {
|
|
@@ -908,6 +916,7 @@ function resetState() {
|
|
|
908
916
|
cachedMulticornConfig = null;
|
|
909
917
|
connectionLogged = false;
|
|
910
918
|
pinnedAgentName = null;
|
|
919
|
+
hasLoggedFirstAction = false;
|
|
911
920
|
}
|
|
912
921
|
|
|
913
922
|
export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName };
|
package/dist/shield-extension.js
CHANGED
|
@@ -22417,7 +22417,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22417
22417
|
|
|
22418
22418
|
// package.json
|
|
22419
22419
|
var package_default = {
|
|
22420
|
-
version: "1.3.
|
|
22420
|
+
version: "1.3.2"};
|
|
22421
22421
|
|
|
22422
22422
|
// src/package-meta.ts
|
|
22423
22423
|
var PACKAGE_VERSION = package_default.version;
|
package/package.json
CHANGED
|
@@ -357,6 +357,17 @@ function writeConsentMarker(agentName) {
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
/**
|
|
361
|
+
* @param {string} agentName
|
|
362
|
+
*/
|
|
363
|
+
function removeConsentMarker(agentName) {
|
|
364
|
+
try {
|
|
365
|
+
fs.unlinkSync(consentMarkerPath(agentName));
|
|
366
|
+
} catch {
|
|
367
|
+
/* ignore */
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
360
371
|
/**
|
|
361
372
|
* @param {string} url
|
|
362
373
|
*/
|
|
@@ -387,33 +398,16 @@ function sleep(ms) {
|
|
|
387
398
|
}
|
|
388
399
|
|
|
389
400
|
/**
|
|
401
|
+
* Polls GET /api/v1/approvals/{id} until the approval is decided or timeout.
|
|
402
|
+
* Returns true if approved (caller should exit 0), false on error/unknown.
|
|
403
|
+
* Exits the process on denial/expiry.
|
|
404
|
+
*
|
|
390
405
|
* @param {{ apiKey: string; baseUrl: string; agentName: string }} config
|
|
391
406
|
* @param {string} approvalId
|
|
392
|
-
* @param {string}
|
|
393
|
-
* @
|
|
394
|
-
* @returns {Promise<void>}
|
|
407
|
+
* @param {string} approvalsUrl
|
|
408
|
+
* @returns {Promise<boolean>}
|
|
395
409
|
*/
|
|
396
|
-
async function
|
|
397
|
-
config,
|
|
398
|
-
approvalId,
|
|
399
|
-
service,
|
|
400
|
-
actionType,
|
|
401
|
-
approvalsUrl,
|
|
402
|
-
) {
|
|
403
|
-
if (hasConsentMarker(config.agentName)) {
|
|
404
|
-
process.stderr.write(
|
|
405
|
-
`[multicorn-shield] PreToolUse: Action blocked: this action requires approval before it can run.\n` +
|
|
406
|
-
` Grant access in the Shield dashboard and retry.\n` +
|
|
407
|
-
` Detail: ${approvalsUrl}\n`,
|
|
408
|
-
);
|
|
409
|
-
process.exit(2);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
413
|
-
writeConsentMarker(config.agentName);
|
|
414
|
-
openBrowser(url);
|
|
415
|
-
process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
|
|
416
|
-
|
|
410
|
+
async function pollApprovalStatus(config, approvalId, approvalsUrl) {
|
|
417
411
|
for (let i = 0; i < MAX_APPROVAL_POLLS; i++) {
|
|
418
412
|
if (i > 0) {
|
|
419
413
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -438,7 +432,7 @@ async function handlePendingWithConsentAndPoll(
|
|
|
438
432
|
const d = /** @type {Record<string, unknown>} */ (data);
|
|
439
433
|
const st = String(d.status ?? "").toLowerCase();
|
|
440
434
|
if (st === "approved") {
|
|
441
|
-
|
|
435
|
+
return true;
|
|
442
436
|
}
|
|
443
437
|
if (st === "blocked" || st === "denied" || st === "rejected") {
|
|
444
438
|
const reason =
|
|
@@ -462,6 +456,60 @@ async function handlePendingWithConsentAndPoll(
|
|
|
462
456
|
continue;
|
|
463
457
|
}
|
|
464
458
|
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {{ apiKey: string; baseUrl: string; agentName: string }} config
|
|
464
|
+
* @param {string} approvalId
|
|
465
|
+
* @param {string} service
|
|
466
|
+
* @param {string} actionType
|
|
467
|
+
* @returns {Promise<void>}
|
|
468
|
+
*/
|
|
469
|
+
async function handlePendingWithConsentAndPoll(
|
|
470
|
+
config,
|
|
471
|
+
approvalId,
|
|
472
|
+
service,
|
|
473
|
+
actionType,
|
|
474
|
+
approvalsUrl,
|
|
475
|
+
) {
|
|
476
|
+
if (hasConsentMarker(config.agentName)) {
|
|
477
|
+
// Consent was previously completed. Poll for the approval decision.
|
|
478
|
+
// If the marker is stale (agent was re-created with no permissions),
|
|
479
|
+
// the API will keep returning "pending" and we'll detect it below.
|
|
480
|
+
process.stderr.write(
|
|
481
|
+
`[multicorn-shield] PreToolUse: Waiting for approval (up to 5 min)...\n` +
|
|
482
|
+
` Approve in the Shield dashboard: ${approvalsUrl}\n`,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
|
|
486
|
+
if (approved) {
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Timed out waiting. The consent marker may be stale (agent re-created
|
|
491
|
+
// on the server without permissions). Remove it so the next tool call
|
|
492
|
+
// triggers the consent flow instead of looping on approvals forever.
|
|
493
|
+
removeConsentMarker(config.agentName);
|
|
494
|
+
|
|
495
|
+
process.stderr.write(
|
|
496
|
+
`[multicorn-shield] PreToolUse: Action blocked: approval timed out after 5 minutes.\n` +
|
|
497
|
+
` Approve in the Shield dashboard, then retry the tool call.\n` +
|
|
498
|
+
` Detail: approvalsUrl=${approvalsUrl}\n`,
|
|
499
|
+
);
|
|
500
|
+
process.exit(2);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// No consent marker: first-time flow. Open the consent screen.
|
|
504
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
505
|
+
writeConsentMarker(config.agentName);
|
|
506
|
+
openBrowser(url);
|
|
507
|
+
process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
|
|
508
|
+
|
|
509
|
+
const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
|
|
510
|
+
if (approved) {
|
|
511
|
+
process.exit(0);
|
|
512
|
+
}
|
|
465
513
|
|
|
466
514
|
process.stderr.write(
|
|
467
515
|
`[multicorn-shield] PreToolUse: Action blocked: approval timed out after 5 minutes.\n` +
|
|
@@ -400,6 +400,17 @@ function writeConsentMarker(agentName) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* @param {string} agentName
|
|
405
|
+
*/
|
|
406
|
+
function removeConsentMarker(agentName) {
|
|
407
|
+
try {
|
|
408
|
+
fs.unlinkSync(consentMarkerPath(agentName));
|
|
409
|
+
} catch {
|
|
410
|
+
/* ignore */
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
403
414
|
/**
|
|
404
415
|
* @param {string} url
|
|
405
416
|
*/
|
|
@@ -430,34 +441,16 @@ function sleep(ms) {
|
|
|
430
441
|
}
|
|
431
442
|
|
|
432
443
|
/**
|
|
444
|
+
* Polls GET /api/v1/approvals/{id} until the approval is decided or timeout.
|
|
445
|
+
* Returns true if approved, false on timeout.
|
|
446
|
+
* Exits the process on denial/expiry.
|
|
447
|
+
*
|
|
433
448
|
* @param {{ apiKey: string; baseUrl: string; agentName: string }} config
|
|
434
449
|
* @param {string} approvalId
|
|
435
|
-
* @param {string} service
|
|
436
|
-
* @param {string} actionType
|
|
437
450
|
* @param {string} approvalsUrl
|
|
438
|
-
* @returns {Promise<
|
|
451
|
+
* @returns {Promise<boolean>}
|
|
439
452
|
*/
|
|
440
|
-
async function
|
|
441
|
-
config,
|
|
442
|
-
approvalId,
|
|
443
|
-
service,
|
|
444
|
-
actionType,
|
|
445
|
-
approvalsUrl,
|
|
446
|
-
) {
|
|
447
|
-
if (hasConsentMarker(config.agentName)) {
|
|
448
|
-
process.stderr.write(
|
|
449
|
-
`${LOG_PREFIX} Action blocked: this action requires approval before it can run.\n` +
|
|
450
|
-
` Grant access in the Shield dashboard and retry.\n` +
|
|
451
|
-
` Detail: ${approvalsUrl}\n`,
|
|
452
|
-
);
|
|
453
|
-
process.exit(2);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
457
|
-
writeConsentMarker(config.agentName);
|
|
458
|
-
openBrowser(url);
|
|
459
|
-
process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
|
|
460
|
-
|
|
453
|
+
async function pollApprovalStatus(config, approvalId, approvalsUrl) {
|
|
461
454
|
for (let i = 0; i < MAX_APPROVAL_POLLS; i++) {
|
|
462
455
|
if (i > 0) {
|
|
463
456
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -482,7 +475,7 @@ async function handlePendingWithConsentAndPoll(
|
|
|
482
475
|
const d = /** @type {Record<string, unknown>} */ (data);
|
|
483
476
|
const st = String(d.status ?? "").toLowerCase();
|
|
484
477
|
if (st === "approved") {
|
|
485
|
-
|
|
478
|
+
return true;
|
|
486
479
|
}
|
|
487
480
|
if (st === "blocked" || st === "denied" || st === "rejected") {
|
|
488
481
|
const reason =
|
|
@@ -506,6 +499,57 @@ async function handlePendingWithConsentAndPoll(
|
|
|
506
499
|
continue;
|
|
507
500
|
}
|
|
508
501
|
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* @param {{ apiKey: string; baseUrl: string; agentName: string }} config
|
|
507
|
+
* @param {string} approvalId
|
|
508
|
+
* @param {string} service
|
|
509
|
+
* @param {string} actionType
|
|
510
|
+
* @param {string} approvalsUrl
|
|
511
|
+
* @returns {Promise<void>}
|
|
512
|
+
*/
|
|
513
|
+
async function handlePendingWithConsentAndPoll(
|
|
514
|
+
config,
|
|
515
|
+
approvalId,
|
|
516
|
+
service,
|
|
517
|
+
actionType,
|
|
518
|
+
approvalsUrl,
|
|
519
|
+
) {
|
|
520
|
+
if (hasConsentMarker(config.agentName)) {
|
|
521
|
+
// Consent was previously completed. Poll for the approval decision.
|
|
522
|
+
process.stderr.write(
|
|
523
|
+
`${LOG_PREFIX} Waiting for approval (up to 5 min)...\n` +
|
|
524
|
+
` Approve in the Shield dashboard: ${approvalsUrl}\n`,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
|
|
528
|
+
if (approved) {
|
|
529
|
+
process.exit(0);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Timed out. Remove stale consent marker so next call triggers consent flow.
|
|
533
|
+
removeConsentMarker(config.agentName);
|
|
534
|
+
|
|
535
|
+
process.stderr.write(
|
|
536
|
+
`${LOG_PREFIX} Action blocked: approval timed out after 5 minutes.\n` +
|
|
537
|
+
` Approve in the Shield dashboard, then retry.\n` +
|
|
538
|
+
` Detail: approvalsUrl=${approvalsUrl}\n`,
|
|
539
|
+
);
|
|
540
|
+
process.exit(2);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// No consent marker: first-time flow. Open the consent screen.
|
|
544
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
545
|
+
writeConsentMarker(config.agentName);
|
|
546
|
+
openBrowser(url);
|
|
547
|
+
process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
|
|
548
|
+
|
|
549
|
+
const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
|
|
550
|
+
if (approved) {
|
|
551
|
+
process.exit(0);
|
|
552
|
+
}
|
|
509
553
|
|
|
510
554
|
process.stderr.write(
|
|
511
555
|
`${LOG_PREFIX} Action blocked: approval timed out after 5 minutes.\n` +
|