@tankpkg/cli 0.14.4 → 0.15.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.
- package/dist/bin/tank.js +513 -104
- package/dist/bin/tank.js.map +1 -1
- package/dist/{debug-logger-DR0pKCTH.js → debug-logger-CxX7rakF.js} +2 -2
- package/dist/{debug-logger-DR0pKCTH.js.map → debug-logger-CxX7rakF.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/package.json +2 -1
- package/dist/proxy-download-ml-DUk7ehNs.js +39 -0
- package/dist/proxy-download-ml-DUk7ehNs.js.map +1 -0
- package/dist/proxy-remote-CCemokkz.js +44 -0
- package/dist/proxy-remote-CCemokkz.js.map +1 -0
- package/dist/proxy-reset-pins-CfEbaL-F.js +28 -0
- package/dist/proxy-reset-pins-CfEbaL-F.js.map +1 -0
- package/package.json +2 -1
package/dist/bin/tank.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as VERSION, i as USER_AGENT, l as setConfig, n as flushLogs, o as getConfig, s as getConfigDir, t as authFlowLog } from "../debug-logger-
|
|
2
|
+
import { a as VERSION, i as USER_AGENT, l as setConfig, n as flushLogs, o as getConfig, s as getConfigDir, t as authFlowLog } from "../debug-logger-CxX7rakF.js";
|
|
3
3
|
import { t as logger } from "../logger-BhULz3Uz.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { Command } from "commander";
|
|
@@ -13,9 +13,9 @@ import ora from "ora";
|
|
|
13
13
|
import { confirm, input } from "@inquirer/prompts";
|
|
14
14
|
import { execSync, spawn } from "node:child_process";
|
|
15
15
|
import crypto$1 from "node:crypto";
|
|
16
|
+
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
|
|
16
17
|
import { create, extract } from "tar";
|
|
17
18
|
import { createInterface } from "node:readline";
|
|
18
|
-
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
|
|
19
19
|
import { Readable } from "node:stream";
|
|
20
20
|
import { pipeline } from "node:stream/promises";
|
|
21
21
|
import open from "open";
|
|
@@ -301,6 +301,35 @@ const packageIRSchema = z.object({
|
|
|
301
301
|
visibility: z.enum(["public", "private"]).optional(),
|
|
302
302
|
audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional()
|
|
303
303
|
}).strict();
|
|
304
|
+
const commandSchema$1 = z.string().min(1, "command must not be empty");
|
|
305
|
+
const argSchema$1 = z.array(z.string()).default([]);
|
|
306
|
+
const envSchema$1 = z.record(z.string(), z.string()).optional();
|
|
307
|
+
const remoteUrlSchema$1 = z.string().url("remote must be a valid URL");
|
|
308
|
+
const localMcpServerSchema = z.object({
|
|
309
|
+
command: commandSchema$1,
|
|
310
|
+
args: argSchema$1,
|
|
311
|
+
env: envSchema$1,
|
|
312
|
+
requires_auth: z.literal(false).optional()
|
|
313
|
+
}).strict();
|
|
314
|
+
const remoteMcpServerSchema = z.object({
|
|
315
|
+
remote: remoteUrlSchema$1,
|
|
316
|
+
requires_auth: z.boolean().default(false),
|
|
317
|
+
env: envSchema$1
|
|
318
|
+
}).strict();
|
|
319
|
+
const mcpServerSchema$1 = z.union([localMcpServerSchema, remoteMcpServerSchema]);
|
|
320
|
+
function isRemoteMcpServer(server) {
|
|
321
|
+
return "remote" in server;
|
|
322
|
+
}
|
|
323
|
+
const perToolOverrideSchema$1 = z.object({
|
|
324
|
+
scan: z.boolean().optional(),
|
|
325
|
+
blockOnMatch: z.boolean().optional()
|
|
326
|
+
}).strict();
|
|
327
|
+
z.object({
|
|
328
|
+
perfBudgetMs: z.number().positive().optional(),
|
|
329
|
+
blockOnMatch: z.boolean().optional(),
|
|
330
|
+
resetPinsOnMismatch: z.boolean().optional(),
|
|
331
|
+
perTool: z.record(z.string(), perToolOverrideSchema$1).optional()
|
|
332
|
+
}).strict();
|
|
304
333
|
const baseManifestFields$1 = {
|
|
305
334
|
name: z.string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
|
|
306
335
|
version: z.string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
|
|
@@ -309,7 +338,8 @@ const baseManifestFields$1 = {
|
|
|
309
338
|
permissions: permissionsSchema$1.optional(),
|
|
310
339
|
repository: z.string().url("Repository must be a valid URL").optional(),
|
|
311
340
|
visibility: z.enum(["public", "private"]).optional(),
|
|
312
|
-
audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional()
|
|
341
|
+
audit: z.object({ min_score: z.number().min(0).max(10) }).strict().optional(),
|
|
342
|
+
mcp_server: mcpServerSchema$1.optional()
|
|
313
343
|
};
|
|
314
344
|
/** Legacy skills.json schema — strict, no atoms. Used for backward-compatible consumers. */
|
|
315
345
|
const skillsJsonSchema = z.object(baseManifestFields$1).strict();
|
|
@@ -467,7 +497,7 @@ function scoreColor$3(score) {
|
|
|
467
497
|
if (score >= 4) return chalk.yellow;
|
|
468
498
|
return chalk.red;
|
|
469
499
|
}
|
|
470
|
-
function formatScore(result) {
|
|
500
|
+
function formatScore$1(result) {
|
|
471
501
|
if (result.error) return chalk.dim("error");
|
|
472
502
|
if (result.score == null || result.status !== "completed") return chalk.dim("pending");
|
|
473
503
|
return scoreColor$3(result.score)(result.score.toFixed(1));
|
|
@@ -510,7 +540,7 @@ function displayDetailedAudit(result) {
|
|
|
510
540
|
console.log(chalk.bold(result.name));
|
|
511
541
|
console.log("");
|
|
512
542
|
console.log(`${chalk.dim("Version:".padEnd(14))}${result.version}`);
|
|
513
|
-
console.log(`${chalk.dim("Audit Score:".padEnd(14))}${formatScore(result)}`);
|
|
543
|
+
console.log(`${chalk.dim("Audit Score:".padEnd(14))}${formatScore$1(result)}`);
|
|
514
544
|
console.log(`${chalk.dim("Status:".padEnd(14))}${result.status}`);
|
|
515
545
|
const perms = result.permissions;
|
|
516
546
|
if (perms) {
|
|
@@ -534,7 +564,7 @@ function displayTable(results) {
|
|
|
534
564
|
for (const result of results) {
|
|
535
565
|
const name = chalk.bold(padRight$1(result.name, 30));
|
|
536
566
|
const version = padRight$1(result.version, 12);
|
|
537
|
-
const score = padRight$1(formatScore(result), 10);
|
|
567
|
+
const score = padRight$1(formatScore$1(result), 10);
|
|
538
568
|
const status = formatStatus(result);
|
|
539
569
|
console.log(`${name}${version}${score}${status}`);
|
|
540
570
|
}
|
|
@@ -2705,6 +2735,8 @@ async function initCommand(options = {}) {
|
|
|
2705
2735
|
}
|
|
2706
2736
|
//#endregion
|
|
2707
2737
|
//#region ../internals-helpers/dist/index.js
|
|
2738
|
+
const ALPHANUMERIC$1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2739
|
+
`${ALPHANUMERIC$1}`, `${ALPHANUMERIC$1}`, `${ALPHANUMERIC$1}`, `${ALPHANUMERIC$1}`;
|
|
2708
2740
|
function resolve$1(range, versions) {
|
|
2709
2741
|
try {
|
|
2710
2742
|
if (!range || !semver.validRange(range)) return null;
|
|
@@ -2716,6 +2748,196 @@ function resolve$1(range, versions) {
|
|
|
2716
2748
|
}
|
|
2717
2749
|
}
|
|
2718
2750
|
//#endregion
|
|
2751
|
+
//#region src/lib/adapter-rewriter.ts
|
|
2752
|
+
const DEFAULT_TANK_BINARY = "tank";
|
|
2753
|
+
const AUTH_ENV_VAR_PREFIX = "TANK_MCP_AUTH_";
|
|
2754
|
+
const AUTH_PLACEHOLDER_VALUE = "<agent-config-resolves-this>";
|
|
2755
|
+
function deriveAuthEnvVarName(skillName) {
|
|
2756
|
+
const lastSlash = skillName.lastIndexOf("/");
|
|
2757
|
+
return `${AUTH_ENV_VAR_PREFIX}${(lastSlash === -1 ? skillName : skillName.slice(lastSlash + 1)).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase()}`;
|
|
2758
|
+
}
|
|
2759
|
+
function passthroughEntry(server) {
|
|
2760
|
+
if (isRemoteMcpServer(server)) throw new Error("adapter-rewriter: remote MCP servers cannot opt out (proxy transport is mandatory for remote)");
|
|
2761
|
+
const entry = {
|
|
2762
|
+
command: server.command,
|
|
2763
|
+
args: [...server.args]
|
|
2764
|
+
};
|
|
2765
|
+
if (server.env) entry.env = { ...server.env };
|
|
2766
|
+
return entry;
|
|
2767
|
+
}
|
|
2768
|
+
function wrapLocal(server, binary) {
|
|
2769
|
+
const entry = {
|
|
2770
|
+
command: binary,
|
|
2771
|
+
args: [
|
|
2772
|
+
"proxy",
|
|
2773
|
+
"--",
|
|
2774
|
+
server.command,
|
|
2775
|
+
...server.args
|
|
2776
|
+
]
|
|
2777
|
+
};
|
|
2778
|
+
if (server.env) entry.env = { ...server.env };
|
|
2779
|
+
return entry;
|
|
2780
|
+
}
|
|
2781
|
+
function wrapRemote(skillName, server, binary) {
|
|
2782
|
+
const entry = {
|
|
2783
|
+
command: binary,
|
|
2784
|
+
args: [
|
|
2785
|
+
"proxy",
|
|
2786
|
+
"--remote",
|
|
2787
|
+
server.remote
|
|
2788
|
+
]
|
|
2789
|
+
};
|
|
2790
|
+
const mergedEnv = { ...server.env ?? {} };
|
|
2791
|
+
if (server.requires_auth) mergedEnv[deriveAuthEnvVarName(skillName)] = AUTH_PLACEHOLDER_VALUE;
|
|
2792
|
+
if (Object.keys(mergedEnv).length > 0) entry.env = mergedEnv;
|
|
2793
|
+
return entry;
|
|
2794
|
+
}
|
|
2795
|
+
function rewriteMcpServerEntry(options) {
|
|
2796
|
+
const binary = options.tankBinaryPath ?? DEFAULT_TANK_BINARY;
|
|
2797
|
+
if (options.dangerouslyNoTankProxy) return passthroughEntry(options.mcpServer);
|
|
2798
|
+
if (isRemoteMcpServer(options.mcpServer)) return wrapRemote(options.skillName, options.mcpServer, binary);
|
|
2799
|
+
return wrapLocal(options.mcpServer, binary);
|
|
2800
|
+
}
|
|
2801
|
+
//#endregion
|
|
2802
|
+
//#region src/lib/mcp-config-writer.ts
|
|
2803
|
+
const AGENT_CONFIG_PATHS = {
|
|
2804
|
+
claude: [".claude", "settings.json"],
|
|
2805
|
+
cursor: [".cursor", "mcp.json"],
|
|
2806
|
+
opencode: [
|
|
2807
|
+
".config",
|
|
2808
|
+
"opencode",
|
|
2809
|
+
"mcp.json"
|
|
2810
|
+
],
|
|
2811
|
+
codex: [".codex", "config.json"],
|
|
2812
|
+
openclaw: [".openclaw", "mcp.json"],
|
|
2813
|
+
universal: [
|
|
2814
|
+
".config",
|
|
2815
|
+
"mcp",
|
|
2816
|
+
"servers.json"
|
|
2817
|
+
]
|
|
2818
|
+
};
|
|
2819
|
+
function getAgentConfigPath(agentId, homedir) {
|
|
2820
|
+
const segments = AGENT_CONFIG_PATHS[agentId];
|
|
2821
|
+
if (!segments) return null;
|
|
2822
|
+
const base = homedir ?? os.homedir();
|
|
2823
|
+
return path.join(base, ...segments);
|
|
2824
|
+
}
|
|
2825
|
+
function readConfigOrEmpty(configPath) {
|
|
2826
|
+
if (!fs.existsSync(configPath)) return {};
|
|
2827
|
+
try {
|
|
2828
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
2829
|
+
const parsed = JSON.parse(raw);
|
|
2830
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2831
|
+
return parsed;
|
|
2832
|
+
} catch {
|
|
2833
|
+
return {};
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function persistConfig(configPath, config) {
|
|
2837
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
2838
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
2839
|
+
}
|
|
2840
|
+
function writeMcpServerEntry(options) {
|
|
2841
|
+
const configPath = getAgentConfigPath(options.agentId, options.homedir);
|
|
2842
|
+
if (!configPath) throw new Error(`unknown agent: ${options.agentId}`);
|
|
2843
|
+
const existing = readConfigOrEmpty(configPath);
|
|
2844
|
+
const mcpServers = { ...existing.mcpServers ?? {} };
|
|
2845
|
+
if (options.remove) {
|
|
2846
|
+
if (!(options.skillName in mcpServers)) {
|
|
2847
|
+
if (!fs.existsSync(configPath)) return;
|
|
2848
|
+
}
|
|
2849
|
+
delete mcpServers[options.skillName];
|
|
2850
|
+
} else {
|
|
2851
|
+
if (!options.entry) throw new Error("writeMcpServerEntry: entry is required when remove=false");
|
|
2852
|
+
mcpServers[options.skillName] = options.entry;
|
|
2853
|
+
}
|
|
2854
|
+
persistConfig(configPath, {
|
|
2855
|
+
...existing,
|
|
2856
|
+
mcpServers
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
//#endregion
|
|
2860
|
+
//#region src/lib/apply-proxy-wrapping.ts
|
|
2861
|
+
function readManifest(skillDir) {
|
|
2862
|
+
const manifestPath = path.join(skillDir, "tank.json");
|
|
2863
|
+
if (!fs.existsSync(manifestPath)) return "missing";
|
|
2864
|
+
try {
|
|
2865
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
2866
|
+
const parsed = JSON.parse(raw);
|
|
2867
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return "invalid";
|
|
2868
|
+
return parsed;
|
|
2869
|
+
} catch {
|
|
2870
|
+
return "invalid";
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function warnOptOut(skillName) {
|
|
2874
|
+
console.warn(`Proxy disabled for ${skillName} — MCP traffic will not be scanned`);
|
|
2875
|
+
}
|
|
2876
|
+
function isKnownAgent(agentId) {
|
|
2877
|
+
return agentId in AGENT_CONFIG_PATHS;
|
|
2878
|
+
}
|
|
2879
|
+
function applyRemoval(options) {
|
|
2880
|
+
const wrapped = [];
|
|
2881
|
+
for (const agentId of options.agentIds) {
|
|
2882
|
+
if (!isKnownAgent(agentId)) continue;
|
|
2883
|
+
writeMcpServerEntry({
|
|
2884
|
+
agentId,
|
|
2885
|
+
skillName: options.skillName,
|
|
2886
|
+
remove: true,
|
|
2887
|
+
homedir: options.homedir
|
|
2888
|
+
});
|
|
2889
|
+
wrapped.push(agentId);
|
|
2890
|
+
}
|
|
2891
|
+
return {
|
|
2892
|
+
wrapped,
|
|
2893
|
+
skipped: []
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function applyProxyWrapping(options) {
|
|
2897
|
+
if (options.remove) return applyRemoval(options);
|
|
2898
|
+
const manifest = readManifest(options.skillDir);
|
|
2899
|
+
if (manifest === "missing") return {
|
|
2900
|
+
wrapped: [],
|
|
2901
|
+
skipped: ["no-manifest"]
|
|
2902
|
+
};
|
|
2903
|
+
if (manifest === "invalid") return {
|
|
2904
|
+
wrapped: [],
|
|
2905
|
+
skipped: ["invalid-manifest"]
|
|
2906
|
+
};
|
|
2907
|
+
const rawMcpServer = manifest.mcp_server;
|
|
2908
|
+
if (rawMcpServer === void 0) return {
|
|
2909
|
+
wrapped: [],
|
|
2910
|
+
skipped: ["no-mcp-server"]
|
|
2911
|
+
};
|
|
2912
|
+
const parseResult = mcpServerSchema$1.safeParse(rawMcpServer);
|
|
2913
|
+
if (!parseResult.success) return {
|
|
2914
|
+
wrapped: [],
|
|
2915
|
+
skipped: ["invalid-mcp-server"]
|
|
2916
|
+
};
|
|
2917
|
+
const entry = rewriteMcpServerEntry({
|
|
2918
|
+
skillName: options.skillName,
|
|
2919
|
+
mcpServer: parseResult.data,
|
|
2920
|
+
...options.tankBinaryPath !== void 0 ? { tankBinaryPath: options.tankBinaryPath } : {},
|
|
2921
|
+
...options.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
2922
|
+
});
|
|
2923
|
+
if (options.dangerouslyNoTankProxy) warnOptOut(options.skillName);
|
|
2924
|
+
const wrapped = [];
|
|
2925
|
+
for (const agentId of options.agentIds) {
|
|
2926
|
+
if (!isKnownAgent(agentId)) continue;
|
|
2927
|
+
writeMcpServerEntry({
|
|
2928
|
+
agentId,
|
|
2929
|
+
skillName: options.skillName,
|
|
2930
|
+
entry,
|
|
2931
|
+
homedir: options.homedir
|
|
2932
|
+
});
|
|
2933
|
+
wrapped.push(agentId);
|
|
2934
|
+
}
|
|
2935
|
+
return {
|
|
2936
|
+
wrapped,
|
|
2937
|
+
skipped: []
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
//#endregion
|
|
2719
2941
|
//#region ../sdk/dist/index.mjs
|
|
2720
2942
|
var __create = Object.create;
|
|
2721
2943
|
var __defProp = Object.defineProperty;
|
|
@@ -6913,6 +7135,30 @@ object({
|
|
|
6913
7135
|
visibility: _enum(["public", "private"]).optional(),
|
|
6914
7136
|
audit: object({ min_score: number().min(0).max(10) }).strict().optional()
|
|
6915
7137
|
}).strict();
|
|
7138
|
+
const commandSchema = string().min(1, "command must not be empty");
|
|
7139
|
+
const argSchema = array(string()).default([]);
|
|
7140
|
+
const envSchema = record(string(), string()).optional();
|
|
7141
|
+
const remoteUrlSchema = string().url("remote must be a valid URL");
|
|
7142
|
+
const mcpServerSchema = union([object({
|
|
7143
|
+
command: commandSchema,
|
|
7144
|
+
args: argSchema,
|
|
7145
|
+
env: envSchema,
|
|
7146
|
+
requires_auth: literal(false).optional()
|
|
7147
|
+
}).strict(), object({
|
|
7148
|
+
remote: remoteUrlSchema,
|
|
7149
|
+
requires_auth: boolean().default(false),
|
|
7150
|
+
env: envSchema
|
|
7151
|
+
}).strict()]);
|
|
7152
|
+
const perToolOverrideSchema = object({
|
|
7153
|
+
scan: boolean().optional(),
|
|
7154
|
+
blockOnMatch: boolean().optional()
|
|
7155
|
+
}).strict();
|
|
7156
|
+
object({
|
|
7157
|
+
perfBudgetMs: number().positive().optional(),
|
|
7158
|
+
blockOnMatch: boolean().optional(),
|
|
7159
|
+
resetPinsOnMismatch: boolean().optional(),
|
|
7160
|
+
perTool: record(string(), perToolOverrideSchema).optional()
|
|
7161
|
+
}).strict();
|
|
6916
7162
|
const baseManifestFields = {
|
|
6917
7163
|
name: string().min(1, "Name must not be empty").max(214, `Name must be 214 characters or fewer`).regex(/^@[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/, "Name must be scoped (@org/name), lowercase alphanumeric and hyphens"),
|
|
6918
7164
|
version: string().regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/, "Version must be valid semver"),
|
|
@@ -6921,7 +7167,8 @@ const baseManifestFields = {
|
|
|
6921
7167
|
permissions: permissionsSchema.optional(),
|
|
6922
7168
|
repository: string().url("Repository must be a valid URL").optional(),
|
|
6923
7169
|
visibility: _enum(["public", "private"]).optional(),
|
|
6924
|
-
audit: object({ min_score: number().min(0).max(10) }).strict().optional()
|
|
7170
|
+
audit: object({ min_score: number().min(0).max(10) }).strict().optional(),
|
|
7171
|
+
mcp_server: mcpServerSchema.optional()
|
|
6925
7172
|
};
|
|
6926
7173
|
object(baseManifestFields).strict();
|
|
6927
7174
|
object({
|
|
@@ -7019,47 +7266,6 @@ var TankIntegrityError = class extends TankError {
|
|
|
7019
7266
|
}
|
|
7020
7267
|
};
|
|
7021
7268
|
createRequire(import.meta.url);
|
|
7022
|
-
function checkPermissionBudget(budget, skillPerms, skillName) {
|
|
7023
|
-
if (!skillPerms) return;
|
|
7024
|
-
if (skillPerms.subprocess === true && budget.subprocess !== true) throw new TankPermissionError(`${skillName} requires subprocess access, but project budget does not allow it`);
|
|
7025
|
-
if (skillPerms.network?.outbound && skillPerms.network.outbound.length > 0) {
|
|
7026
|
-
const budgetDomains = budget.network?.outbound ?? [];
|
|
7027
|
-
for (const domain of skillPerms.network.outbound) if (!isDomainAllowed$1(domain, budgetDomains)) throw new TankPermissionError(`${skillName} requests network access to "${domain}", which is not in the project's permission budget`);
|
|
7028
|
-
}
|
|
7029
|
-
if (skillPerms.filesystem?.read && skillPerms.filesystem.read.length > 0) {
|
|
7030
|
-
const budgetPaths = budget.filesystem?.read ?? [];
|
|
7031
|
-
for (const p of skillPerms.filesystem.read) if (!isPathAllowed$1(p, budgetPaths)) throw new TankPermissionError(`${skillName} requests filesystem read access to "${p}", which is not in the project's permission budget`);
|
|
7032
|
-
}
|
|
7033
|
-
if (skillPerms.filesystem?.write && skillPerms.filesystem.write.length > 0) {
|
|
7034
|
-
const budgetPaths = budget.filesystem?.write ?? [];
|
|
7035
|
-
for (const p of skillPerms.filesystem.write) if (!isPathAllowed$1(p, budgetPaths)) throw new TankPermissionError(`${skillName} requests filesystem write access to "${p}", which is not in the project's permission budget`);
|
|
7036
|
-
}
|
|
7037
|
-
}
|
|
7038
|
-
function isDomainAllowed$1(domain, allowedDomains) {
|
|
7039
|
-
for (const allowed of allowedDomains) {
|
|
7040
|
-
if (allowed === domain) return true;
|
|
7041
|
-
if (allowed.startsWith("*.")) {
|
|
7042
|
-
const suffix = allowed.slice(1);
|
|
7043
|
-
if (domain.endsWith(suffix) || domain === allowed.slice(2)) return true;
|
|
7044
|
-
if (domain === allowed) return true;
|
|
7045
|
-
}
|
|
7046
|
-
}
|
|
7047
|
-
return false;
|
|
7048
|
-
}
|
|
7049
|
-
function isPathAllowed$1(requestedPath, allowedPaths) {
|
|
7050
|
-
const norm = (p) => p.replaceAll("\\", "/");
|
|
7051
|
-
const req = norm(requestedPath);
|
|
7052
|
-
if (req.includes("..")) return false;
|
|
7053
|
-
for (const allowed of allowedPaths) {
|
|
7054
|
-
const a = norm(allowed);
|
|
7055
|
-
if (a === req) return true;
|
|
7056
|
-
if (a.endsWith("/**")) {
|
|
7057
|
-
const prefix = a.slice(0, -3);
|
|
7058
|
-
if (req === prefix || req.startsWith(`${prefix}/`)) return true;
|
|
7059
|
-
}
|
|
7060
|
-
}
|
|
7061
|
-
return false;
|
|
7062
|
-
}
|
|
7063
7269
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
7064
7270
|
const SEMVER_SPEC_VERSION = "2.0.0";
|
|
7065
7271
|
const MAX_LENGTH = 256;
|
|
@@ -8307,6 +8513,8 @@ var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
8307
8513
|
rcompareIdentifiers: identifiers.rcompareIdentifiers
|
|
8308
8514
|
};
|
|
8309
8515
|
})))(), 1);
|
|
8516
|
+
const ALPHANUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
8517
|
+
`${ALPHANUMERIC}`, `${ALPHANUMERIC}`, `${ALPHANUMERIC}`, `${ALPHANUMERIC}`;
|
|
8310
8518
|
function resolve(range, versions) {
|
|
8311
8519
|
try {
|
|
8312
8520
|
if (!range || !import_semver.default.validRange(range)) return null;
|
|
@@ -8317,6 +8525,68 @@ function resolve(range, versions) {
|
|
|
8317
8525
|
return null;
|
|
8318
8526
|
}
|
|
8319
8527
|
}
|
|
8528
|
+
function isDomainAllowed$1(domain, allowedDomains) {
|
|
8529
|
+
for (const allowed of allowedDomains) {
|
|
8530
|
+
if (allowed === domain) return true;
|
|
8531
|
+
if (allowed.startsWith("*.")) {
|
|
8532
|
+
const suffix = allowed.slice(1);
|
|
8533
|
+
if (domain.endsWith(suffix) || domain === allowed.slice(2)) return true;
|
|
8534
|
+
if (domain === allowed) return true;
|
|
8535
|
+
}
|
|
8536
|
+
}
|
|
8537
|
+
return false;
|
|
8538
|
+
}
|
|
8539
|
+
function isPathAllowed$1(requestedPath, allowedPaths) {
|
|
8540
|
+
const norm = (p) => p.replaceAll("\\", "/");
|
|
8541
|
+
const req = norm(requestedPath);
|
|
8542
|
+
if (req.includes("..")) return false;
|
|
8543
|
+
for (const allowed of allowedPaths) {
|
|
8544
|
+
const a = norm(allowed);
|
|
8545
|
+
if (a === req) return true;
|
|
8546
|
+
if (a.endsWith("/**")) {
|
|
8547
|
+
const prefix = a.slice(0, -3);
|
|
8548
|
+
if (req === prefix || req.startsWith(`${prefix}/`)) return true;
|
|
8549
|
+
}
|
|
8550
|
+
}
|
|
8551
|
+
return false;
|
|
8552
|
+
}
|
|
8553
|
+
/**
|
|
8554
|
+
* Thrown by checkPermissionBudget when a skill's declared permissions exceed the project budget.
|
|
8555
|
+
*
|
|
8556
|
+
* internals-helpers cannot import from @tankpkg/sdk (would invert dep graph).
|
|
8557
|
+
* sdk shim catches this and re-throws as TankPermissionError to preserve
|
|
8558
|
+
* the public sdk error API. See D7 / INTENT C25b.
|
|
8559
|
+
*/
|
|
8560
|
+
var PermissionBudgetError = class extends Error {
|
|
8561
|
+
constructor(message) {
|
|
8562
|
+
super(message);
|
|
8563
|
+
this.name = "PermissionBudgetError";
|
|
8564
|
+
}
|
|
8565
|
+
};
|
|
8566
|
+
function checkPermissionBudget$1(budget, skillPerms, skillName) {
|
|
8567
|
+
if (!skillPerms) return;
|
|
8568
|
+
if (skillPerms.subprocess === true && budget.subprocess !== true) throw new PermissionBudgetError(`${skillName} requires subprocess access, but project budget does not allow it`);
|
|
8569
|
+
if (skillPerms.network?.outbound && skillPerms.network.outbound.length > 0) {
|
|
8570
|
+
const budgetDomains = budget.network?.outbound ?? [];
|
|
8571
|
+
for (const domain of skillPerms.network.outbound) if (!isDomainAllowed$1(domain, budgetDomains)) throw new PermissionBudgetError(`${skillName} requests network access to "${domain}", which is not in the project's permission budget`);
|
|
8572
|
+
}
|
|
8573
|
+
if (skillPerms.filesystem?.read && skillPerms.filesystem.read.length > 0) {
|
|
8574
|
+
const budgetPaths = budget.filesystem?.read ?? [];
|
|
8575
|
+
for (const p of skillPerms.filesystem.read) if (!isPathAllowed$1(p, budgetPaths)) throw new PermissionBudgetError(`${skillName} requests filesystem read access to "${p}", which is not in the project's permission budget`);
|
|
8576
|
+
}
|
|
8577
|
+
if (skillPerms.filesystem?.write && skillPerms.filesystem.write.length > 0) {
|
|
8578
|
+
const budgetPaths = budget.filesystem?.write ?? [];
|
|
8579
|
+
for (const p of skillPerms.filesystem.write) if (!isPathAllowed$1(p, budgetPaths)) throw new PermissionBudgetError(`${skillName} requests filesystem write access to "${p}", which is not in the project's permission budget`);
|
|
8580
|
+
}
|
|
8581
|
+
}
|
|
8582
|
+
function checkPermissionBudget(budget, skillPerms, skillName) {
|
|
8583
|
+
try {
|
|
8584
|
+
checkPermissionBudget$1(budget, skillPerms, skillName);
|
|
8585
|
+
} catch (e) {
|
|
8586
|
+
if (e instanceof PermissionBudgetError) throw new TankPermissionError(e.message);
|
|
8587
|
+
throw e;
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8320
8590
|
function buildSkillKey(name, version) {
|
|
8321
8591
|
return `${name}@${version}`;
|
|
8322
8592
|
}
|
|
@@ -8937,12 +9207,13 @@ const HOST_MAP = [
|
|
|
8937
9207
|
[/registry\.npmjs\.org/i, "npm"]
|
|
8938
9208
|
];
|
|
8939
9209
|
function detectSourceType(url) {
|
|
9210
|
+
if (url.startsWith("file://")) return "file";
|
|
8940
9211
|
for (const [pattern, sourceType] of HOST_MAP) if (pattern.test(url)) return sourceType;
|
|
8941
9212
|
return "unknown";
|
|
8942
9213
|
}
|
|
8943
9214
|
/** Returns true if the input looks like a URL rather than a package name. */
|
|
8944
9215
|
function isUrl(input) {
|
|
8945
|
-
if (input.startsWith("http://") || input.startsWith("https://")) return true;
|
|
9216
|
+
if (input.startsWith("http://") || input.startsWith("https://") || input.startsWith("file://")) return true;
|
|
8946
9217
|
for (const [pattern] of HOST_MAP) if (pattern.test(input)) return true;
|
|
8947
9218
|
return false;
|
|
8948
9219
|
}
|
|
@@ -9226,6 +9497,24 @@ async function fetchFromGenericUrl(url, tempDir) {
|
|
|
9226
9497
|
cleanup: () => cleanupDir(tempDir)
|
|
9227
9498
|
};
|
|
9228
9499
|
}
|
|
9500
|
+
async function fetchFromFileUrl(url, tempDir) {
|
|
9501
|
+
const sourcePath = new URL(url).pathname;
|
|
9502
|
+
const destDir = join(tempDir, "skill");
|
|
9503
|
+
const { cp } = await import("node:fs/promises");
|
|
9504
|
+
await mkdir(destDir, { recursive: true });
|
|
9505
|
+
await cp(sourcePath, destDir, {
|
|
9506
|
+
recursive: true,
|
|
9507
|
+
errorOnExist: false
|
|
9508
|
+
});
|
|
9509
|
+
return {
|
|
9510
|
+
localPath: destDir,
|
|
9511
|
+
sourceType: "file",
|
|
9512
|
+
sourceUrl: url,
|
|
9513
|
+
commitSha: null,
|
|
9514
|
+
inferredName: sourcePath.replace(/\/$/, "").split("/").pop() ?? null,
|
|
9515
|
+
cleanup: () => cleanupDir(tempDir)
|
|
9516
|
+
};
|
|
9517
|
+
}
|
|
9229
9518
|
/** Fetch a skill from a URL to a local temp directory. */
|
|
9230
9519
|
async function fetchFromUrl(url) {
|
|
9231
9520
|
const sourceType = detectSourceType(url);
|
|
@@ -9243,6 +9532,9 @@ async function fetchFromUrl(url) {
|
|
|
9243
9532
|
case "skills_sh":
|
|
9244
9533
|
result = await fetchFromSkillsSh(url, tempDir);
|
|
9245
9534
|
break;
|
|
9535
|
+
case "file":
|
|
9536
|
+
result = await fetchFromFileUrl(url, tempDir);
|
|
9537
|
+
break;
|
|
9246
9538
|
default:
|
|
9247
9539
|
result = await fetchFromGenericUrl(url, tempDir);
|
|
9248
9540
|
break;
|
|
@@ -9397,8 +9689,28 @@ function installToolDependencies(extractDir, skillName) {
|
|
|
9397
9689
|
logger.warn(`Dependency install skipped for ${skillName} (non-fatal)`);
|
|
9398
9690
|
}
|
|
9399
9691
|
}
|
|
9692
|
+
function wrapMcpServerForSkill(options) {
|
|
9693
|
+
try {
|
|
9694
|
+
const agents = detectInstalledAgents(options.homedir);
|
|
9695
|
+
if (agents.length === 0) return;
|
|
9696
|
+
const result = applyProxyWrapping({
|
|
9697
|
+
skillName: options.skillName,
|
|
9698
|
+
skillDir: options.extractDir,
|
|
9699
|
+
agentIds: agents.map((a) => a.id),
|
|
9700
|
+
...options.homedir !== void 0 ? { homedir: options.homedir } : {},
|
|
9701
|
+
...options.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
9702
|
+
});
|
|
9703
|
+
if (result.wrapped.length > 0) {
|
|
9704
|
+
const mode = options.dangerouslyNoTankProxy ? "registered (proxy disabled)" : "proxy-wrapped";
|
|
9705
|
+
logger.info(`${options.skillName}: ${mode} in ${result.wrapped.length} agent config(s)`);
|
|
9706
|
+
}
|
|
9707
|
+
} catch (err) {
|
|
9708
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9709
|
+
logger.warn(`MCP proxy wrapping skipped for ${options.skillName}: ${msg}`);
|
|
9710
|
+
}
|
|
9711
|
+
}
|
|
9400
9712
|
async function linkInstalledRoots(options) {
|
|
9401
|
-
const { rootSkillNames, resolvedNodeByName, extractDirForSkill, directory, global, resolvedHome, homedir } = options;
|
|
9713
|
+
const { rootSkillNames, resolvedNodeByName, extractDirForSkill, directory, global, resolvedHome, homedir, dangerouslyNoTankProxy } = options;
|
|
9402
9714
|
const agentSkillsBaseDir = global ? getGlobalAgentSkillsDir(resolvedHome) : path.join(directory, ".tank", "agent-skills");
|
|
9403
9715
|
const linksDir = global ? path.join(resolvedHome, ".tank") : path.join(directory, ".tank");
|
|
9404
9716
|
for (const skillName of rootSkillNames) try {
|
|
@@ -9418,6 +9730,12 @@ async function linkInstalledRoots(options) {
|
|
|
9418
9730
|
});
|
|
9419
9731
|
if (linkResult.linked.length > 0) logger.info(`Linked to ${linkResult.linked.length} agent(s)`);
|
|
9420
9732
|
if (linkResult.failed.length > 0) for (const failedLink of linkResult.failed) logger.warn(`Failed to link to ${failedLink.agentId}: ${failedLink.error}`);
|
|
9733
|
+
wrapMcpServerForSkill({
|
|
9734
|
+
skillName,
|
|
9735
|
+
extractDir: extractDirForSkill(skillName),
|
|
9736
|
+
homedir,
|
|
9737
|
+
dangerouslyNoTankProxy
|
|
9738
|
+
});
|
|
9421
9739
|
} catch {
|
|
9422
9740
|
if (rootSkillNames.length === 1) logger.warn("Agent linking skipped (non-fatal)");
|
|
9423
9741
|
else logger.warn(`Agent linking skipped for ${skillName} (non-fatal)`);
|
|
@@ -9490,12 +9808,13 @@ async function executeInstallPipeline(options) {
|
|
|
9490
9808
|
directory,
|
|
9491
9809
|
global,
|
|
9492
9810
|
resolvedHome,
|
|
9493
|
-
homedir
|
|
9811
|
+
homedir,
|
|
9812
|
+
...options.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
9494
9813
|
});
|
|
9495
9814
|
return updatedLock;
|
|
9496
9815
|
}
|
|
9497
9816
|
async function installCommand(options) {
|
|
9498
|
-
const { name, versionRange = "*", directory = process.cwd(), configDir, global = false, homedir, isTransitive = false } = options;
|
|
9817
|
+
const { name, versionRange = "*", directory = process.cwd(), configDir, global = false, homedir, isTransitive = false, dangerouslyNoTankProxy } = options;
|
|
9499
9818
|
const config = getConfig(configDir);
|
|
9500
9819
|
const resolvedHome = homedir ?? os.homedir();
|
|
9501
9820
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -9551,7 +9870,8 @@ async function installCommand(options) {
|
|
|
9551
9870
|
rootSkillNames: [name],
|
|
9552
9871
|
projectPermissions,
|
|
9553
9872
|
auditMinScore,
|
|
9554
|
-
spinner
|
|
9873
|
+
spinner,
|
|
9874
|
+
...dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
9555
9875
|
});
|
|
9556
9876
|
if (!global && !isTransitive) {
|
|
9557
9877
|
const skills = skillsJson.skills ?? {};
|
|
@@ -9566,7 +9886,7 @@ async function installCommand(options) {
|
|
|
9566
9886
|
}
|
|
9567
9887
|
}
|
|
9568
9888
|
async function installFromLockfile(options) {
|
|
9569
|
-
const { directory = process.cwd(), configDir, global = false, homedir } = options;
|
|
9889
|
+
const { directory = process.cwd(), configDir, global = false, homedir, dangerouslyNoTankProxy } = options;
|
|
9570
9890
|
const resolvedHome = homedir ?? os.homedir();
|
|
9571
9891
|
const config = getConfig(configDir);
|
|
9572
9892
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -9593,42 +9913,46 @@ async function installFromLockfile(options) {
|
|
|
9593
9913
|
const skillName = parseLockKey$2(key);
|
|
9594
9914
|
const version = parseVersionFromLockKey(key);
|
|
9595
9915
|
spinner.text = `Installing ${key}...`;
|
|
9596
|
-
const encodedName = encodeURIComponent(skillName);
|
|
9597
|
-
const metaUrl = `${config.registry}/api/v1/skills/${encodedName}/${version}`;
|
|
9598
|
-
let metaRes;
|
|
9599
|
-
try {
|
|
9600
|
-
metaRes = await fetch(metaUrl, { headers: requestHeaders });
|
|
9601
|
-
} catch (err) {
|
|
9602
|
-
throw new Error(`Network error fetching ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9603
|
-
}
|
|
9604
|
-
if (!metaRes.ok) {
|
|
9605
|
-
if (metaRes.status === 404) throw new Error(`Skill or version not found: ${key}`);
|
|
9606
|
-
const body = await metaRes.json().catch(() => null);
|
|
9607
|
-
throw new Error(`Failed to fetch ${key}: ${body?.error ?? metaRes.statusText}`);
|
|
9608
|
-
}
|
|
9609
|
-
const downloadUrl = (await metaRes.json()).downloadUrl;
|
|
9610
|
-
const downloadRes = await fetch(downloadUrl);
|
|
9611
|
-
if (!downloadRes.ok) throw new Error(`Failed to download ${key}: ${downloadRes.status} ${downloadRes.statusText}`);
|
|
9612
|
-
const tarballBuffer = Buffer.from(await downloadRes.arrayBuffer());
|
|
9613
|
-
const computedIntegrity = buildIntegrity(tarballBuffer);
|
|
9614
|
-
if (computedIntegrity !== entry.integrity) throw new Error(`Integrity mismatch for ${key}. Expected: ${entry.integrity}, Got: ${computedIntegrity}`);
|
|
9615
9916
|
const extractDir = global ? getGlobalExtractDir(resolvedHome, skillName) : getExtractDir$1(directory, skillName);
|
|
9616
|
-
if (fs.existsSync(
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9917
|
+
if (!fs.existsSync(path.join(extractDir, "tank.json"))) {
|
|
9918
|
+
const encodedName = encodeURIComponent(skillName);
|
|
9919
|
+
const metaUrl = `${config.registry}/api/v1/skills/${encodedName}/${version}`;
|
|
9920
|
+
let metaRes;
|
|
9921
|
+
try {
|
|
9922
|
+
metaRes = await fetch(metaUrl, { headers: requestHeaders });
|
|
9923
|
+
} catch (err) {
|
|
9924
|
+
throw new Error(`Network error fetching ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9925
|
+
}
|
|
9926
|
+
if (!metaRes.ok) {
|
|
9927
|
+
if (metaRes.status === 404) throw new Error(`Skill or version not found: ${key}`);
|
|
9928
|
+
const body = await metaRes.json().catch(() => null);
|
|
9929
|
+
throw new Error(`Failed to fetch ${key}: ${body?.error ?? metaRes.statusText}`);
|
|
9930
|
+
}
|
|
9931
|
+
const downloadUrl = (await metaRes.json()).downloadUrl;
|
|
9932
|
+
const downloadRes = await fetch(downloadUrl);
|
|
9933
|
+
if (!downloadRes.ok) throw new Error(`Failed to download ${key}: ${downloadRes.status} ${downloadRes.statusText}`);
|
|
9934
|
+
const tarballBuffer = Buffer.from(await downloadRes.arrayBuffer());
|
|
9935
|
+
const computedIntegrity = buildIntegrity(tarballBuffer);
|
|
9936
|
+
if (computedIntegrity !== entry.integrity) throw new Error(`Integrity mismatch for ${key}. Expected: ${entry.integrity}, Got: ${computedIntegrity}`);
|
|
9937
|
+
if (fs.existsSync(extractDir)) fs.rmSync(extractDir, {
|
|
9938
|
+
recursive: true,
|
|
9939
|
+
force: true
|
|
9940
|
+
});
|
|
9941
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
9942
|
+
await extractSafely(tarballBuffer, extractDir);
|
|
9943
|
+
}
|
|
9944
|
+
try {
|
|
9945
|
+
const agentSkillsBaseDir = global ? getGlobalAgentSkillsDir(resolvedHome) : path.join(directory, ".tank", "agent-skills");
|
|
9946
|
+
const linksDir = global ? path.join(resolvedHome, ".tank") : path.join(directory, ".tank");
|
|
9623
9947
|
const linkResult = linkSkillToAgents({
|
|
9624
9948
|
skillName,
|
|
9625
9949
|
sourceDir: prepareAgentSkillDir({
|
|
9626
9950
|
skillName,
|
|
9627
9951
|
extractDir,
|
|
9628
|
-
agentSkillsBaseDir
|
|
9952
|
+
agentSkillsBaseDir
|
|
9629
9953
|
}),
|
|
9630
|
-
linksDir
|
|
9631
|
-
source: "global",
|
|
9954
|
+
linksDir,
|
|
9955
|
+
source: global ? "global" : "local",
|
|
9632
9956
|
homedir
|
|
9633
9957
|
});
|
|
9634
9958
|
if (detectInstalledAgents(homedir).length === 0) logger.warn("No agents detected for linking");
|
|
@@ -9637,6 +9961,12 @@ async function installFromLockfile(options) {
|
|
|
9637
9961
|
} catch {
|
|
9638
9962
|
logger.warn("Agent linking skipped (non-fatal)");
|
|
9639
9963
|
}
|
|
9964
|
+
wrapMcpServerForSkill({
|
|
9965
|
+
skillName,
|
|
9966
|
+
extractDir,
|
|
9967
|
+
...homedir !== void 0 ? { homedir } : {},
|
|
9968
|
+
...dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
9969
|
+
});
|
|
9640
9970
|
}
|
|
9641
9971
|
spinner.succeed(`Installed ${entries.length} skill${entries.length === 1 ? "" : "s"} from lockfile`);
|
|
9642
9972
|
} catch (err) {
|
|
@@ -9649,7 +9979,7 @@ async function installFromLockfile(options) {
|
|
|
9649
9979
|
}
|
|
9650
9980
|
}
|
|
9651
9981
|
async function installAll(options) {
|
|
9652
|
-
const { directory = process.cwd(), configDir, global = false, homedir } = options;
|
|
9982
|
+
const { directory = process.cwd(), configDir, global = false, homedir, dangerouslyNoTankProxy } = options;
|
|
9653
9983
|
const resolvedHome = homedir ?? os.homedir();
|
|
9654
9984
|
const config = getConfig(configDir);
|
|
9655
9985
|
const requestHeaders = { "User-Agent": USER_AGENT };
|
|
@@ -9662,7 +9992,8 @@ async function installAll(options) {
|
|
|
9662
9992
|
directory,
|
|
9663
9993
|
configDir,
|
|
9664
9994
|
global,
|
|
9665
|
-
homedir
|
|
9995
|
+
homedir,
|
|
9996
|
+
...dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
9666
9997
|
});
|
|
9667
9998
|
if (global) {
|
|
9668
9999
|
logger.info(`No ${LOCKFILE_FILENAME} found — nothing to install`);
|
|
@@ -9704,7 +10035,8 @@ async function installAll(options) {
|
|
|
9704
10035
|
rootSkillNames: skillEntries.map(([skillName]) => skillName),
|
|
9705
10036
|
projectPermissions,
|
|
9706
10037
|
auditMinScore,
|
|
9707
|
-
spinner
|
|
10038
|
+
spinner,
|
|
10039
|
+
...dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
9708
10040
|
});
|
|
9709
10041
|
spinner.succeed(`Installed ${skillEntries.length} root skill${skillEntries.length === 1 ? "" : "s"}`);
|
|
9710
10042
|
} catch (err) {
|
|
@@ -9757,7 +10089,7 @@ function readManifestFromDir(dir) {
|
|
|
9757
10089
|
return null;
|
|
9758
10090
|
}
|
|
9759
10091
|
async function installFromUrl(url, options) {
|
|
9760
|
-
const { global = false, yes = false } = options;
|
|
10092
|
+
const { global = false, yes = false, dangerouslyNoTankProxy } = options;
|
|
9761
10093
|
const resolvedHome = os.homedir();
|
|
9762
10094
|
const directory = process.cwd();
|
|
9763
10095
|
const spinner = ora(`Fetching from URL...`).start();
|
|
@@ -9778,13 +10110,16 @@ async function installFromUrl(url, options) {
|
|
|
9778
10110
|
process.exit(1);
|
|
9779
10111
|
}
|
|
9780
10112
|
try {
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
10113
|
+
let scanResult = null;
|
|
10114
|
+
if (!url.startsWith("file://")) {
|
|
10115
|
+
scanResult = await scanUrl(url);
|
|
10116
|
+
displayScanResults(scanResult);
|
|
10117
|
+
const enforcement = await enforceVerdict(scanResult, { yes });
|
|
10118
|
+
if (!enforcement.allowed) {
|
|
10119
|
+
spinner.fail(enforcement.reason ?? "Install blocked by security scan");
|
|
10120
|
+
await fetchResult.cleanup();
|
|
10121
|
+
process.exit(1);
|
|
10122
|
+
}
|
|
9788
10123
|
}
|
|
9789
10124
|
const skillMdPath = path.join(fetchResult.localPath, "SKILL.md");
|
|
9790
10125
|
if (!fs.existsSync(skillMdPath)) throw new Error("No SKILL.md found. This doesn't appear to be a valid skill.");
|
|
@@ -9816,12 +10151,12 @@ async function installFromUrl(url, options) {
|
|
|
9816
10151
|
const lockKey = `${skillName}@${skillVersion}`;
|
|
9817
10152
|
const skillPermissions = existingManifest?.permissions ?? {};
|
|
9818
10153
|
lock.skills[lockKey] = {
|
|
9819
|
-
resolved: url.startsWith("http") ? url : `https://${url}`,
|
|
10154
|
+
resolved: url.startsWith("http") ? url : url.startsWith("file://") ? url : `https://${url}`,
|
|
9820
10155
|
integrity,
|
|
9821
10156
|
permissions: skillPermissions,
|
|
9822
|
-
audit_score: scanResult
|
|
10157
|
+
audit_score: scanResult?.auditScore ?? null,
|
|
9823
10158
|
source: mapSourceType(fetchResult.sourceType),
|
|
9824
|
-
scan_verdict: scanResult
|
|
10159
|
+
scan_verdict: scanResult?.verdict ?? "pass",
|
|
9825
10160
|
scanned_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9826
10161
|
};
|
|
9827
10162
|
lock.lockfileVersion = 2;
|
|
@@ -9848,6 +10183,11 @@ async function installFromUrl(url, options) {
|
|
|
9848
10183
|
logger.warn("Agent linking skipped (non-fatal)");
|
|
9849
10184
|
}
|
|
9850
10185
|
if (detectInstalledAgents().length === 0) logger.warn("No agents detected for linking");
|
|
10186
|
+
wrapMcpServerForSkill({
|
|
10187
|
+
skillName,
|
|
10188
|
+
extractDir: installDir,
|
|
10189
|
+
...dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
10190
|
+
});
|
|
9851
10191
|
await fetchResult.cleanup();
|
|
9852
10192
|
spinner.succeed(`Installed ${skillName} from ${fetchResult.sourceType}`);
|
|
9853
10193
|
if (linkedAgents.length > 0) logger.info(`Linked to ${linkedAgents.join(", ")}`);
|
|
@@ -10211,6 +10551,30 @@ async function permissionsCommand(options) {
|
|
|
10211
10551
|
}
|
|
10212
10552
|
}
|
|
10213
10553
|
//#endregion
|
|
10554
|
+
//#region src/commands/proxy.ts
|
|
10555
|
+
const PROXY_MODULE = "@tankpkg/proxy";
|
|
10556
|
+
async function proxyCommand(options) {
|
|
10557
|
+
if (!options.command || options.command.length === 0) throw new Error("tank proxy: missing child command (usage: tank proxy -- <command> [args...])");
|
|
10558
|
+
const { startProxy } = await import(PROXY_MODULE);
|
|
10559
|
+
if (options.verbose) console.error(chalk.dim(`[tank proxy] spawning: ${options.command} ${options.args.join(" ")}`));
|
|
10560
|
+
const startOptions = {
|
|
10561
|
+
command: options.command,
|
|
10562
|
+
args: options.args
|
|
10563
|
+
};
|
|
10564
|
+
if (options.auditPath !== void 0) startOptions.auditPath = options.auditPath;
|
|
10565
|
+
if (options.enableMl === true) startOptions.enableMl = true;
|
|
10566
|
+
const handle = await startProxy(startOptions);
|
|
10567
|
+
const forwardSignal = (signal) => {
|
|
10568
|
+
try {
|
|
10569
|
+
handle.kill(signal);
|
|
10570
|
+
} catch {}
|
|
10571
|
+
};
|
|
10572
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
10573
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
10574
|
+
const code = await handle.exitCode;
|
|
10575
|
+
process.exit(code);
|
|
10576
|
+
}
|
|
10577
|
+
//#endregion
|
|
10214
10578
|
//#region src/lib/packer.ts
|
|
10215
10579
|
const MAX_PACKAGE_SIZE = 50 * 1024 * 1024;
|
|
10216
10580
|
const MAX_FILE_COUNT = 1e3;
|
|
@@ -10892,6 +11256,11 @@ function scoreColor(score) {
|
|
|
10892
11256
|
if (score >= 4) return chalk.yellow;
|
|
10893
11257
|
return chalk.red;
|
|
10894
11258
|
}
|
|
11259
|
+
function formatScore(score) {
|
|
11260
|
+
if (score == null || !Number.isFinite(score)) return chalk.dim(padRight("N/A", 8));
|
|
11261
|
+
const scoreStr = Number.isInteger(score) ? score.toFixed(1) : String(score);
|
|
11262
|
+
return scoreColor(score)(padRight(scoreStr, 8));
|
|
11263
|
+
}
|
|
10895
11264
|
function truncate(text, maxLen) {
|
|
10896
11265
|
if (text.length <= maxLen) return text;
|
|
10897
11266
|
return `${text.slice(0, maxLen - 3)}...`;
|
|
@@ -10924,9 +11293,8 @@ async function searchCommand(options) {
|
|
|
10924
11293
|
console.log(`${padRight("NAME", 30) + padRight("VERSION", 10) + padRight("SCORE", 8)}DESCRIPTION`);
|
|
10925
11294
|
for (const result of data.results) {
|
|
10926
11295
|
const name = chalk.bold(padRight(result.name, 30));
|
|
10927
|
-
const version = padRight(result.latestVersion, 10);
|
|
10928
|
-
const
|
|
10929
|
-
const score = scoreColor(result.auditScore)(padRight(scoreStr, 8));
|
|
11296
|
+
const version = padRight(result.latestVersion ?? "-", 10);
|
|
11297
|
+
const score = formatScore(result.auditScore);
|
|
10930
11298
|
const desc = truncate(result.description ?? "", MAX_DESC_LENGTH);
|
|
10931
11299
|
console.log(`${name}${version}${score}${desc}`);
|
|
10932
11300
|
}
|
|
@@ -11509,18 +11877,23 @@ program.command("publish").alias("pub").description("Pack and publish a skill to
|
|
|
11509
11877
|
process.exit(1);
|
|
11510
11878
|
}
|
|
11511
11879
|
});
|
|
11512
|
-
program.command("install").alias("i").description("Install a skill from the Tank registry, a URL, or all skills from lockfile").argument("[name]", "Skill name or URL (e.g., @org/skill-name or https://github.com/owner/repo). Omit to install from lockfile.").argument("[version-range]", "Semver range (default: *)", "*").option("-g, --global", "Install skill globally (available to all projects)").option("-y, --yes", "Auto-accept flagged scan verdicts").action(async (name, versionRange, opts) => {
|
|
11880
|
+
program.command("install").alias("i").description("Install a skill from the Tank registry, a URL, or all skills from lockfile").argument("[name]", "Skill name or URL (e.g., @org/skill-name or https://github.com/owner/repo). Omit to install from lockfile.").argument("[version-range]", "Semver range (default: *)", "*").option("-g, --global", "Install skill globally (available to all projects)").option("-y, --yes", "Auto-accept flagged scan verdicts").option("--dangerously-no-tank-proxy", "Skip wrapping MCP servers with the tank proxy (no scanning, no enforcement)").action(async (name, versionRange, opts) => {
|
|
11513
11881
|
try {
|
|
11514
11882
|
if (name && isUrl(name)) await installFromUrl(name, {
|
|
11515
11883
|
global: opts.global,
|
|
11516
|
-
yes: opts.yes
|
|
11884
|
+
yes: opts.yes,
|
|
11885
|
+
...opts.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
11517
11886
|
});
|
|
11518
11887
|
else if (name) await installCommand({
|
|
11519
11888
|
name,
|
|
11520
11889
|
versionRange,
|
|
11521
|
-
global: opts.global
|
|
11890
|
+
global: opts.global,
|
|
11891
|
+
...opts.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
11892
|
+
});
|
|
11893
|
+
else await installAll({
|
|
11894
|
+
global: opts.global,
|
|
11895
|
+
...opts.dangerouslyNoTankProxy ? { dangerouslyNoTankProxy: true } : {}
|
|
11522
11896
|
});
|
|
11523
|
-
else await installAll({ global: opts.global });
|
|
11524
11897
|
} catch (err) {
|
|
11525
11898
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11526
11899
|
console.error(`Install failed: ${msg}`);
|
|
@@ -11610,6 +11983,42 @@ program.command("run").description("Launch an agent with credential protection (
|
|
|
11610
11983
|
process.exit(1);
|
|
11611
11984
|
}
|
|
11612
11985
|
});
|
|
11986
|
+
program.command("proxy").description("Transparent MCP proxy — wraps an MCP server with runtime enforcement").argument("[command]", "Child MCP server command to wrap (omit when using --reset-pins, --remote, or download-ml-model)").allowUnknownOption(true).allowExcessArguments(true).option("--audit-path <path>", "JSONL audit log path (default: ~/.tank/proxy/audit.jsonl)").option("--reset-pins", "Delete all rug-pull schema pins under ~/.tank/proxy/pins/ and continue").option("--remote <url>", "Connect to a remote MCP server over SSE/HTTP instead of spawning a child").option("--requires-auth", "Require TANK_MCP_AUTH_<SLUG> env var before connecting to the remote").option("--enable-ml", "Enable the opt-in ML-based prompt-injection classifier (requires ~500MB model; run `tank proxy download-ml-model` first)").option("--verbose", "Print proxy diagnostic details to stderr").action(async (command, opts, cmd) => {
|
|
11987
|
+
try {
|
|
11988
|
+
if (command === "download-ml-model") {
|
|
11989
|
+
const { proxyDownloadMlCommand } = await import("../proxy-download-ml-DUk7ehNs.js");
|
|
11990
|
+
const downloadOpts = {};
|
|
11991
|
+
if (process.argv.includes("--yes") || process.argv.includes("-y")) downloadOpts.yes = true;
|
|
11992
|
+
await proxyDownloadMlCommand(downloadOpts);
|
|
11993
|
+
return;
|
|
11994
|
+
}
|
|
11995
|
+
if (opts.resetPins) {
|
|
11996
|
+
const { proxyResetPinsCommand } = await import("../proxy-reset-pins-CfEbaL-F.js");
|
|
11997
|
+
proxyResetPinsCommand();
|
|
11998
|
+
}
|
|
11999
|
+
if (opts.remote) {
|
|
12000
|
+
const { proxyRemoteCommand } = await import("../proxy-remote-CCemokkz.js");
|
|
12001
|
+
await proxyRemoteCommand({
|
|
12002
|
+
url: opts.remote,
|
|
12003
|
+
requiresAuth: opts.requiresAuth === true
|
|
12004
|
+
});
|
|
12005
|
+
return;
|
|
12006
|
+
}
|
|
12007
|
+
if (!command) return;
|
|
12008
|
+
const proxyOpts = {
|
|
12009
|
+
command,
|
|
12010
|
+
args: cmd.args.slice(1)
|
|
12011
|
+
};
|
|
12012
|
+
if (opts.auditPath !== void 0) proxyOpts.auditPath = opts.auditPath;
|
|
12013
|
+
if (opts.verbose !== void 0) proxyOpts.verbose = opts.verbose;
|
|
12014
|
+
if (opts.enableMl === true) proxyOpts.enableMl = true;
|
|
12015
|
+
await proxyCommand(proxyOpts);
|
|
12016
|
+
} catch (err) {
|
|
12017
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12018
|
+
console.error(`Proxy failed: ${msg}`);
|
|
12019
|
+
process.exit(1);
|
|
12020
|
+
}
|
|
12021
|
+
});
|
|
11613
12022
|
program.command("scan").description("Scan a local skill for security issues without publishing").option("-d, --directory <path>", "Directory to scan (default: current directory)").action(async (opts) => {
|
|
11614
12023
|
try {
|
|
11615
12024
|
await scanCommand({ directory: opts.directory });
|