multicorn-shield 0.12.0 → 0.13.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 +9 -0
- package/dist/index.cjs +13 -0
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +13 -1
- package/dist/multicorn-proxy.js +406 -15
- package/dist/shield-extension.js +1 -1
- package/package.json +2 -1
- package/plugins/gemini-cli/hooks/scripts/after-tool.cjs +110 -0
- package/plugins/gemini-cli/hooks/scripts/before-tool.cjs +197 -0
- package/plugins/gemini-cli/hooks/scripts/shared.cjs +319 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Gemini CLI native plugin: BeforeTool/AfterTool hook scripts for full governance
|
|
13
|
+
- Gemini CLI hosted proxy support with httpUrl config field
|
|
14
|
+
- CLI wizard: Gemini CLI platform with native plugin and hosted proxy integration modes
|
|
15
|
+
- CLI wizard: platform prerequisite detection (warns if target platform is not installed)
|
|
16
|
+
|
|
8
17
|
## [X.Y.Z] - YYYY-MM-DD
|
|
9
18
|
|
|
10
19
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -14,6 +14,18 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// src/types/index.ts
|
|
17
|
+
var AGENT_PLATFORM_SLUGS = [
|
|
18
|
+
"openclaw",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"claude-desktop",
|
|
21
|
+
"cursor",
|
|
22
|
+
"windsurf",
|
|
23
|
+
"cline",
|
|
24
|
+
"gemini-cli",
|
|
25
|
+
"other-mcp",
|
|
26
|
+
"github-actions",
|
|
27
|
+
"unknown"
|
|
28
|
+
];
|
|
17
29
|
var AGENT_STATUSES = {
|
|
18
30
|
Active: "active",
|
|
19
31
|
Paused: "paused",
|
|
@@ -2893,6 +2905,7 @@ function validateApiKey(apiKey) {
|
|
|
2893
2905
|
}
|
|
2894
2906
|
|
|
2895
2907
|
exports.ACTION_STATUSES = ACTION_STATUSES;
|
|
2908
|
+
exports.AGENT_PLATFORM_SLUGS = AGENT_PLATFORM_SLUGS;
|
|
2896
2909
|
exports.AGENT_STATUSES = AGENT_STATUSES;
|
|
2897
2910
|
exports.BUILT_IN_SERVICES = BUILT_IN_SERVICES;
|
|
2898
2911
|
exports.CONSENT_ELEMENT_TAG = CONSENT_ELEMENT_TAG;
|
package/dist/index.d.cts
CHANGED
|
@@ -9,6 +9,11 @@ import { LitElement, PropertyValues, HTMLTemplateResult } from 'lit';
|
|
|
9
9
|
*
|
|
10
10
|
* @module types
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Agent client platforms supported by hosted proxy and native hooks (aligned with API validation).
|
|
14
|
+
*/
|
|
15
|
+
declare const AGENT_PLATFORM_SLUGS: readonly ["openclaw", "claude-code", "claude-desktop", "cursor", "windsurf", "cline", "gemini-cli", "other-mcp", "github-actions", "unknown"];
|
|
16
|
+
type AgentPlatformSlug = (typeof AGENT_PLATFORM_SLUGS)[number];
|
|
12
17
|
/**
|
|
13
18
|
* Possible operational states for an agent.
|
|
14
19
|
*
|
|
@@ -2310,4 +2315,4 @@ interface ContentReviewRequestPayload {
|
|
|
2310
2315
|
*/
|
|
2311
2316
|
declare function requestContentReview(payload: ContentReviewRequestPayload, apiKey: string, baseUrl: string, logger?: PluginLogger): Promise<ContentReviewResult>;
|
|
2312
2317
|
|
|
2313
|
-
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, MulticornBadge, 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 };
|
|
2318
|
+
export { ACTION_STATUSES, AGENT_PLATFORM_SLUGS, AGENT_STATUSES, type Action, type ActionInput, type ActionLogger, type ActionLoggerConfig, type ActionPayload, type ActionStatus, type Agent, type AgentPlatformSlug, 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, MulticornBadge, 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
|
@@ -9,6 +9,11 @@ import { LitElement, PropertyValues, HTMLTemplateResult } from 'lit';
|
|
|
9
9
|
*
|
|
10
10
|
* @module types
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Agent client platforms supported by hosted proxy and native hooks (aligned with API validation).
|
|
14
|
+
*/
|
|
15
|
+
declare const AGENT_PLATFORM_SLUGS: readonly ["openclaw", "claude-code", "claude-desktop", "cursor", "windsurf", "cline", "gemini-cli", "other-mcp", "github-actions", "unknown"];
|
|
16
|
+
type AgentPlatformSlug = (typeof AGENT_PLATFORM_SLUGS)[number];
|
|
12
17
|
/**
|
|
13
18
|
* Possible operational states for an agent.
|
|
14
19
|
*
|
|
@@ -2310,4 +2315,4 @@ interface ContentReviewRequestPayload {
|
|
|
2310
2315
|
*/
|
|
2311
2316
|
declare function requestContentReview(payload: ContentReviewRequestPayload, apiKey: string, baseUrl: string, logger?: PluginLogger): Promise<ContentReviewResult>;
|
|
2312
2317
|
|
|
2313
|
-
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, MulticornBadge, 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 };
|
|
2318
|
+
export { ACTION_STATUSES, AGENT_PLATFORM_SLUGS, AGENT_STATUSES, type Action, type ActionInput, type ActionLogger, type ActionLoggerConfig, type ActionPayload, type ActionStatus, type Agent, type AgentPlatformSlug, 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, MulticornBadge, 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.js
CHANGED
|
@@ -12,6 +12,18 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
// src/types/index.ts
|
|
15
|
+
var AGENT_PLATFORM_SLUGS = [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"claude-desktop",
|
|
19
|
+
"cursor",
|
|
20
|
+
"windsurf",
|
|
21
|
+
"cline",
|
|
22
|
+
"gemini-cli",
|
|
23
|
+
"other-mcp",
|
|
24
|
+
"github-actions",
|
|
25
|
+
"unknown"
|
|
26
|
+
];
|
|
15
27
|
var AGENT_STATUSES = {
|
|
16
28
|
Active: "active",
|
|
17
29
|
Paused: "paused",
|
|
@@ -2890,4 +2902,4 @@ function validateApiKey(apiKey) {
|
|
|
2890
2902
|
}
|
|
2891
2903
|
}
|
|
2892
2904
|
|
|
2893
|
-
export { ACTION_STATUSES, AGENT_STATUSES, BUILT_IN_SERVICES, CONSENT_ELEMENT_TAG, MulticornBadge, 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 };
|
|
2905
|
+
export { ACTION_STATUSES, AGENT_PLATFORM_SLUGS, AGENT_STATUSES, BUILT_IN_SERVICES, CONSENT_ELEMENT_TAG, MulticornBadge, 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,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, statSync } from 'fs';
|
|
3
3
|
import { mkdir, writeFile, readFile, copyFile, chmod, unlink } from 'fs/promises';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
@@ -43,6 +43,23 @@ function withSpinner(message) {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
var NativePluginPrerequisiteMissingError = class extends Error {
|
|
47
|
+
constructor() {
|
|
48
|
+
super("Native plugin prerequisites not met");
|
|
49
|
+
this.name = "NativePluginPrerequisiteMissingError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function isExistingDirectory(path) {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(path)) return false;
|
|
55
|
+
return statSync(path).isDirectory();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function nativePluginSkippedSaveNote(wizardCommand, productName) {
|
|
61
|
+
return "\n" + style.dim("Your agent config has been saved. Run ") + style.cyan(wizardCommand) + style.dim(` again after installing ${productName} to complete hook setup.`) + "\n";
|
|
62
|
+
}
|
|
46
63
|
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
47
64
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
48
65
|
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
@@ -426,6 +443,18 @@ async function installWindsurfNativeHooks() {
|
|
|
426
443
|
`Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
427
444
|
);
|
|
428
445
|
}
|
|
446
|
+
const windsurfConfigDir = join(homedir(), ".codeium", "windsurf");
|
|
447
|
+
if (!isExistingDirectory(windsurfConfigDir)) {
|
|
448
|
+
process.stderr.write(
|
|
449
|
+
style.yellow("\u26A0") + " Windsurf does not appear to be installed (~/.codeium/windsurf/ not found).\n\n"
|
|
450
|
+
);
|
|
451
|
+
process.stderr.write(
|
|
452
|
+
"Open Windsurf at least once so this folder exists, or install from:\n " + style.cyan("https://windsurf.com/download") + "\n\n"
|
|
453
|
+
);
|
|
454
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
455
|
+
process.stderr.write(" " + style.cyan("npx multicorn-proxy init") + "\n");
|
|
456
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
457
|
+
}
|
|
429
458
|
const installDir = getWindsurfHooksInstallDir();
|
|
430
459
|
await mkdir(installDir, { recursive: true });
|
|
431
460
|
const destPre = join(installDir, "pre-action.cjs");
|
|
@@ -489,6 +518,19 @@ async function installClineNativeHooks() {
|
|
|
489
518
|
`Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
490
519
|
);
|
|
491
520
|
}
|
|
521
|
+
const clineDocsDir = join(homedir(), "Documents", "Cline");
|
|
522
|
+
if (!isExistingDirectory(clineDocsDir)) {
|
|
523
|
+
process.stderr.write(
|
|
524
|
+
style.yellow("\u26A0") + " Cline does not appear to be installed (~/Documents/Cline/ not found).\n\n"
|
|
525
|
+
);
|
|
526
|
+
process.stderr.write("Install the Cline VS Code extension first. See:\n");
|
|
527
|
+
process.stderr.write(
|
|
528
|
+
" " + style.cyan("https://docs.cline.bot/getting-started/installing-cline") + "\n\n"
|
|
529
|
+
);
|
|
530
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
531
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
532
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
533
|
+
}
|
|
492
534
|
const installDir = getClineHooksInstallDir();
|
|
493
535
|
await mkdir(installDir, { recursive: true });
|
|
494
536
|
const destPre = join(installDir, "pre-tool-use.cjs");
|
|
@@ -531,12 +573,211 @@ async function promptClineIntegrationMode(ask) {
|
|
|
531
573
|
}
|
|
532
574
|
return choice === 1 ? "native" : "hosted";
|
|
533
575
|
}
|
|
576
|
+
function getGeminiCliHooksInstallDir() {
|
|
577
|
+
return join(homedir(), ".multicorn", "gemini-cli-hooks");
|
|
578
|
+
}
|
|
579
|
+
function getGeminiCliSettingsPath() {
|
|
580
|
+
return join(homedir(), ".gemini", "settings.json");
|
|
581
|
+
}
|
|
582
|
+
function geminiInnerHooksReferenceShield(inner, multicornName) {
|
|
583
|
+
if (!Array.isArray(inner)) return false;
|
|
584
|
+
for (const h of inner) {
|
|
585
|
+
if (typeof h !== "object" || h === null) continue;
|
|
586
|
+
const rec = h;
|
|
587
|
+
if (rec["name"] === multicornName) return true;
|
|
588
|
+
const cmd = rec["command"];
|
|
589
|
+
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
function geminiHookEventsReferenceShield(arr) {
|
|
594
|
+
if (!Array.isArray(arr)) return false;
|
|
595
|
+
for (const entry of arr) {
|
|
596
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
597
|
+
const hooks = entry["hooks"];
|
|
598
|
+
if (geminiInnerHooksReferenceShield(hooks, "multicorn-shield") || geminiInnerHooksReferenceShield(hooks, "multicorn-shield-log")) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
function geminiSettingsHasMulticornHooks(hooks) {
|
|
605
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
|
|
606
|
+
const h = hooks;
|
|
607
|
+
return geminiHookEventsReferenceShield(h["BeforeTool"]) || geminiHookEventsReferenceShield(h["AfterTool"]);
|
|
608
|
+
}
|
|
609
|
+
function geminiFilterInnerHooks(inner) {
|
|
610
|
+
if (!Array.isArray(inner)) return [];
|
|
611
|
+
return inner.filter((h) => {
|
|
612
|
+
if (typeof h !== "object" || h === null) return true;
|
|
613
|
+
const rec = h;
|
|
614
|
+
if (rec["name"] === "multicorn-shield" || rec["name"] === "multicorn-shield-log") return false;
|
|
615
|
+
const cmd = rec["command"];
|
|
616
|
+
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return false;
|
|
617
|
+
return true;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function geminiStripMatcherGroups(arr) {
|
|
621
|
+
if (!Array.isArray(arr)) return [];
|
|
622
|
+
const out = [];
|
|
623
|
+
for (const entry of arr) {
|
|
624
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
625
|
+
const e = entry;
|
|
626
|
+
const filtered = geminiFilterInnerHooks(e["hooks"]);
|
|
627
|
+
if (filtered.length > 0) {
|
|
628
|
+
out.push({ ...e, hooks: filtered });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function geminiStripMulticornHookEntries(hooks) {
|
|
634
|
+
const out = { ...hooks };
|
|
635
|
+
out["BeforeTool"] = geminiStripMatcherGroups(out["BeforeTool"]);
|
|
636
|
+
out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
async function installGeminiCliNativeHooks(ask) {
|
|
640
|
+
const root = multicornShieldPackageRoot();
|
|
641
|
+
const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
|
|
642
|
+
const srcAfter = join(root, "plugins", "gemini-cli", "hooks", "scripts", "after-tool.cjs");
|
|
643
|
+
const srcShared = join(root, "plugins", "gemini-cli", "hooks", "scripts", "shared.cjs");
|
|
644
|
+
if (!existsSync(srcBefore) || !existsSync(srcAfter) || !existsSync(srcShared)) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Could not find Shield Gemini CLI hook scripts at ${srcBefore}. If you use npm, install the latest multicorn-shield package.`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const geminiConfigDir = join(homedir(), ".gemini");
|
|
650
|
+
if (!isExistingDirectory(geminiConfigDir)) {
|
|
651
|
+
process.stderr.write(
|
|
652
|
+
style.yellow("\u26A0") + " Gemini CLI does not appear to be installed (~/.gemini/ not found).\n\n"
|
|
653
|
+
);
|
|
654
|
+
process.stderr.write("Install Gemini CLI first:\n");
|
|
655
|
+
process.stderr.write(" " + style.cyan("npm install -g @google/gemini-cli") + "\n\n");
|
|
656
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
657
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
658
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
659
|
+
}
|
|
660
|
+
const installDir = getGeminiCliHooksInstallDir();
|
|
661
|
+
await mkdir(installDir, { recursive: true });
|
|
662
|
+
const destBefore = join(installDir, "before-tool.cjs");
|
|
663
|
+
const destAfter = join(installDir, "after-tool.cjs");
|
|
664
|
+
const destShared = join(installDir, "shared.cjs");
|
|
665
|
+
await copyFile(srcBefore, destBefore);
|
|
666
|
+
await copyFile(srcAfter, destAfter);
|
|
667
|
+
await copyFile(srcShared, destShared);
|
|
668
|
+
const mode = 493;
|
|
669
|
+
await chmod(destBefore, mode);
|
|
670
|
+
await chmod(destAfter, mode);
|
|
671
|
+
await chmod(destShared, mode);
|
|
672
|
+
const settingsPath = getGeminiCliSettingsPath();
|
|
673
|
+
let existing = {};
|
|
674
|
+
try {
|
|
675
|
+
const rawText = await readFile(settingsPath, "utf8");
|
|
676
|
+
const parsed = JSON.parse(rawText);
|
|
677
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
678
|
+
existing = parsed;
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
682
|
+
existing = {};
|
|
683
|
+
} else {
|
|
684
|
+
process.stderr.write(
|
|
685
|
+
style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Create valid JSON or remove the file, then run init again.
|
|
686
|
+
`
|
|
687
|
+
);
|
|
688
|
+
throw new Error(`Invalid Gemini CLI settings at ${settingsPath}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const hooksRaw = existing["hooks"];
|
|
692
|
+
const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? hooksRaw : {};
|
|
693
|
+
if (geminiSettingsHasMulticornHooks(hooksObj)) {
|
|
694
|
+
const answer = await ask(
|
|
695
|
+
"Existing Multicorn Shield hooks were found in ~/.gemini/settings.json. Overwrite? (Y/n) "
|
|
696
|
+
);
|
|
697
|
+
if (answer.trim().toLowerCase() === "n") {
|
|
698
|
+
throw new Error("Installation cancelled: existing Shield hooks left unchanged.");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const cleaned = geminiStripMulticornHookEntries({ ...hooksObj });
|
|
702
|
+
const beforeArr = Array.isArray(cleaned["BeforeTool"]) ? [...cleaned["BeforeTool"]] : [];
|
|
703
|
+
const afterArr = Array.isArray(cleaned["AfterTool"]) ? [...cleaned["AfterTool"]] : [];
|
|
704
|
+
const beforeCmd = `node ${destBefore}`;
|
|
705
|
+
const afterCmd = `node ${destAfter}`;
|
|
706
|
+
beforeArr.push({
|
|
707
|
+
matcher: ".*",
|
|
708
|
+
hooks: [
|
|
709
|
+
{
|
|
710
|
+
type: "command",
|
|
711
|
+
name: "multicorn-shield",
|
|
712
|
+
command: beforeCmd,
|
|
713
|
+
timeout: 6e4
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
});
|
|
717
|
+
afterArr.push({
|
|
718
|
+
matcher: ".*",
|
|
719
|
+
hooks: [
|
|
720
|
+
{
|
|
721
|
+
type: "command",
|
|
722
|
+
name: "multicorn-shield-log",
|
|
723
|
+
command: afterCmd,
|
|
724
|
+
timeout: 1e4
|
|
725
|
+
}
|
|
726
|
+
]
|
|
727
|
+
});
|
|
728
|
+
existing["hooks"] = {
|
|
729
|
+
...cleaned,
|
|
730
|
+
BeforeTool: beforeArr,
|
|
731
|
+
AfterTool: afterArr
|
|
732
|
+
};
|
|
733
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
734
|
+
await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
735
|
+
}
|
|
736
|
+
async function promptGeminiCliIntegrationMode(ask) {
|
|
737
|
+
process.stderr.write("\n" + style.bold("Gemini CLI integration") + "\n");
|
|
738
|
+
process.stderr.write(
|
|
739
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Gemini CLI Hooks see every file, terminal, web, and MCP action\n"
|
|
740
|
+
);
|
|
741
|
+
process.stderr.write(
|
|
742
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Gemini CLI settings)\n"
|
|
743
|
+
);
|
|
744
|
+
let choice = 0;
|
|
745
|
+
while (choice === 0) {
|
|
746
|
+
const input = await ask("Choose integration (1-2): ");
|
|
747
|
+
const num = parseInt(input.trim(), 10);
|
|
748
|
+
if (num === 1) choice = 1;
|
|
749
|
+
if (num === 2) choice = 2;
|
|
750
|
+
}
|
|
751
|
+
return choice === 1 ? "native" : "hosted";
|
|
752
|
+
}
|
|
753
|
+
function getClaudeDesktopConfigPath() {
|
|
754
|
+
switch (process.platform) {
|
|
755
|
+
case "win32":
|
|
756
|
+
return join(
|
|
757
|
+
process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"),
|
|
758
|
+
"Claude",
|
|
759
|
+
"claude_desktop_config.json"
|
|
760
|
+
);
|
|
761
|
+
case "linux":
|
|
762
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
763
|
+
default:
|
|
764
|
+
return join(
|
|
765
|
+
homedir(),
|
|
766
|
+
"Library",
|
|
767
|
+
"Application Support",
|
|
768
|
+
"Claude",
|
|
769
|
+
"claude_desktop_config.json"
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
534
773
|
var PLATFORM_LABELS = [
|
|
535
774
|
"OpenClaw",
|
|
536
775
|
"Claude Code",
|
|
537
776
|
"Cursor",
|
|
538
777
|
"Windsurf",
|
|
539
778
|
"Cline",
|
|
779
|
+
"Claude Desktop",
|
|
780
|
+
"Gemini CLI",
|
|
540
781
|
"Local MCP / Other"
|
|
541
782
|
];
|
|
542
783
|
var PLATFORM_BY_SELECTION = {
|
|
@@ -545,14 +786,18 @@ var PLATFORM_BY_SELECTION = {
|
|
|
545
786
|
3: "cursor",
|
|
546
787
|
4: "windsurf",
|
|
547
788
|
5: "cline",
|
|
548
|
-
6: "
|
|
789
|
+
6: "claude-desktop",
|
|
790
|
+
7: "gemini-cli",
|
|
791
|
+
8: "other-mcp"
|
|
549
792
|
};
|
|
550
793
|
var DEFAULT_AGENT_NAMES = {
|
|
551
794
|
openclaw: "my-openclaw-agent",
|
|
552
795
|
"claude-code": "my-claude-code-agent",
|
|
553
796
|
cursor: "my-cursor-agent",
|
|
554
797
|
windsurf: "my-windsurf-agent",
|
|
555
|
-
cline: "my-cline-agent"
|
|
798
|
+
cline: "my-cline-agent",
|
|
799
|
+
"claude-desktop": "my-claude-desktop-agent",
|
|
800
|
+
"gemini-cli": "my-gemini-cli-agent"
|
|
556
801
|
};
|
|
557
802
|
async function promptPlatformSelection(ask) {
|
|
558
803
|
process.stderr.write(
|
|
@@ -572,13 +817,13 @@ async function promptPlatformSelection(ask) {
|
|
|
572
817
|
);
|
|
573
818
|
}
|
|
574
819
|
process.stderr.write(
|
|
575
|
-
style.dim(" Pick
|
|
820
|
+
style.dim(" Pick 8 if you want to wrap a local MCP server with multicorn-proxy --wrap.") + "\n"
|
|
576
821
|
);
|
|
577
822
|
let selection = 0;
|
|
578
823
|
while (selection === 0) {
|
|
579
|
-
const input = await ask("Select (1-
|
|
824
|
+
const input = await ask("Select (1-8): ");
|
|
580
825
|
const num = parseInt(input.trim(), 10);
|
|
581
|
-
if (num >= 1 && num <=
|
|
826
|
+
if (num >= 1 && num <= 8) {
|
|
582
827
|
selection = num;
|
|
583
828
|
}
|
|
584
829
|
}
|
|
@@ -587,10 +832,10 @@ async function promptPlatformSelection(ask) {
|
|
|
587
832
|
async function promptWindsurfIntegrationMode(ask) {
|
|
588
833
|
process.stderr.write("\n" + style.bold("Windsurf integration") + "\n");
|
|
589
834
|
process.stderr.write(
|
|
590
|
-
" " + style.violet("1") + ". Native plugin (recommended)
|
|
835
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Cascade Hooks see every file, terminal, and MCP action\n"
|
|
591
836
|
);
|
|
592
837
|
process.stderr.write(
|
|
593
|
-
" " + style.violet("2") + ". Hosted proxy
|
|
838
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into mcp_config)\n"
|
|
594
839
|
);
|
|
595
840
|
let choice = 0;
|
|
596
841
|
while (choice === 0) {
|
|
@@ -699,9 +944,9 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
|
|
|
699
944
|
return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
|
|
700
945
|
}
|
|
701
946
|
function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
702
|
-
const usesInlineKey = platform === "cursor" || platform === "windsurf" || platform === "cline";
|
|
947
|
+
const usesInlineKey = platform === "cursor" || platform === "claude-desktop" || platform === "windsurf" || platform === "cline" || platform === "gemini-cli";
|
|
703
948
|
const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
704
|
-
const urlKey = platform === "windsurf" ? "serverUrl" : "url";
|
|
949
|
+
const urlKey = platform === "windsurf" ? "serverUrl" : platform === "gemini-cli" ? "httpUrl" : "url";
|
|
705
950
|
const mcpSnippet = JSON.stringify(
|
|
706
951
|
{
|
|
707
952
|
mcpServers: {
|
|
@@ -720,6 +965,8 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
720
965
|
process.stderr.write("\n" + style.dim("Add this to your OpenClaw agent config:") + "\n\n");
|
|
721
966
|
} else if (platform === "claude-code") {
|
|
722
967
|
process.stderr.write("\n" + style.dim("Add this to your Claude Code MCP config:") + "\n\n");
|
|
968
|
+
} else if (platform === "claude-desktop") {
|
|
969
|
+
process.stderr.write("\n" + style.dim(`Add this to ${getClaudeDesktopConfigPath()}:`) + "\n\n");
|
|
723
970
|
} else if (platform === "windsurf") {
|
|
724
971
|
process.stderr.write(
|
|
725
972
|
"\n" + style.dim("Add this to ~/.codeium/windsurf/mcp_config.json:") + "\n\n"
|
|
@@ -741,6 +988,12 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
741
988
|
" Linux: ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
|
|
742
989
|
) + "\n\n"
|
|
743
990
|
);
|
|
991
|
+
} else if (platform === "gemini-cli") {
|
|
992
|
+
process.stderr.write(
|
|
993
|
+
"\n" + style.dim(
|
|
994
|
+
"Add this to ~/.gemini/settings.json (create the file if it does not exist). For project-specific config, use .gemini/settings.json in your project root. Restart Gemini CLI after saving. Run /mcp to verify the server is connected."
|
|
995
|
+
) + "\n\n"
|
|
996
|
+
);
|
|
744
997
|
} else {
|
|
745
998
|
process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
|
|
746
999
|
}
|
|
@@ -764,6 +1017,9 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
764
1017
|
) + "\n"
|
|
765
1018
|
);
|
|
766
1019
|
}
|
|
1020
|
+
if (platform === "claude-desktop") {
|
|
1021
|
+
process.stderr.write(style.dim("Then restart Claude Desktop to load the MCP server.") + "\n");
|
|
1022
|
+
}
|
|
767
1023
|
if (platform === "cline") {
|
|
768
1024
|
process.stderr.write(
|
|
769
1025
|
style.dim(
|
|
@@ -865,10 +1121,11 @@ async function runInit(explicitBaseUrl) {
|
|
|
865
1121
|
};
|
|
866
1122
|
let configuring = true;
|
|
867
1123
|
while (configuring) {
|
|
1124
|
+
let postSaveNativeSkipNote = null;
|
|
868
1125
|
const selection = await promptPlatformSelection(ask);
|
|
869
1126
|
const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
|
|
870
1127
|
const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
|
|
871
|
-
if (selection ===
|
|
1128
|
+
if (selection === 8) {
|
|
872
1129
|
const raw = existing !== null ? { ...existing } : {};
|
|
873
1130
|
raw["apiKey"] = apiKey;
|
|
874
1131
|
raw["baseUrl"] = resolvedBaseUrl;
|
|
@@ -1047,8 +1304,22 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1047
1304
|
});
|
|
1048
1305
|
setupSucceeded = true;
|
|
1049
1306
|
} catch (error) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1307
|
+
if (error instanceof NativePluginPrerequisiteMissingError) {
|
|
1308
|
+
postSaveNativeSkipNote = nativePluginSkippedSaveNote(
|
|
1309
|
+
"npx multicorn-proxy init",
|
|
1310
|
+
"Windsurf"
|
|
1311
|
+
);
|
|
1312
|
+
configuredAgents.push({
|
|
1313
|
+
selection,
|
|
1314
|
+
platform: selectedPlatform,
|
|
1315
|
+
platformLabel: selectedLabel,
|
|
1316
|
+
agentName
|
|
1317
|
+
});
|
|
1318
|
+
setupSucceeded = true;
|
|
1319
|
+
} else {
|
|
1320
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1321
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1322
|
+
}
|
|
1052
1323
|
}
|
|
1053
1324
|
} else {
|
|
1054
1325
|
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
@@ -1092,6 +1363,91 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1092
1363
|
setupSucceeded = true;
|
|
1093
1364
|
}
|
|
1094
1365
|
}
|
|
1366
|
+
} else if (selection === 7) {
|
|
1367
|
+
const geminiMode = await promptGeminiCliIntegrationMode(ask);
|
|
1368
|
+
if (geminiMode === "native") {
|
|
1369
|
+
try {
|
|
1370
|
+
await installGeminiCliNativeHooks(ask);
|
|
1371
|
+
process.stderr.write("\n" + style.bold("Shield Gemini CLI hooks installed") + "\n\n");
|
|
1372
|
+
process.stderr.write(
|
|
1373
|
+
style.dim("Hook scripts: ") + style.cyan(getGeminiCliHooksInstallDir()) + "\n"
|
|
1374
|
+
);
|
|
1375
|
+
process.stderr.write(
|
|
1376
|
+
style.dim("Settings updated at ") + style.cyan("~/.gemini/settings.json") + "\n"
|
|
1377
|
+
);
|
|
1378
|
+
process.stderr.write(
|
|
1379
|
+
style.dim(
|
|
1380
|
+
"The Shield hook runs with your user permissions. It intercepts Gemini CLI tool calls to check permissions and log activity. Review the scripts if that is a concern."
|
|
1381
|
+
) + "\n"
|
|
1382
|
+
);
|
|
1383
|
+
configuredAgents.push({
|
|
1384
|
+
selection,
|
|
1385
|
+
platform: selectedPlatform,
|
|
1386
|
+
platformLabel: selectedLabel,
|
|
1387
|
+
agentName,
|
|
1388
|
+
geminiCliIntegration: "native"
|
|
1389
|
+
});
|
|
1390
|
+
setupSucceeded = true;
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
if (error instanceof NativePluginPrerequisiteMissingError) {
|
|
1393
|
+
postSaveNativeSkipNote = nativePluginSkippedSaveNote(
|
|
1394
|
+
"npx multicorn-shield init",
|
|
1395
|
+
"Gemini CLI"
|
|
1396
|
+
);
|
|
1397
|
+
configuredAgents.push({
|
|
1398
|
+
selection,
|
|
1399
|
+
platform: selectedPlatform,
|
|
1400
|
+
platformLabel: selectedLabel,
|
|
1401
|
+
agentName
|
|
1402
|
+
});
|
|
1403
|
+
setupSucceeded = true;
|
|
1404
|
+
} else {
|
|
1405
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1406
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
1411
|
+
let proxyUrl = "";
|
|
1412
|
+
let created = false;
|
|
1413
|
+
while (!created) {
|
|
1414
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
1415
|
+
try {
|
|
1416
|
+
proxyUrl = await createProxyConfig(
|
|
1417
|
+
resolvedBaseUrl,
|
|
1418
|
+
apiKey,
|
|
1419
|
+
agentName,
|
|
1420
|
+
targetUrl,
|
|
1421
|
+
shortName,
|
|
1422
|
+
selectedPlatform
|
|
1423
|
+
);
|
|
1424
|
+
spinner.stop(true, "Proxy config created!");
|
|
1425
|
+
created = true;
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1428
|
+
spinner.stop(false, detail);
|
|
1429
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
1430
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (created && proxyUrl.length > 0) {
|
|
1436
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
1437
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
1438
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
|
|
1439
|
+
configuredAgents.push({
|
|
1440
|
+
selection,
|
|
1441
|
+
platform: selectedPlatform,
|
|
1442
|
+
platformLabel: selectedLabel,
|
|
1443
|
+
agentName,
|
|
1444
|
+
shortName,
|
|
1445
|
+
proxyUrl,
|
|
1446
|
+
geminiCliIntegration: "hosted"
|
|
1447
|
+
});
|
|
1448
|
+
setupSucceeded = true;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1095
1451
|
} else if (selection === 5) {
|
|
1096
1452
|
const clineMode = await promptClineIntegrationMode(ask);
|
|
1097
1453
|
if (clineMode === "native") {
|
|
@@ -1112,8 +1468,22 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1112
1468
|
});
|
|
1113
1469
|
setupSucceeded = true;
|
|
1114
1470
|
} catch (error) {
|
|
1115
|
-
|
|
1116
|
-
|
|
1471
|
+
if (error instanceof NativePluginPrerequisiteMissingError) {
|
|
1472
|
+
postSaveNativeSkipNote = nativePluginSkippedSaveNote(
|
|
1473
|
+
"npx multicorn-shield init",
|
|
1474
|
+
"Cline"
|
|
1475
|
+
);
|
|
1476
|
+
configuredAgents.push({
|
|
1477
|
+
selection,
|
|
1478
|
+
platform: selectedPlatform,
|
|
1479
|
+
platformLabel: selectedLabel,
|
|
1480
|
+
agentName
|
|
1481
|
+
});
|
|
1482
|
+
setupSucceeded = true;
|
|
1483
|
+
} else {
|
|
1484
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1485
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1486
|
+
}
|
|
1117
1487
|
}
|
|
1118
1488
|
} else {
|
|
1119
1489
|
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
@@ -1215,9 +1585,14 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1215
1585
|
style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
|
|
1216
1586
|
`
|
|
1217
1587
|
);
|
|
1588
|
+
if (postSaveNativeSkipNote != null) {
|
|
1589
|
+
process.stderr.write(postSaveNativeSkipNote);
|
|
1590
|
+
postSaveNativeSkipNote = null;
|
|
1591
|
+
}
|
|
1218
1592
|
} catch (error) {
|
|
1219
1593
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1220
1594
|
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
1595
|
+
postSaveNativeSkipNote = null;
|
|
1221
1596
|
}
|
|
1222
1597
|
}
|
|
1223
1598
|
const another = await ask("\nConnect another agent? (Y/n) ");
|
|
@@ -1291,6 +1666,22 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1291
1666
|
"\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
|
|
1292
1667
|
);
|
|
1293
1668
|
}
|
|
1669
|
+
const geminiCliNativeConfigured = configuredAgents.some(
|
|
1670
|
+
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "native"
|
|
1671
|
+
);
|
|
1672
|
+
const geminiCliHostedConfigured = configuredAgents.some(
|
|
1673
|
+
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "hosted"
|
|
1674
|
+
);
|
|
1675
|
+
if (geminiCliNativeConfigured) {
|
|
1676
|
+
blocks.push(
|
|
1677
|
+
"\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
if (geminiCliHostedConfigured) {
|
|
1681
|
+
blocks.push(
|
|
1682
|
+
"\n" + style.bold("To complete your Gemini CLI setup:") + "\n 1. Open " + style.cyan("~/.gemini/settings.json") + "\n 2. Paste the config snippet shown above\n 3. Restart Gemini CLI, then run /mcp to verify\n"
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1294
1685
|
if (blocks.length > 0) {
|
|
1295
1686
|
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
1296
1687
|
process.stderr.write(blocks.join("") + "\n");
|
package/dist/shield-extension.js
CHANGED
|
@@ -22359,7 +22359,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22359
22359
|
|
|
22360
22360
|
// package.json
|
|
22361
22361
|
var package_default = {
|
|
22362
|
-
version: "0.
|
|
22362
|
+
version: "0.13.0"};
|
|
22363
22363
|
|
|
22364
22364
|
// src/package-meta.ts
|
|
22365
22365
|
var PACKAGE_VERSION = package_default.version;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multicorn-shield",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Multicorn AI Pty Ltd",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"dist",
|
|
39
39
|
"plugins/windsurf",
|
|
40
40
|
"plugins/cline",
|
|
41
|
+
"plugins/gemini-cli",
|
|
41
42
|
"LICENSE",
|
|
42
43
|
"README.md",
|
|
43
44
|
"CHANGELOG.md"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
|
|
3
|
+
/**
|
|
4
|
+
* Gemini CLI AfterTool hook: audit logging to Shield.
|
|
5
|
+
* Always returns { "decision": "allow" } on stdout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
loadConfig,
|
|
12
|
+
logPrefix,
|
|
13
|
+
mapToolName,
|
|
14
|
+
postJson,
|
|
15
|
+
readStdin,
|
|
16
|
+
scrubParameters,
|
|
17
|
+
scrubResultForMetadata,
|
|
18
|
+
} = require("./shared.cjs");
|
|
19
|
+
|
|
20
|
+
const HOOK_PREFIX = logPrefix("after-tool");
|
|
21
|
+
|
|
22
|
+
function respond() {
|
|
23
|
+
process.stdout.write(JSON.stringify({ decision: "allow" }) + "\n");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = await readStdin();
|
|
31
|
+
} catch {
|
|
32
|
+
respond();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @type {Record<string, unknown>} */
|
|
37
|
+
let hookPayload;
|
|
38
|
+
try {
|
|
39
|
+
hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
40
|
+
} catch {
|
|
41
|
+
respond();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const toolName = typeof hookPayload.tool_name === "string" ? hookPayload.tool_name : "";
|
|
46
|
+
const mapped = mapToolName(toolName);
|
|
47
|
+
|
|
48
|
+
if (mapped === null) {
|
|
49
|
+
respond();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const { service, actionType } = mapped;
|
|
53
|
+
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
56
|
+
respond();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const toolInput =
|
|
61
|
+
typeof hookPayload.tool_input === "object" && hookPayload.tool_input !== null
|
|
62
|
+
? /** @type {Record<string, unknown>} */ (hookPayload.tool_input)
|
|
63
|
+
: {};
|
|
64
|
+
|
|
65
|
+
const paramsSerialized = scrubParameters(toolInput);
|
|
66
|
+
const toolResponse = hookPayload.tool_response;
|
|
67
|
+
|
|
68
|
+
/** @type {Record<string, unknown>} */
|
|
69
|
+
const metadata = {
|
|
70
|
+
tool_name: toolName,
|
|
71
|
+
session_id: typeof hookPayload.session_id === "string" ? hookPayload.session_id : "",
|
|
72
|
+
cwd: typeof hookPayload.cwd === "string" ? hookPayload.cwd : "",
|
|
73
|
+
parameters: paramsSerialized,
|
|
74
|
+
result: scrubResultForMetadata(toolResponse),
|
|
75
|
+
source: "gemini-cli",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** @type {Record<string, unknown>} */
|
|
79
|
+
const payload = {
|
|
80
|
+
agent: config.agentName,
|
|
81
|
+
service,
|
|
82
|
+
actionType,
|
|
83
|
+
status: "approved",
|
|
84
|
+
metadata,
|
|
85
|
+
platform: "gemini-cli",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const res = await postJson(config.baseUrl, config.apiKey, payload);
|
|
90
|
+
const code = res.statusCode ?? 0;
|
|
91
|
+
if (code < 200 || code >= 300) {
|
|
92
|
+
throw new Error(`HTTP ${String(code)}`);
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
`${HOOK_PREFIX} Warning: failed to log action to Shield audit trail. Detail: ${msg}\n`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
respond();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main().catch((e) => {
|
|
105
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
`${HOOK_PREFIX} Warning: failed to log action to Shield audit trail. Detail: ${msg}\n`,
|
|
108
|
+
);
|
|
109
|
+
respond();
|
|
110
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
|
|
3
|
+
/**
|
|
4
|
+
* Gemini CLI BeforeTool hook: asks Shield whether a tool call is allowed.
|
|
5
|
+
* Reads JSON from stdin. Writes JSON to stdout only (decision allow/deny). Logs to stderr.
|
|
6
|
+
* Fail-open on missing config or unreachable API.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
loadConfig,
|
|
13
|
+
logPrefix,
|
|
14
|
+
mapToolName,
|
|
15
|
+
postJson,
|
|
16
|
+
readStdin,
|
|
17
|
+
safeJsonParse,
|
|
18
|
+
scrubParameters,
|
|
19
|
+
unwrapData,
|
|
20
|
+
consentUrl,
|
|
21
|
+
openBrowser,
|
|
22
|
+
} = require("./shared.cjs");
|
|
23
|
+
|
|
24
|
+
const HOOK_PREFIX = logPrefix("before-tool");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} decision
|
|
28
|
+
* @param {string} [reason]
|
|
29
|
+
*/
|
|
30
|
+
function respond(decision, reason) {
|
|
31
|
+
/** @type {Record<string, unknown>} */
|
|
32
|
+
const out = { decision };
|
|
33
|
+
if (reason !== undefined && reason.length > 0) {
|
|
34
|
+
out.reason = reason;
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dashboardHintUrl(apiBaseUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = String(apiBaseUrl).replace(/\/+$/, "");
|
|
43
|
+
const lower = raw.toLowerCase();
|
|
44
|
+
if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
|
|
45
|
+
return "http://localhost:5173/approvals";
|
|
46
|
+
}
|
|
47
|
+
const u = new URL(raw);
|
|
48
|
+
if (u.hostname.startsWith("api.")) {
|
|
49
|
+
u.hostname = "app." + u.hostname.slice(4);
|
|
50
|
+
}
|
|
51
|
+
return `${u.origin}/approvals`;
|
|
52
|
+
} catch {
|
|
53
|
+
return "https://app.multicorn.ai/approvals";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} data
|
|
59
|
+
* @param {string} approvalsUrl
|
|
60
|
+
*/
|
|
61
|
+
function blockedReason(data, approvalsUrl) {
|
|
62
|
+
if (data !== null && typeof data === "object") {
|
|
63
|
+
const d = /** @type {Record<string, unknown>} */ (data);
|
|
64
|
+
const meta = d.metadata;
|
|
65
|
+
if (typeof meta === "string" && meta.length > 0) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(meta);
|
|
68
|
+
if (parsed !== null && typeof parsed === "object" && "block_reason" in parsed) {
|
|
69
|
+
const br = /** @type {Record<string, unknown>} */ (parsed).block_reason;
|
|
70
|
+
if (typeof br === "string" && br.length > 0) {
|
|
71
|
+
return `Blocked by Multicorn Shield: ${br}. Grant access at ${approvalsUrl}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return `Blocked by Multicorn Shield. Grant access at ${approvalsUrl}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await readStdin();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
88
|
+
process.stderr.write(`${HOOK_PREFIX} could not read stdin (${msg}). Allowing.\n`);
|
|
89
|
+
respond("allow");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @type {Record<string, unknown>} */
|
|
94
|
+
let hookPayload;
|
|
95
|
+
try {
|
|
96
|
+
hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
99
|
+
process.stderr.write(`${HOOK_PREFIX} invalid JSON (${msg}). Allowing.\n`);
|
|
100
|
+
respond("allow");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const toolName = typeof hookPayload.tool_name === "string" ? hookPayload.tool_name : "";
|
|
105
|
+
|
|
106
|
+
const mapped = mapToolName(toolName);
|
|
107
|
+
if (mapped === null) {
|
|
108
|
+
respond("allow");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const { service, actionType } = mapped;
|
|
112
|
+
|
|
113
|
+
const toolInput =
|
|
114
|
+
typeof hookPayload.tool_input === "object" && hookPayload.tool_input !== null
|
|
115
|
+
? /** @type {Record<string, unknown>} */ (hookPayload.tool_input)
|
|
116
|
+
: {};
|
|
117
|
+
|
|
118
|
+
const config = loadConfig();
|
|
119
|
+
if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
120
|
+
respond("allow");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const paramsSerialized = scrubParameters(toolInput);
|
|
125
|
+
const approvalsUrl = dashboardHintUrl(config.baseUrl);
|
|
126
|
+
|
|
127
|
+
/** @type {Record<string, unknown>} */
|
|
128
|
+
const metadata = {
|
|
129
|
+
tool_name: toolName,
|
|
130
|
+
session_id: typeof hookPayload.session_id === "string" ? hookPayload.session_id : "",
|
|
131
|
+
cwd: typeof hookPayload.cwd === "string" ? hookPayload.cwd : "",
|
|
132
|
+
parameters: paramsSerialized,
|
|
133
|
+
source: "gemini-cli",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** @type {Record<string, unknown>} */
|
|
137
|
+
const payload = {
|
|
138
|
+
agent: config.agentName,
|
|
139
|
+
service,
|
|
140
|
+
actionType,
|
|
141
|
+
status: "pending",
|
|
142
|
+
metadata,
|
|
143
|
+
platform: "gemini-cli",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let statusCode;
|
|
147
|
+
let bodyText;
|
|
148
|
+
try {
|
|
149
|
+
const res = await postJson(config.baseUrl, config.apiKey, payload);
|
|
150
|
+
statusCode = res.statusCode;
|
|
151
|
+
bodyText = res.bodyText;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
154
|
+
process.stderr.write(`${HOOK_PREFIX} Shield API unreachable (${msg}). Allowing.\n`);
|
|
155
|
+
respond("allow");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parsed = safeJsonParse(bodyText);
|
|
160
|
+
const data = unwrapData(parsed);
|
|
161
|
+
|
|
162
|
+
if (statusCode === 202) {
|
|
163
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
164
|
+
openBrowser(url);
|
|
165
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (statusCode === 201) {
|
|
170
|
+
if (data === null || typeof data !== "object") {
|
|
171
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
172
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const st = String(/** @type {Record<string, unknown>} */ (data).status || "").toLowerCase();
|
|
176
|
+
if (st === "approved") {
|
|
177
|
+
respond("allow");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (st === "blocked") {
|
|
181
|
+
respond("deny", blockedReason(data, approvalsUrl));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
185
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
|
|
190
|
+
respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch((e) => {
|
|
194
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
195
|
+
process.stderr.write(`${HOOK_PREFIX} unexpected error (${msg}). Allowing.\n`);
|
|
196
|
+
respond("allow");
|
|
197
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Shared helpers for Gemini CLI BeforeTool / AfterTool Shield hooks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const http = require("node:http");
|
|
11
|
+
const https = require("node:https");
|
|
12
|
+
const os = require("node:os");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
const AUTH_HEADER = "X-Multicorn-Key";
|
|
16
|
+
const HTTP_REQUEST_TIMEOUT_MS = 10000;
|
|
17
|
+
|
|
18
|
+
/** Tools that should pass through without calling Shield (internal / UX-only). */
|
|
19
|
+
const SKIP_TOOLS = new Set([
|
|
20
|
+
"save_memory",
|
|
21
|
+
"activate_skill",
|
|
22
|
+
"get_internal_docs",
|
|
23
|
+
"ask_user",
|
|
24
|
+
"write_todos",
|
|
25
|
+
"enter_plan_mode",
|
|
26
|
+
"exit_plan_mode",
|
|
27
|
+
"update_topic",
|
|
28
|
+
"complete_task",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** @type {Readonly<Record<string, { service: string; actionType: string }>>} */
|
|
32
|
+
const TOOL_MAP = {
|
|
33
|
+
read_file: { service: "filesystem", actionType: "read" },
|
|
34
|
+
read_many_files: { service: "filesystem", actionType: "read" },
|
|
35
|
+
list_directory: { service: "filesystem", actionType: "read" },
|
|
36
|
+
glob: { service: "filesystem", actionType: "read" },
|
|
37
|
+
grep_search: { service: "filesystem", actionType: "read" },
|
|
38
|
+
write_file: { service: "filesystem", actionType: "write" },
|
|
39
|
+
replace: { service: "filesystem", actionType: "write" },
|
|
40
|
+
run_shell_command: { service: "terminal", actionType: "execute" },
|
|
41
|
+
google_web_search: { service: "browser", actionType: "execute" },
|
|
42
|
+
web_fetch: { service: "browser", actionType: "execute" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function logPrefix(label) {
|
|
46
|
+
return `[multicorn-shield] Gemini CLI ${label}:`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readStdin() {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
process.stdin.setEncoding("utf8");
|
|
53
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
54
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
55
|
+
process.stdin.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveGeminiCliAgentName(obj) {
|
|
60
|
+
const agents = obj.agents;
|
|
61
|
+
if (Array.isArray(agents)) {
|
|
62
|
+
for (const entry of agents) {
|
|
63
|
+
if (
|
|
64
|
+
entry &&
|
|
65
|
+
typeof entry === "object" &&
|
|
66
|
+
/** @type {{ platform?: string; name?: string }} */ (entry).platform === "gemini-cli" &&
|
|
67
|
+
typeof (/** @type {{ platform?: string; name?: string }} */ (entry).name) === "string"
|
|
68
|
+
) {
|
|
69
|
+
return /** @type {{ name: string }} */ (entry).name;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return typeof obj.agentName === "string" ? obj.agentName : "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function loadConfig() {
|
|
77
|
+
try {
|
|
78
|
+
const configPath = path.join(os.homedir(), ".multicorn", "config.json");
|
|
79
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
80
|
+
const obj = JSON.parse(raw);
|
|
81
|
+
const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
|
|
82
|
+
const baseUrl =
|
|
83
|
+
typeof obj.baseUrl === "string" && obj.baseUrl.length > 0
|
|
84
|
+
? obj.baseUrl.replace(/\/+$/, "")
|
|
85
|
+
: "https://api.multicorn.ai";
|
|
86
|
+
const baseLower = baseUrl.toLowerCase();
|
|
87
|
+
const isHttps = baseLower.startsWith("https://");
|
|
88
|
+
const isLocal = baseLower.includes("localhost") || baseLower.includes("127.0.0.1");
|
|
89
|
+
if (!isHttps && !isLocal) {
|
|
90
|
+
process.stderr.write(
|
|
91
|
+
`${logPrefix("config")} baseUrl must use HTTPS for non-local servers. Fail-open: Shield disabled.\n`,
|
|
92
|
+
);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const agentName = resolveGeminiCliAgentName(obj);
|
|
96
|
+
return { apiKey, baseUrl, agentName };
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} toolName
|
|
104
|
+
* @returns {{ service: string; actionType: string } | null} null = skip hook API calls
|
|
105
|
+
*/
|
|
106
|
+
function mapToolName(toolName) {
|
|
107
|
+
const name = String(toolName || "").trim();
|
|
108
|
+
if (name.length === 0) return null;
|
|
109
|
+
if (SKIP_TOOLS.has(name)) return null;
|
|
110
|
+
|
|
111
|
+
if (name.startsWith("mcp_")) {
|
|
112
|
+
const rest = name.slice(4);
|
|
113
|
+
const idx = rest.indexOf("_");
|
|
114
|
+
if (idx <= 0) {
|
|
115
|
+
const safe = rest.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
116
|
+
return { service: `mcp:${safe}`, actionType: "execute" };
|
|
117
|
+
}
|
|
118
|
+
const server = rest.slice(0, idx).replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
119
|
+
const tool = rest.slice(idx + 1).replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
120
|
+
return { service: `mcp:${server}.${tool}`, actionType: "execute" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mapped = TOOL_MAP[name];
|
|
124
|
+
if (mapped !== undefined) {
|
|
125
|
+
return mapped;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { service: "unknown", actionType: "execute" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function postJson(baseUrl, apiKey, bodyObj) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
let u;
|
|
134
|
+
try {
|
|
135
|
+
const root = String(baseUrl).replace(/\/+$/, "");
|
|
136
|
+
u = new URL(`${root}/api/v1/actions`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
reject(e);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const payload = JSON.stringify(bodyObj);
|
|
142
|
+
const isHttps = u.protocol === "https:";
|
|
143
|
+
const lib = isHttps ? https : http;
|
|
144
|
+
const port = u.port || (isHttps ? 443 : 80);
|
|
145
|
+
const hostname = u.hostname;
|
|
146
|
+
/** @type {string} */
|
|
147
|
+
const pathnamePlusSearch = u.pathname + u.search;
|
|
148
|
+
const options = {
|
|
149
|
+
hostname,
|
|
150
|
+
port,
|
|
151
|
+
path: pathnamePlusSearch,
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
Connection: "close",
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
"Content-Length": Buffer.byteLength(payload, "utf8"),
|
|
157
|
+
[AUTH_HEADER]: apiKey,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const req = lib.request(options, (res) => {
|
|
161
|
+
const chunks = [];
|
|
162
|
+
res.on("data", (c) => chunks.push(c));
|
|
163
|
+
res.on("end", () => {
|
|
164
|
+
resolve({
|
|
165
|
+
statusCode: res.statusCode ?? 0,
|
|
166
|
+
bodyText: Buffer.concat(chunks).toString("utf8"),
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
req.setTimeout(HTTP_REQUEST_TIMEOUT_MS, () => {
|
|
171
|
+
req.destroy(new Error("request timeout"));
|
|
172
|
+
});
|
|
173
|
+
req.on("error", reject);
|
|
174
|
+
req.write(payload);
|
|
175
|
+
req.end();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safeJsonParse(text) {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(text);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function unwrapData(body) {
|
|
188
|
+
if (typeof body !== "object" || body === null) return null;
|
|
189
|
+
const o = /** @type {Record<string, unknown>} */ (body);
|
|
190
|
+
return o.success === true ? o.data : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function applyParameterScrub(parameters) {
|
|
194
|
+
const scrubbedParams = { ...parameters };
|
|
195
|
+
if (typeof scrubbedParams.content === "string") {
|
|
196
|
+
scrubbedParams.content = `[${scrubbedParams.content.length} chars redacted]`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof scrubbedParams.command === "string" && scrubbedParams.command.length > 200) {
|
|
199
|
+
scrubbedParams.command = scrubbedParams.command.slice(0, 200) + "... [truncated]";
|
|
200
|
+
}
|
|
201
|
+
return scrubbedParams;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function scrubParameters(parameters, maxLen = 4096) {
|
|
205
|
+
/** @type {Record<string, unknown>} */
|
|
206
|
+
let base = {};
|
|
207
|
+
if (typeof parameters === "object" && parameters !== null) {
|
|
208
|
+
base = { .../** @type {Record<string, unknown>} */ (parameters) };
|
|
209
|
+
} else if (typeof parameters === "string") {
|
|
210
|
+
try {
|
|
211
|
+
const p = JSON.parse(parameters);
|
|
212
|
+
if (p !== null && typeof p === "object") {
|
|
213
|
+
base = { .../** @type {Record<string, unknown>} */ (p) };
|
|
214
|
+
} else {
|
|
215
|
+
base = { raw: parameters };
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
base = { raw: parameters };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const scrubbed = applyParameterScrub(base);
|
|
223
|
+
let paramsSerialized;
|
|
224
|
+
try {
|
|
225
|
+
paramsSerialized = JSON.stringify(scrubbed);
|
|
226
|
+
} catch {
|
|
227
|
+
paramsSerialized = "{}";
|
|
228
|
+
}
|
|
229
|
+
if (paramsSerialized.length > maxLen) {
|
|
230
|
+
paramsSerialized = paramsSerialized.slice(0, maxLen);
|
|
231
|
+
}
|
|
232
|
+
return paramsSerialized;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function scrubResultForMetadata(result) {
|
|
236
|
+
if (result === null || result === undefined) return "";
|
|
237
|
+
let s;
|
|
238
|
+
try {
|
|
239
|
+
s = typeof result === "string" ? result : JSON.stringify(result);
|
|
240
|
+
} catch {
|
|
241
|
+
s = String(result);
|
|
242
|
+
}
|
|
243
|
+
s = s.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
|
|
244
|
+
s = s.replace(/\bmcs_[A-Za-z0-9_-]+\b/g, "[REDACTED]");
|
|
245
|
+
s = s.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "[REDACTED]");
|
|
246
|
+
s = s.replace(/Bearer\s+[^\s]+/gi, "[REDACTED]");
|
|
247
|
+
s = s.replace(/\b(password|token)\s*[:=]\s*[^\s]+\b/gi, "[REDACTED]");
|
|
248
|
+
if (s.length > 500) {
|
|
249
|
+
s = s.slice(0, 500) + "[truncated]";
|
|
250
|
+
}
|
|
251
|
+
return s;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} apiBaseUrl
|
|
256
|
+
* @param {string} agentName
|
|
257
|
+
* @param {string} service
|
|
258
|
+
* @param {string} actionType
|
|
259
|
+
*/
|
|
260
|
+
function consentUrl(apiBaseUrl, agentName, service, actionType) {
|
|
261
|
+
let origin = "https://app.multicorn.ai";
|
|
262
|
+
try {
|
|
263
|
+
const raw = String(apiBaseUrl).replace(/\/+$/, "");
|
|
264
|
+
const lower = raw.toLowerCase();
|
|
265
|
+
if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
|
|
266
|
+
origin = "http://localhost:5173";
|
|
267
|
+
} else {
|
|
268
|
+
const u = new URL(raw);
|
|
269
|
+
if (u.hostname.startsWith("api.")) {
|
|
270
|
+
u.hostname = "app." + u.hostname.slice(4);
|
|
271
|
+
}
|
|
272
|
+
origin = u.origin;
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
/* keep default */
|
|
276
|
+
}
|
|
277
|
+
const params = new URLSearchParams();
|
|
278
|
+
params.set("agent", agentName);
|
|
279
|
+
params.set("scopes", `${service}:${actionType}`);
|
|
280
|
+
params.set("platform", "gemini-cli");
|
|
281
|
+
return `${origin}/consent?${params.toString()}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @param {string} url */
|
|
285
|
+
function openBrowser(url) {
|
|
286
|
+
try {
|
|
287
|
+
const { execFileSync } = require("node:child_process");
|
|
288
|
+
if (process.platform === "darwin") {
|
|
289
|
+
execFileSync("open", [url], { stdio: "ignore" });
|
|
290
|
+
} else if (process.platform === "win32") {
|
|
291
|
+
execFileSync("cmd.exe", ["/c", "start", "", url], {
|
|
292
|
+
stdio: "ignore",
|
|
293
|
+
windowsHide: true,
|
|
294
|
+
});
|
|
295
|
+
} else {
|
|
296
|
+
execFileSync("xdg-open", [url], { stdio: "ignore" });
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
/* ignore */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
AUTH_HEADER,
|
|
305
|
+
logPrefix,
|
|
306
|
+
HTTP_REQUEST_TIMEOUT_MS,
|
|
307
|
+
TOOL_MAP,
|
|
308
|
+
readStdin,
|
|
309
|
+
loadConfig,
|
|
310
|
+
resolveGeminiCliAgentName,
|
|
311
|
+
mapToolName,
|
|
312
|
+
postJson,
|
|
313
|
+
safeJsonParse,
|
|
314
|
+
unwrapData,
|
|
315
|
+
scrubParameters,
|
|
316
|
+
scrubResultForMetadata,
|
|
317
|
+
consentUrl,
|
|
318
|
+
openBrowser,
|
|
319
|
+
};
|