multicorn-shield 1.11.1 → 1.12.1

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Windsurf Cascade pre-hook: permission check before read, write, terminal, or MCP tool use.
3
3
  * Routes by stdin JSON field agent_action_name (see Windsurf Cascade Hooks docs).
4
- * Fail-closed on API errors once config is loaded. Fail-open if Shield is not configured.
4
+ * Fail-closed on API errors once config is loaded or Windsurf hooks are installed.
5
+ * Fail-open only when Shield is not configured (no config file and no installed hook copy).
5
6
  */
6
7
 
7
8
  "use strict";
@@ -110,12 +111,83 @@ function resolveWindsurfAgentName(obj) {
110
111
  return pickAgentNameForPlatform(obj, "windsurf", process.cwd());
111
112
  }
112
113
 
114
+ /**
115
+ * @returns {string}
116
+ */
117
+ function multicornConfigPath() {
118
+ return path.join(os.homedir(), ".multicorn", "config.json");
119
+ }
120
+
121
+ /**
122
+ * @returns {boolean}
123
+ */
124
+ function configFileExists() {
125
+ try {
126
+ fs.accessSync(multicornConfigPath());
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @returns {boolean}
135
+ */
136
+ function isShieldWindsurfHooksInstalled() {
137
+ try {
138
+ fs.accessSync(path.join(os.homedir(), ".multicorn", "windsurf-hooks", "pre-action.cjs"));
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @returns {boolean}
147
+ */
148
+ function shouldEnforceGovernance() {
149
+ return configFileExists() || isShieldWindsurfHooksInstalled();
150
+ }
151
+
152
+ /**
153
+ * @param {string} baseUrl
154
+ * @returns {string}
155
+ */
156
+ function apiHostFromBaseUrl(baseUrl) {
157
+ try {
158
+ return new URL(baseUrl).host;
159
+ } catch {
160
+ return "unknown";
161
+ }
162
+ }
163
+
164
+ /**
165
+ * @param {string} agentActionName
166
+ * @param {string} agentName
167
+ * @param {string} baseUrl
168
+ */
169
+ function logInvocation(agentActionName, agentName, baseUrl) {
170
+ process.stderr.write(
171
+ `${LOG_PREFIX} check event=${agentActionName} agent=${agentName} api=${apiHostFromBaseUrl(baseUrl)}\n`,
172
+ );
173
+ }
174
+
175
+ /**
176
+ * @param {string} cause
177
+ * @param {string} fix
178
+ * @returns {never}
179
+ */
180
+ function blockMisconfigured(cause, fix) {
181
+ process.stderr.write(`${LOG_PREFIX} Action blocked: ${cause}\n ${fix}\n`);
182
+ process.exit(2);
183
+ }
184
+
113
185
  /**
114
186
  * @returns {{ apiKey: string; baseUrl: string; agentName: string } | null}
115
187
  */
116
188
  function loadConfig() {
117
189
  try {
118
- const configPath = path.join(os.homedir(), ".multicorn", "config.json");
190
+ const configPath = multicornConfigPath();
119
191
  const raw = fs.readFileSync(configPath, "utf8");
120
192
  const obj = JSON.parse(raw);
121
193
  const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
@@ -564,16 +636,13 @@ async function main() {
564
636
  try {
565
637
  raw = await readStdin();
566
638
  } catch (e) {
567
- const msg = e instanceof Error ? e.message : String(e);
568
- process.stderr.write(`${LOG_PREFIX} could not read stdin (${msg}). Allowing action.\n`);
569
- process.exit(0);
570
- }
571
-
572
- const config = loadConfig();
573
- if (config === null) {
574
- process.exit(0);
575
- }
576
- if (config.apiKey.length === 0 || config.agentName.length === 0) {
639
+ if (shouldEnforceGovernance()) {
640
+ const msg = e instanceof Error ? e.message : String(e);
641
+ blockMisconfigured(
642
+ `could not read stdin (${msg}), cannot verify permissions.`,
643
+ "Retry the action. If it keeps failing, check Windsurf hook configuration.",
644
+ );
645
+ }
577
646
  process.exit(0);
578
647
  }
579
648
 
@@ -582,8 +651,13 @@ async function main() {
582
651
  try {
583
652
  hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
584
653
  } catch (e) {
585
- const msg = e instanceof Error ? e.message : String(e);
586
- process.stderr.write(`${LOG_PREFIX} invalid JSON (${msg}). Allowing action.\n`);
654
+ if (shouldEnforceGovernance()) {
655
+ const msg = e instanceof Error ? e.message : String(e);
656
+ blockMisconfigured(
657
+ `invalid JSON on stdin (${msg}), cannot verify permissions.`,
658
+ "Retry the action or reinstall hooks with npx multicorn-shield init.",
659
+ );
660
+ }
587
661
  process.exit(0);
588
662
  }
589
663
 
@@ -606,6 +680,32 @@ async function main() {
606
680
  }
607
681
  const { service, actionType } = mapped;
608
682
 
683
+ const config = loadConfig();
684
+ if (config === null) {
685
+ if (shouldEnforceGovernance()) {
686
+ blockMisconfigured(
687
+ "Shield config missing or unreadable, cannot verify permissions.",
688
+ "Run npx multicorn-shield init and complete setup, then check ~/.multicorn/config.json.",
689
+ );
690
+ }
691
+ process.exit(0);
692
+ }
693
+ if (config.apiKey.length === 0 || config.agentName.length === 0) {
694
+ if (shouldEnforceGovernance()) {
695
+ const cause =
696
+ config.apiKey.length === 0
697
+ ? "API key missing from Shield config, cannot verify permissions."
698
+ : "Windsurf agent name missing from Shield config, cannot verify permissions.";
699
+ blockMisconfigured(
700
+ cause,
701
+ "Run npx multicorn-shield init, paste a valid mcs_ key, and configure the Windsurf agent.",
702
+ );
703
+ }
704
+ process.exit(0);
705
+ }
706
+
707
+ logInvocation(agentActionName, config.agentName, config.baseUrl);
708
+
609
709
  let toolInfoSerialized;
610
710
  try {
611
711
  toolInfoSerialized =
@@ -614,10 +714,10 @@ async function main() {
614
714
  : JSON.stringify(toolInfo === undefined ? null : toolInfo);
615
715
  } catch (e) {
616
716
  const msg = e instanceof Error ? e.message : String(e);
617
- process.stderr.write(
618
- `${LOG_PREFIX} could not serialize tool_info (${msg}). Allowing action.\n`,
717
+ blockMisconfigured(
718
+ `could not serialize tool_info (${msg}), cannot verify permissions.`,
719
+ "Retry the action. If it keeps failing, reinstall hooks with npx multicorn-shield init.",
619
720
  );
620
- process.exit(0);
621
721
  }
622
722
 
623
723
  if (typeof toolInfoSerialized === "string" && toolInfoSerialized.length > 4096) {
@@ -718,6 +818,15 @@ async function main() {
718
818
  process.exit(2);
719
819
  }
720
820
 
821
+ if (statusCode === 401 || statusCode === 403) {
822
+ const host = apiHostFromBaseUrl(config.baseUrl);
823
+ process.stderr.write(
824
+ `${LOG_PREFIX} Action blocked: API key not recognized for ${host}.\n` +
825
+ ` Run npx multicorn-shield init and paste a valid key at the prompt.\n`,
826
+ );
827
+ process.exit(2);
828
+ }
829
+
721
830
  const httpDetail = bodyText.length > 300 ? `${bodyText.slice(0, 300)}...` : bodyText;
722
831
  process.stderr.write(
723
832
  `${LOG_PREFIX} Action blocked: Shield returned HTTP ${String(statusCode)}, cannot verify permissions.\n` +