multicorn-shield 0.11.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 +39 -0
- package/dist/badge.js +4 -4
- package/dist/index.cjs +38 -19
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +37 -20
- package/dist/multicorn-proxy.js +578 -15
- package/dist/openclaw-hook/handler.js +0 -1
- package/dist/openclaw-plugin/multicorn-shield.js +11 -17
- package/dist/openclaw-plugin/openclaw.plugin.json +3 -1
- package/dist/proxy.cjs +174 -0
- package/dist/proxy.d.cts +228 -1
- package/dist/proxy.d.ts +228 -1
- package/dist/proxy.js +174 -1
- package/dist/shield-extension.js +1 -4
- package/package.json +4 -2
- package/plugins/cline/README.md +61 -0
- package/plugins/cline/hooks/scripts/post-tool-use.cjs +116 -0
- package/plugins/cline/hooks/scripts/pre-tool-use.cjs +271 -0
- package/plugins/cline/hooks/scripts/shared.cjs +303 -0
- 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/dist/proxy.d.ts
CHANGED
|
@@ -68,6 +68,23 @@ declare const PERMISSION_LEVELS: {
|
|
|
68
68
|
readonly Create: "create";
|
|
69
69
|
};
|
|
70
70
|
type PermissionLevel = (typeof PERMISSION_LEVELS)[keyof typeof PERMISSION_LEVELS];
|
|
71
|
+
/**
|
|
72
|
+
* Lifecycle states for an action processed by the policy engine.
|
|
73
|
+
*
|
|
74
|
+
* - `approved`: action passed policy checks and was executed
|
|
75
|
+
* - `blocked`: action was denied by policy
|
|
76
|
+
* - `pending`: action is awaiting human approval
|
|
77
|
+
* - `flagged`: action was executed but flagged for review
|
|
78
|
+
* - `requires_approval`: action requires content review before execution
|
|
79
|
+
*/
|
|
80
|
+
declare const ACTION_STATUSES: {
|
|
81
|
+
readonly Approved: "approved";
|
|
82
|
+
readonly Blocked: "blocked";
|
|
83
|
+
readonly Pending: "pending";
|
|
84
|
+
readonly Flagged: "flagged";
|
|
85
|
+
readonly RequiresApproval: "requires_approval";
|
|
86
|
+
};
|
|
87
|
+
type ActionStatus = (typeof ACTION_STATUSES)[keyof typeof ACTION_STATUSES];
|
|
71
88
|
/**
|
|
72
89
|
* A single permission scope binding a service to an access level.
|
|
73
90
|
*
|
|
@@ -232,4 +249,214 @@ interface McpToolScopeMapping {
|
|
|
232
249
|
*/
|
|
233
250
|
declare function mapMcpToolToScope(toolName: string): McpToolScopeMapping;
|
|
234
251
|
|
|
235
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Action logging client for Multicorn Shield.
|
|
254
|
+
*
|
|
255
|
+
* Sends structured action events to the Multicorn hosted API, providing the
|
|
256
|
+
* observability backbone for Shield. Every agent action is logged with
|
|
257
|
+
* metadata, cost information, and status tracking.
|
|
258
|
+
*
|
|
259
|
+
* **Design principles:**
|
|
260
|
+
* - **Fire-and-forget**: Logging failures MUST NOT block the agent's action.
|
|
261
|
+
* - **Security (Jordan persona)**: API keys never logged, HTTPS enforced.
|
|
262
|
+
* - **Clear errors (Yuki persona)**: Descriptive messages for all failure modes.
|
|
263
|
+
* - **Clean async (The Team persona)**: Proper promise handling, no hanging requests.
|
|
264
|
+
*
|
|
265
|
+
* @module logger/action-logger
|
|
266
|
+
*/
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Configuration options for the action logger client.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* const config: ActionLoggerConfig = {
|
|
274
|
+
* apiKey: process.env.MULTICORN_API_KEY,
|
|
275
|
+
* baseUrl: "https://api.multicorn.ai",
|
|
276
|
+
* timeout: 5000,
|
|
277
|
+
* batchMode: {
|
|
278
|
+
* enabled: true,
|
|
279
|
+
* maxSize: 10,
|
|
280
|
+
* flushIntervalMs: 5000,
|
|
281
|
+
* },
|
|
282
|
+
* };
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
interface ActionLoggerConfig {
|
|
286
|
+
/**
|
|
287
|
+
* Multicorn API key for authentication.
|
|
288
|
+
* Passed as `X-Multicorn-Key` header. Never logged.
|
|
289
|
+
*/
|
|
290
|
+
readonly apiKey: string;
|
|
291
|
+
/**
|
|
292
|
+
* Base URL for the Multicorn API.
|
|
293
|
+
* @default "https://api.multicorn.ai"
|
|
294
|
+
*/
|
|
295
|
+
readonly baseUrl?: string;
|
|
296
|
+
/**
|
|
297
|
+
* Request timeout in milliseconds.
|
|
298
|
+
* @default 5000
|
|
299
|
+
*/
|
|
300
|
+
readonly timeout?: number;
|
|
301
|
+
/**
|
|
302
|
+
* Optional batch mode configuration.
|
|
303
|
+
* When enabled, actions are queued and flushed periodically.
|
|
304
|
+
*/
|
|
305
|
+
readonly batchMode?: BatchModeConfig;
|
|
306
|
+
/**
|
|
307
|
+
* Optional error handler for logging failures.
|
|
308
|
+
* Called asynchronously. Does not block the main action flow.
|
|
309
|
+
*/
|
|
310
|
+
readonly onError?: (error: Error) => void;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Batch mode configuration.
|
|
314
|
+
*
|
|
315
|
+
* Actions are flushed when **either** condition is met:
|
|
316
|
+
* - The queue reaches `maxSize` actions, OR
|
|
317
|
+
* - `flushIntervalMs` milliseconds have elapsed since the last flush.
|
|
318
|
+
*/
|
|
319
|
+
interface BatchModeConfig {
|
|
320
|
+
/** Whether batch mode is enabled. */
|
|
321
|
+
readonly enabled: boolean;
|
|
322
|
+
/**
|
|
323
|
+
* Maximum number of actions to queue before forcing a flush.
|
|
324
|
+
* @default 10
|
|
325
|
+
*/
|
|
326
|
+
readonly maxSize?: number;
|
|
327
|
+
/**
|
|
328
|
+
* Maximum time (ms) between flushes.
|
|
329
|
+
* @default 5000
|
|
330
|
+
*/
|
|
331
|
+
readonly flushIntervalMs?: number;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A single action event to be logged.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* const action: ActionPayload = {
|
|
339
|
+
* agent: "inbox-assistant",
|
|
340
|
+
* service: "gmail",
|
|
341
|
+
* actionType: "send_email",
|
|
342
|
+
* status: "approved",
|
|
343
|
+
* cost: 0.002,
|
|
344
|
+
* metadata: {
|
|
345
|
+
* recipient: "user@example.com",
|
|
346
|
+
* subject: "Weekly report",
|
|
347
|
+
* },
|
|
348
|
+
* };
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
interface ActionPayload {
|
|
352
|
+
/** Agent identifier (e.g. "inbox-assistant"). */
|
|
353
|
+
readonly agent: string;
|
|
354
|
+
/** Service being accessed (e.g. "gmail", "slack"). */
|
|
355
|
+
readonly service: string;
|
|
356
|
+
/** Type of action performed (e.g. "send_email", "read_message"). */
|
|
357
|
+
readonly actionType: string;
|
|
358
|
+
/** Lifecycle status of the action. */
|
|
359
|
+
readonly status: ActionStatus;
|
|
360
|
+
/**
|
|
361
|
+
* Optional cost in USD incurred by this action.
|
|
362
|
+
* Present only for actions with usage-based pricing.
|
|
363
|
+
*/
|
|
364
|
+
readonly cost?: number;
|
|
365
|
+
/**
|
|
366
|
+
* Optional structured metadata for additional context.
|
|
367
|
+
* Keys and values must be serializable to JSON.
|
|
368
|
+
*/
|
|
369
|
+
readonly metadata?: Readonly<Record<string, string | number | boolean>>;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* HTTP client for logging agent actions to the Multicorn Shield API.
|
|
373
|
+
*
|
|
374
|
+
* Supports both immediate and batched delivery modes. All network failures
|
|
375
|
+
* are handled gracefully to ensure logging never blocks the agent's execution.
|
|
376
|
+
*
|
|
377
|
+
* @example Basic usage (immediate mode)
|
|
378
|
+
* ```ts
|
|
379
|
+
* const logger = createActionLogger({
|
|
380
|
+
* apiKey: process.env.MULTICORN_API_KEY!,
|
|
381
|
+
* });
|
|
382
|
+
*
|
|
383
|
+
* await logger.logAction({
|
|
384
|
+
* agent: "email-assistant",
|
|
385
|
+
* service: "gmail",
|
|
386
|
+
* actionType: "send_email",
|
|
387
|
+
* status: "approved",
|
|
388
|
+
* cost: 0.002,
|
|
389
|
+
* });
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @example Batch mode with error handling
|
|
393
|
+
* ```ts
|
|
394
|
+
* const logger = createActionLogger({
|
|
395
|
+
* apiKey: process.env.MULTICORN_API_KEY!,
|
|
396
|
+
* batchMode: { enabled: true, maxSize: 10, flushIntervalMs: 5000 },
|
|
397
|
+
* onError: (err) => console.error("[ActionLogger]", err.message),
|
|
398
|
+
* });
|
|
399
|
+
*
|
|
400
|
+
* // Actions are queued and flushed automatically
|
|
401
|
+
* logger.logAction({
|
|
402
|
+
* agent: "inbox-assistant",
|
|
403
|
+
* service: "gmail",
|
|
404
|
+
* actionType: "read_message",
|
|
405
|
+
* status: "approved",
|
|
406
|
+
* });
|
|
407
|
+
*
|
|
408
|
+
* // Force immediate flush
|
|
409
|
+
* await logger.flush();
|
|
410
|
+
*
|
|
411
|
+
* // Clean up resources
|
|
412
|
+
* logger.shutdown();
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
interface ActionLogger {
|
|
416
|
+
/**
|
|
417
|
+
* Log a single action event.
|
|
418
|
+
*
|
|
419
|
+
* In immediate mode, sends the action to the API right away (non-blocking).
|
|
420
|
+
* In batch mode, queues the action and flushes when thresholds are met.
|
|
421
|
+
*
|
|
422
|
+
* @param action - The action event to log.
|
|
423
|
+
* @returns A promise that resolves when the action is sent (immediate mode)
|
|
424
|
+
* or queued (batch mode). Rejects only on validation errors, not
|
|
425
|
+
* network failures (those are passed to `onError`).
|
|
426
|
+
*/
|
|
427
|
+
logAction(action: ActionPayload): Promise<void>;
|
|
428
|
+
/**
|
|
429
|
+
* Flush all queued actions immediately (batch mode only).
|
|
430
|
+
*
|
|
431
|
+
* In immediate mode, this is a no-op.
|
|
432
|
+
*
|
|
433
|
+
* @returns A promise that resolves when all queued actions have been sent.
|
|
434
|
+
*/
|
|
435
|
+
flush(): Promise<void>;
|
|
436
|
+
/**
|
|
437
|
+
* Shut down the logger and clean up resources.
|
|
438
|
+
*
|
|
439
|
+
* Stops the flush timer (if in batch mode) and flushes any remaining actions.
|
|
440
|
+
* After calling `shutdown()`, the logger should not be used.
|
|
441
|
+
*/
|
|
442
|
+
shutdown(): Promise<void>;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Create a new action logger client.
|
|
446
|
+
*
|
|
447
|
+
* @param config - Configuration options.
|
|
448
|
+
* @returns An {@link ActionLogger} instance.
|
|
449
|
+
* @throws {Error} If the API key is missing or the base URL is not HTTPS.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```ts
|
|
453
|
+
* const logger = createActionLogger({
|
|
454
|
+
* apiKey: process.env.MULTICORN_API_KEY!,
|
|
455
|
+
* baseUrl: "https://api.multicorn.ai",
|
|
456
|
+
* timeout: 3000,
|
|
457
|
+
* });
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
declare function createActionLogger(config: ActionLoggerConfig): ActionLogger;
|
|
461
|
+
|
|
462
|
+
export { type ActionLogger, type ActionLoggerConfig, type ActionPayload, type AgentRecord, type JsonRpcError, type JsonRpcRequest, type JsonRpcResponse, type LogLevel, type PermissionLevel, type ProxyLogger, type Scope, ShieldAuthError, type ToolCallParams, buildAuthErrorResponse, buildBlockedResponse, buildInternalErrorResponse, buildServiceUnreachableResponse, buildSpendingBlockedResponse, createActionLogger, createLogger, deriveDashboardUrl, extractActionFromToolName, extractServiceFromToolName, extractToolCallParams, fetchGrantedScopes, findAgentByName, isValidLogLevel, mapMcpToolToScope, parseJsonRpcLine, registerAgent, validateScopeAccess };
|
package/dist/proxy.js
CHANGED
|
@@ -435,4 +435,177 @@ function mapMcpToolToScope(toolName) {
|
|
|
435
435
|
return { service: head, permissionLevel, actionType };
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
|
|
438
|
+
// src/logger/action-logger.ts
|
|
439
|
+
function createActionLogger(config) {
|
|
440
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
"[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
|
|
446
|
+
const timeout = config.timeout ?? 5e3;
|
|
447
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const endpoint = `${baseUrl}/api/v1/actions`;
|
|
453
|
+
const batchEnabled = config.batchMode?.enabled ?? false;
|
|
454
|
+
const maxBatchSize = config.batchMode?.maxSize ?? 10;
|
|
455
|
+
const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
|
|
456
|
+
const queue = [];
|
|
457
|
+
let flushTimer;
|
|
458
|
+
let isShutdown = false;
|
|
459
|
+
async function sendActions(actions) {
|
|
460
|
+
if (actions.length === 0) return;
|
|
461
|
+
const convertAction = (action) => ({
|
|
462
|
+
agent: action.agent,
|
|
463
|
+
service: action.service,
|
|
464
|
+
actionType: action.actionType,
|
|
465
|
+
status: action.status,
|
|
466
|
+
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
467
|
+
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
468
|
+
});
|
|
469
|
+
const convertedActions = actions.map(convertAction);
|
|
470
|
+
const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
|
|
471
|
+
let lastError;
|
|
472
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
473
|
+
try {
|
|
474
|
+
const controller = new AbortController();
|
|
475
|
+
const timeoutId = setTimeout(() => {
|
|
476
|
+
controller.abort();
|
|
477
|
+
}, timeout);
|
|
478
|
+
const response = await fetch(endpoint, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: {
|
|
481
|
+
"Content-Type": "application/json",
|
|
482
|
+
"X-Multicorn-Key": config.apiKey
|
|
483
|
+
},
|
|
484
|
+
body: JSON.stringify(payload),
|
|
485
|
+
signal: controller.signal
|
|
486
|
+
});
|
|
487
|
+
clearTimeout(timeoutId);
|
|
488
|
+
if (response.ok) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (response.status >= 400 && response.status < 500) {
|
|
492
|
+
const body = await response.text().catch(() => "");
|
|
493
|
+
throw new Error(
|
|
494
|
+
`[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (response.status >= 500 && attempt === 0) {
|
|
498
|
+
lastError = new Error(
|
|
499
|
+
`[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
|
|
500
|
+
);
|
|
501
|
+
await sleep(100 * Math.pow(2, attempt));
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
throw new Error(
|
|
505
|
+
`[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error instanceof Error) {
|
|
509
|
+
if (error.name === "AbortError") {
|
|
510
|
+
lastError = new Error(
|
|
511
|
+
`[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
|
|
512
|
+
);
|
|
513
|
+
} else if (error.message.includes("Client error") || error.message.includes("Server error")) {
|
|
514
|
+
lastError = error;
|
|
515
|
+
} else {
|
|
516
|
+
lastError = new Error(
|
|
517
|
+
`[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
|
|
522
|
+
}
|
|
523
|
+
if (attempt === 0 && !lastError.message.includes("Client error")) {
|
|
524
|
+
await sleep(100 * Math.pow(2, attempt));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (lastError) {
|
|
531
|
+
if (config.onError) {
|
|
532
|
+
config.onError(lastError);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function flushQueue() {
|
|
537
|
+
if (queue.length === 0) return;
|
|
538
|
+
const actions = queue.map((item) => item.payload);
|
|
539
|
+
queue.length = 0;
|
|
540
|
+
await sendActions(actions);
|
|
541
|
+
}
|
|
542
|
+
function startFlushTimer() {
|
|
543
|
+
if (flushTimer !== void 0) return;
|
|
544
|
+
flushTimer = setInterval(() => {
|
|
545
|
+
flushQueue().catch(() => {
|
|
546
|
+
});
|
|
547
|
+
}, flushInterval);
|
|
548
|
+
const timer = flushTimer;
|
|
549
|
+
if (typeof timer.unref === "function") {
|
|
550
|
+
timer.unref();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function stopFlushTimer() {
|
|
554
|
+
if (flushTimer) {
|
|
555
|
+
clearInterval(flushTimer);
|
|
556
|
+
flushTimer = void 0;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (batchEnabled) {
|
|
560
|
+
startFlushTimer();
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
logAction(action) {
|
|
564
|
+
if (isShutdown) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
"[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (action.agent.trim().length === 0) {
|
|
570
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
|
|
571
|
+
}
|
|
572
|
+
if (action.service.trim().length === 0) {
|
|
573
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
|
|
574
|
+
}
|
|
575
|
+
if (action.actionType.trim().length === 0) {
|
|
576
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
|
|
577
|
+
}
|
|
578
|
+
if (action.status.trim().length === 0) {
|
|
579
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
|
|
580
|
+
}
|
|
581
|
+
if (batchEnabled) {
|
|
582
|
+
queue.push({ payload: action, timestamp: Date.now() });
|
|
583
|
+
if (queue.length >= maxBatchSize) {
|
|
584
|
+
flushQueue().catch(() => {
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
sendActions([action]).catch(() => {
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return Promise.resolve();
|
|
592
|
+
},
|
|
593
|
+
async flush() {
|
|
594
|
+
if (!batchEnabled) return;
|
|
595
|
+
await flushQueue();
|
|
596
|
+
},
|
|
597
|
+
async shutdown() {
|
|
598
|
+
if (isShutdown) return;
|
|
599
|
+
isShutdown = true;
|
|
600
|
+
stopFlushTimer();
|
|
601
|
+
if (batchEnabled) {
|
|
602
|
+
await flushQueue();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function sleep(ms) {
|
|
608
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export { ShieldAuthError, buildAuthErrorResponse, buildBlockedResponse, buildInternalErrorResponse, buildServiceUnreachableResponse, buildSpendingBlockedResponse, createActionLogger, createLogger, deriveDashboardUrl, extractActionFromToolName, extractServiceFromToolName, extractToolCallParams, fetchGrantedScopes, findAgentByName, isValidLogLevel, mapMcpToolToScope, parseJsonRpcLine, registerAgent, validateScopeAccess };
|
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;
|
|
@@ -23705,9 +23705,6 @@ async function autoCreateProxyConfig(baseUrl, apiKey, serverName, entry, agentNa
|
|
|
23705
23705
|
return true;
|
|
23706
23706
|
}
|
|
23707
23707
|
async function runShieldExtension() {
|
|
23708
|
-
const debugBaseUrl = process.env["MULTICORN_BASE_URL"] ?? "";
|
|
23709
|
-
const debugApiKeyPrefix = process.env["MULTICORN_API_KEY"]?.slice(0, 8) ?? "";
|
|
23710
|
-
console.error(`[SHIELD-DEBUG] BASE_URL=${debugBaseUrl} API_KEY=${debugApiKeyPrefix}...`);
|
|
23711
23708
|
const logger = createLogger(readLogLevel());
|
|
23712
23709
|
const apiKey = readApiKey();
|
|
23713
23710
|
if (apiKey === null) {
|
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",
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
39
|
"plugins/windsurf",
|
|
40
|
+
"plugins/cline",
|
|
41
|
+
"plugins/gemini-cli",
|
|
40
42
|
"LICENSE",
|
|
41
43
|
"README.md",
|
|
42
44
|
"CHANGELOG.md"
|
|
@@ -74,7 +76,7 @@
|
|
|
74
76
|
"@open-wc/testing-helpers": "^3.0.1",
|
|
75
77
|
"@size-limit/file": "^11.1.6",
|
|
76
78
|
"@types/node": "^22.0.0",
|
|
77
|
-
"@vitest/coverage-
|
|
79
|
+
"@vitest/coverage-v8": "^3.0.5",
|
|
78
80
|
"eslint": "^9.19.0",
|
|
79
81
|
"eslint-config-prettier": "^10.0.1",
|
|
80
82
|
"eslint-plugin-unicorn": "^57.0.0",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Multicorn Shield Cline plugin
|
|
2
|
+
|
|
3
|
+
This folder ships **Shield hooks for the [Cline](https://github.com/cline/cline) VS Code extension**. The PreToolUse script asks Shield whether a pending tool call is allowed; the PostToolUse script records completed actions in the Shield audit trail.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Node.js** 20 or newer (hooks run as standalone Node scripts).
|
|
8
|
+
- **Cline** v3.36 or newer with **Hooks** enabled in settings.
|
|
9
|
+
|
|
10
|
+
Keep these three scripts together whenever you install by hand:
|
|
11
|
+
|
|
12
|
+
- `hooks/scripts/pre-tool-use.cjs`
|
|
13
|
+
- `hooks/scripts/post-tool-use.cjs`
|
|
14
|
+
- `hooks/scripts/shared.cjs`
|
|
15
|
+
|
|
16
|
+
## Installing the hooks
|
|
17
|
+
|
|
18
|
+
**CLI (recommended):** run `npx multicorn-shield init` and follow prompts so the scripts are copied into Cline's hooks directory.
|
|
19
|
+
|
|
20
|
+
**Manual:** copy the `hooks/scripts/` `.cjs` files into:
|
|
21
|
+
|
|
22
|
+
`~/Documents/Cline/Hooks/`
|
|
23
|
+
|
|
24
|
+
(or the equivalent Hooks path on your machine). Cline runs each hook with stdin JSON from its Hooks API.
|
|
25
|
+
|
|
26
|
+
## Config file
|
|
27
|
+
|
|
28
|
+
Path: **`~/.multicorn/config.json`**
|
|
29
|
+
|
|
30
|
+
| Field | Required | Description |
|
|
31
|
+
| --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `apiKey` | yes | Multicorn API key used in the Shield request (`X-Multicorn-Key`). |
|
|
33
|
+
| `baseUrl` | no | API root. Defaults to `https://api.multicorn.ai` (no trailing slash). Non-local URLs must use `https://` or the hooks disable Shield (fail-open). `localhost` / `127.0.0.1` may use HTTP. |
|
|
34
|
+
| `agents` | no | Array of `{ "name": "...", "platform": "cline" }` objects so the hooks know which Shield agent name to use. Legacy `agentName` string is still read if present. |
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"apiKey": "mcs_your_key_here",
|
|
41
|
+
"baseUrl": "https://api.multicorn.ai",
|
|
42
|
+
"agents": [{ "name": "my-repo-agent", "platform": "cline" }]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
1. **PreToolUse:** before Cline runs a tool, stdin carries the pending request. The script maps the tool name to a Shield **service** and **actionType**, POSTs `status: pending` to `/api/v1/actions`, and reads the response. It either allows the tool (`cancel: false`) or blocks with an error message and optional consent workflow.
|
|
49
|
+
2. **PostToolUse:** after the tool completes, stdin includes the outcome. The script POSTs `status: approved` with metadata (scrubbed parameters and result) so the audit log stays usable without stuffing large secrets into the payload.
|
|
50
|
+
|
|
51
|
+
Both hooks reply with JSON on stdout. The post-hook always finishes with `{ "cancel": false }`.
|
|
52
|
+
|
|
53
|
+
## Security model
|
|
54
|
+
|
|
55
|
+
Shield wiring is **fail-open by design**. If config is missing, invalid for remote HTTP, or the API errors or times out, **actions are allowed** so work is not silently blocked because Shield is down. Deployers treat Shield as governance and auditing, not as a cryptographic boundary against a hostile process on the same machine.
|
|
56
|
+
|
|
57
|
+
## Troubleshooting
|
|
58
|
+
|
|
59
|
+
1. **Confirm hooks run:** temporarily add stderr output (not recommended long term), or tail Cline's hook output/logs if exposed. Successful runs should not spam the developer console unless there is an API warning.
|
|
60
|
+
2. **Nothing reaches Shield:** check `config.json` path and `apiKey`, that `agents` includes `platform: "cline"` with the right `name`, and that `baseUrl` uses HTTPS on non-local installs.
|
|
61
|
+
3. **Windows consent / browser:** the pre-hook opens the consent URL via `cmd.exe start`; if that fails, open the URL from the blocking message manually.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
|
|
3
|
+
/**
|
|
4
|
+
* Cline PostToolUse hook: logs completed actions to the Shield audit trail.
|
|
5
|
+
* Reads JSON from stdin (Cline Hooks API), posts to Shield API.
|
|
6
|
+
* Always returns {"cancel": false} - post hooks never block.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
buildScrubbedParametersJson,
|
|
13
|
+
loadConfig,
|
|
14
|
+
logPrefix,
|
|
15
|
+
mapToolName,
|
|
16
|
+
postJson,
|
|
17
|
+
readStdin,
|
|
18
|
+
scrubResultForMetadata,
|
|
19
|
+
} = require("./shared.cjs");
|
|
20
|
+
|
|
21
|
+
const HOOK_PREFIX = logPrefix("post-hook");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Outputs JSON response to stdout and exits.
|
|
25
|
+
*/
|
|
26
|
+
function respond() {
|
|
27
|
+
process.stdout.write(JSON.stringify({ cancel: false }) + "\n");
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = await readStdin();
|
|
35
|
+
} catch {
|
|
36
|
+
respond();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const config = loadConfig();
|
|
41
|
+
if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
42
|
+
respond();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @type {Record<string, unknown>} */
|
|
47
|
+
let hookPayload;
|
|
48
|
+
try {
|
|
49
|
+
hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
50
|
+
} catch {
|
|
51
|
+
respond();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const postToolUse = hookPayload.postToolUse;
|
|
56
|
+
if (postToolUse === null || typeof postToolUse !== "object") {
|
|
57
|
+
respond();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const toolUse = /** @type {Record<string, unknown>} */ (postToolUse);
|
|
62
|
+
const toolName = typeof toolUse.tool === "string" ? toolUse.tool : "";
|
|
63
|
+
|
|
64
|
+
if (toolName.length === 0) {
|
|
65
|
+
respond();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { service, actionType } = mapToolName(toolName);
|
|
70
|
+
|
|
71
|
+
const paramsSerialized = buildScrubbedParametersJson(toolUse.parameters);
|
|
72
|
+
|
|
73
|
+
/** @type {Record<string, unknown>} */
|
|
74
|
+
const metadata = {
|
|
75
|
+
tool_name: toolName,
|
|
76
|
+
task_id: typeof hookPayload.taskId === "string" ? hookPayload.taskId : "",
|
|
77
|
+
cline_version: typeof hookPayload.clineVersion === "string" ? hookPayload.clineVersion : "",
|
|
78
|
+
parameters: paramsSerialized,
|
|
79
|
+
result: scrubResultForMetadata(toolUse.result),
|
|
80
|
+
timing: typeof toolUse.timing === "object" ? JSON.stringify(toolUse.timing) : "",
|
|
81
|
+
source: "cline",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @type {Record<string, unknown>} */
|
|
85
|
+
const payload = {
|
|
86
|
+
agent: config.agentName,
|
|
87
|
+
service,
|
|
88
|
+
actionType,
|
|
89
|
+
status: "approved",
|
|
90
|
+
metadata,
|
|
91
|
+
platform: "cline",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await postJson(config.baseUrl, config.apiKey, payload);
|
|
96
|
+
const code = res.statusCode ?? 0;
|
|
97
|
+
if (code < 200 || code >= 300) {
|
|
98
|
+
throw new Error(`HTTP ${String(code)}`);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
102
|
+
process.stderr.write(
|
|
103
|
+
`${HOOK_PREFIX} Warning: failed to log action to Shield audit trail. Detail: ${msg}\n`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
respond();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((e) => {
|
|
111
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
112
|
+
process.stderr.write(
|
|
113
|
+
`${HOOK_PREFIX} Warning: failed to log action to Shield audit trail. Detail: ${msg}\n`,
|
|
114
|
+
);
|
|
115
|
+
respond();
|
|
116
|
+
});
|