multicorn-shield 0.10.0 → 0.12.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 +40 -0
- package/dist/badge.js +44 -0
- package/dist/index.cjs +166 -17
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +165 -18
- package/dist/multicorn-proxy.js +181 -9
- package/dist/openclaw-hook/handler.js +0 -1
- package/dist/openclaw-plugin/multicorn-shield.js +14 -18
- 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 +126 -8
- package/package.json +17 -6
- 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/dist/shield-extension.js
CHANGED
|
@@ -22359,11 +22359,117 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22359
22359
|
|
|
22360
22360
|
// package.json
|
|
22361
22361
|
var package_default = {
|
|
22362
|
-
version: "0.
|
|
22362
|
+
version: "0.12.0"};
|
|
22363
22363
|
|
|
22364
22364
|
// src/package-meta.ts
|
|
22365
22365
|
var PACKAGE_VERSION = package_default.version;
|
|
22366
22366
|
|
|
22367
|
+
// src/extension/proxy-url-validator.ts
|
|
22368
|
+
var ProxyUrlValidationError = class extends Error {
|
|
22369
|
+
constructor(message) {
|
|
22370
|
+
super(message);
|
|
22371
|
+
this.name = "ProxyUrlValidationError";
|
|
22372
|
+
}
|
|
22373
|
+
};
|
|
22374
|
+
function isPrivateOrReservedIpv4(hostname3) {
|
|
22375
|
+
const parts = hostname3.split(".");
|
|
22376
|
+
if (parts.length !== 4) {
|
|
22377
|
+
return false;
|
|
22378
|
+
}
|
|
22379
|
+
const octets = [];
|
|
22380
|
+
for (const p of parts) {
|
|
22381
|
+
if (!/^\d{1,3}$/.test(p)) {
|
|
22382
|
+
return false;
|
|
22383
|
+
}
|
|
22384
|
+
const n = Number.parseInt(p, 10);
|
|
22385
|
+
if (Number.isNaN(n) || n < 0 || n > 255) {
|
|
22386
|
+
return false;
|
|
22387
|
+
}
|
|
22388
|
+
octets.push(n);
|
|
22389
|
+
}
|
|
22390
|
+
const [a, b] = octets;
|
|
22391
|
+
if (a === void 0 || b === void 0) {
|
|
22392
|
+
return false;
|
|
22393
|
+
}
|
|
22394
|
+
if (a === 127) {
|
|
22395
|
+
return true;
|
|
22396
|
+
}
|
|
22397
|
+
if (a === 10) {
|
|
22398
|
+
return true;
|
|
22399
|
+
}
|
|
22400
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
22401
|
+
return true;
|
|
22402
|
+
}
|
|
22403
|
+
if (a === 192 && b === 168) {
|
|
22404
|
+
return true;
|
|
22405
|
+
}
|
|
22406
|
+
if (a === 169 && b === 254) {
|
|
22407
|
+
return true;
|
|
22408
|
+
}
|
|
22409
|
+
if (a === 0 && b === 0 && octets[2] === 0 && octets[3] === 0) {
|
|
22410
|
+
return true;
|
|
22411
|
+
}
|
|
22412
|
+
return false;
|
|
22413
|
+
}
|
|
22414
|
+
function isBlockedIpv6(host) {
|
|
22415
|
+
const h = host.split("%")[0]?.toLowerCase() ?? host.toLowerCase();
|
|
22416
|
+
if (h === "::1") {
|
|
22417
|
+
return true;
|
|
22418
|
+
}
|
|
22419
|
+
if (h.startsWith("fe80:")) {
|
|
22420
|
+
return true;
|
|
22421
|
+
}
|
|
22422
|
+
if (h.startsWith("fc") || h.startsWith("fd")) {
|
|
22423
|
+
return true;
|
|
22424
|
+
}
|
|
22425
|
+
return false;
|
|
22426
|
+
}
|
|
22427
|
+
function hostForValidation(hostname3) {
|
|
22428
|
+
if (hostname3.startsWith("[") && hostname3.endsWith("]")) {
|
|
22429
|
+
return hostname3.slice(1, -1);
|
|
22430
|
+
}
|
|
22431
|
+
return hostname3;
|
|
22432
|
+
}
|
|
22433
|
+
function assertSafeProxyUrl(raw, options) {
|
|
22434
|
+
let url2;
|
|
22435
|
+
try {
|
|
22436
|
+
url2 = new URL(raw);
|
|
22437
|
+
} catch {
|
|
22438
|
+
throw new ProxyUrlValidationError(`Invalid proxy URL: ${raw}`);
|
|
22439
|
+
}
|
|
22440
|
+
if (url2.protocol !== "https:" && url2.protocol !== "http:") {
|
|
22441
|
+
throw new ProxyUrlValidationError(
|
|
22442
|
+
`Unsupported proxy URL scheme: ${url2.protocol} - only https: and http: are allowed`
|
|
22443
|
+
);
|
|
22444
|
+
}
|
|
22445
|
+
const hostname3 = url2.hostname;
|
|
22446
|
+
if (hostname3.length === 0) {
|
|
22447
|
+
throw new ProxyUrlValidationError(`Proxy URL has no hostname: ${raw}`);
|
|
22448
|
+
}
|
|
22449
|
+
if (options?.allowPrivateNetworks === true) {
|
|
22450
|
+
return;
|
|
22451
|
+
}
|
|
22452
|
+
const host = hostForValidation(hostname3);
|
|
22453
|
+
if (host.toLowerCase() === "localhost") {
|
|
22454
|
+
throw new ProxyUrlValidationError(
|
|
22455
|
+
`Proxy URL points to a private/reserved network address: ${url2.hostname}`
|
|
22456
|
+
);
|
|
22457
|
+
}
|
|
22458
|
+
if (host.includes(":")) {
|
|
22459
|
+
if (isBlockedIpv6(host)) {
|
|
22460
|
+
throw new ProxyUrlValidationError(
|
|
22461
|
+
`Proxy URL points to a private/reserved network address: ${url2.hostname}`
|
|
22462
|
+
);
|
|
22463
|
+
}
|
|
22464
|
+
return;
|
|
22465
|
+
}
|
|
22466
|
+
if (isPrivateOrReservedIpv4(host)) {
|
|
22467
|
+
throw new ProxyUrlValidationError(
|
|
22468
|
+
`Proxy URL points to a private/reserved network address: ${url2.hostname}`
|
|
22469
|
+
);
|
|
22470
|
+
}
|
|
22471
|
+
}
|
|
22472
|
+
|
|
22367
22473
|
// src/extension/proxy-client.ts
|
|
22368
22474
|
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
22369
22475
|
var ProxyConfigFetchError = class extends Error {
|
|
@@ -22404,8 +22510,12 @@ function isProxyConfigRow(v) {
|
|
|
22404
22510
|
const o = v;
|
|
22405
22511
|
return typeof o["proxy_url"] === "string" && o["proxy_url"].length > 0 && typeof o["server_name"] === "string" && typeof o["target_url"] === "string";
|
|
22406
22512
|
}
|
|
22407
|
-
async function fetchProxyConfigs(baseUrl, apiKey, timeoutMs) {
|
|
22513
|
+
async function fetchProxyConfigs(baseUrl, apiKey, timeoutMs, options) {
|
|
22408
22514
|
const url2 = `${normalizeBaseUrl(baseUrl)}/api/v1/proxy/config`;
|
|
22515
|
+
assertSafeProxyUrl(
|
|
22516
|
+
url2,
|
|
22517
|
+
options?.allowPrivateNetworks === true ? { allowPrivateNetworks: true } : void 0
|
|
22518
|
+
);
|
|
22409
22519
|
let response;
|
|
22410
22520
|
try {
|
|
22411
22521
|
response = await fetch(url2, {
|
|
@@ -22563,6 +22673,10 @@ var ProxySession = class {
|
|
|
22563
22673
|
this.nextId = 1;
|
|
22564
22674
|
this.sessionId = null;
|
|
22565
22675
|
this.closed = false;
|
|
22676
|
+
assertSafeProxyUrl(
|
|
22677
|
+
proxyUrl,
|
|
22678
|
+
options?.allowPrivateNetworks === true ? { allowPrivateNetworks: true } : void 0
|
|
22679
|
+
);
|
|
22566
22680
|
this.proxyUrl = proxyUrl.replace(/\/+$/, "") + "/mcp";
|
|
22567
22681
|
this.apiKey = apiKey;
|
|
22568
22682
|
this.requestTimeoutMs = options?.requestTimeoutMs ?? 6e4;
|
|
@@ -23554,6 +23668,8 @@ async function autoCreateProxyConfig(baseUrl, apiKey, serverName, entry, agentNa
|
|
|
23554
23668
|
const targetUrl = `stdio://${entry.command}/${entry.args.join("/")}`;
|
|
23555
23669
|
const url2 = `${baseUrl.replace(/\/+$/, "")}/api/v1/proxy/config`;
|
|
23556
23670
|
debugLog2(`[SHIELD] Auto-creating proxy config for "${serverName}".`);
|
|
23671
|
+
const allowPrivateNetworks = process.env["MULTICORN_ALLOW_PRIVATE_PROXY_HOSTS"] === "1";
|
|
23672
|
+
assertSafeProxyUrl(url2, { allowPrivateNetworks });
|
|
23557
23673
|
let response;
|
|
23558
23674
|
try {
|
|
23559
23675
|
response = await fetch(url2, {
|
|
@@ -23589,9 +23705,6 @@ async function autoCreateProxyConfig(baseUrl, apiKey, serverName, entry, agentNa
|
|
|
23589
23705
|
return true;
|
|
23590
23706
|
}
|
|
23591
23707
|
async function runShieldExtension() {
|
|
23592
|
-
const debugBaseUrl = process.env["MULTICORN_BASE_URL"] ?? "";
|
|
23593
|
-
const debugApiKeyPrefix = process.env["MULTICORN_API_KEY"]?.slice(0, 8) ?? "";
|
|
23594
|
-
console.error(`[SHIELD-DEBUG] BASE_URL=${debugBaseUrl} API_KEY=${debugApiKeyPrefix}...`);
|
|
23595
23708
|
const logger = createLogger(readLogLevel());
|
|
23596
23709
|
const apiKey = readApiKey();
|
|
23597
23710
|
if (apiKey === null) {
|
|
@@ -23686,6 +23799,7 @@ async function runShieldExtension() {
|
|
|
23686
23799
|
`[SHIELD] Config read; ${String(serverCount)} MCP server(s) discovered (excluding Shield).`
|
|
23687
23800
|
);
|
|
23688
23801
|
debugLog2("[SHIELD] Resolving proxy configs (local config or API).");
|
|
23802
|
+
const allowPrivateNetworks = process.env["MULTICORN_ALLOW_PRIVATE_PROXY_HOSTS"] === "1";
|
|
23689
23803
|
let configs;
|
|
23690
23804
|
const localConfigs = await readProxyConfigsFromLocalMulticornConfig();
|
|
23691
23805
|
if (localConfigs.length > 0) {
|
|
@@ -23694,7 +23808,9 @@ async function runShieldExtension() {
|
|
|
23694
23808
|
} else {
|
|
23695
23809
|
debugLog2("[SHIELD] No local proxy configs; fetching from API.");
|
|
23696
23810
|
try {
|
|
23697
|
-
configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS
|
|
23811
|
+
configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS, {
|
|
23812
|
+
allowPrivateNetworks
|
|
23813
|
+
});
|
|
23698
23814
|
} catch (e) {
|
|
23699
23815
|
clearTimeout(setupTimeout);
|
|
23700
23816
|
if (e instanceof ProxyConfigFetchError) {
|
|
@@ -23728,7 +23844,9 @@ async function runShieldExtension() {
|
|
|
23728
23844
|
`[SHIELD] Auto-created ${String(createdCount)} proxy config(s); re-fetching from API.`
|
|
23729
23845
|
);
|
|
23730
23846
|
try {
|
|
23731
|
-
configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS
|
|
23847
|
+
configs = await fetchProxyConfigs(baseUrl, apiKey, SETUP_TIMEOUT_MS, {
|
|
23848
|
+
allowPrivateNetworks
|
|
23849
|
+
});
|
|
23732
23850
|
} catch (e) {
|
|
23733
23851
|
const message = e instanceof Error ? e.message : String(e);
|
|
23734
23852
|
debugLog2(`[SHIELD] Re-fetch after auto-creation failed: ${message}`);
|
|
@@ -23766,7 +23884,7 @@ async function runShieldExtension() {
|
|
|
23766
23884
|
const toolsByProxy = /* @__PURE__ */ new Map();
|
|
23767
23885
|
const sessionByProxyUrl = /* @__PURE__ */ new Map();
|
|
23768
23886
|
for (const cfg of configs) {
|
|
23769
|
-
const session = new ProxySession(cfg.proxy_url, apiKey);
|
|
23887
|
+
const session = new ProxySession(cfg.proxy_url, apiKey, { allowPrivateNetworks });
|
|
23770
23888
|
try {
|
|
23771
23889
|
debugLog2(`[SHIELD] Initializing proxy session for ${cfg.server_name}.`);
|
|
23772
23890
|
await session.initialize();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multicorn-shield",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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,7 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
39
|
"plugins/windsurf",
|
|
40
|
+
"plugins/cline",
|
|
40
41
|
"LICENSE",
|
|
41
42
|
"README.md",
|
|
42
43
|
"CHANGELOG.md"
|
|
@@ -45,13 +46,18 @@
|
|
|
45
46
|
"access": "public",
|
|
46
47
|
"provenance": true
|
|
47
48
|
},
|
|
48
|
-
"sideEffects":
|
|
49
|
+
"sideEffects": [
|
|
50
|
+
"dist/index.js",
|
|
51
|
+
"dist/index.cjs",
|
|
52
|
+
"dist/badge.js",
|
|
53
|
+
"src/badge/multicorn-badge.ts"
|
|
54
|
+
],
|
|
49
55
|
"engines": {
|
|
50
56
|
"node": ">=20"
|
|
51
57
|
},
|
|
52
58
|
"lint-staged": {
|
|
53
59
|
"*.ts": [
|
|
54
|
-
"eslint --fix",
|
|
60
|
+
"eslint --fix --no-warn-ignored",
|
|
55
61
|
"prettier --write"
|
|
56
62
|
],
|
|
57
63
|
"*.{json,md}": [
|
|
@@ -69,7 +75,7 @@
|
|
|
69
75
|
"@open-wc/testing-helpers": "^3.0.1",
|
|
70
76
|
"@size-limit/file": "^11.1.6",
|
|
71
77
|
"@types/node": "^22.0.0",
|
|
72
|
-
"@vitest/coverage-
|
|
78
|
+
"@vitest/coverage-v8": "^3.0.5",
|
|
73
79
|
"eslint": "^9.19.0",
|
|
74
80
|
"eslint-config-prettier": "^10.0.1",
|
|
75
81
|
"eslint-plugin-unicorn": "^57.0.0",
|
|
@@ -97,6 +103,11 @@
|
|
|
97
103
|
"path": "dist/index.cjs",
|
|
98
104
|
"limit": "50 kB",
|
|
99
105
|
"gzip": true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"path": "dist/badge.js",
|
|
109
|
+
"limit": "5 kB",
|
|
110
|
+
"gzip": true
|
|
100
111
|
}
|
|
101
112
|
],
|
|
102
113
|
"keywords": [
|
|
@@ -123,8 +134,8 @@
|
|
|
123
134
|
"scripts": {
|
|
124
135
|
"build": "tsup",
|
|
125
136
|
"dev": "tsup --watch",
|
|
126
|
-
"lint": "eslint . && prettier --check .",
|
|
127
|
-
"lint:fix": "eslint --fix . && prettier --write .",
|
|
137
|
+
"lint": "eslint . --no-warn-ignored && prettier --check .",
|
|
138
|
+
"lint:fix": "eslint --fix . --no-warn-ignored && prettier --write .",
|
|
128
139
|
"test": "vitest run",
|
|
129
140
|
"test:watch": "vitest",
|
|
130
141
|
"test:coverage": "vitest run --coverage",
|
|
@@ -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
|
+
});
|