@ductape/mcp 0.2.9 → 0.2.10
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 +4 -0
- package/dist/cli-command.d.ts +6 -0
- package/dist/cli-command.d.ts.map +1 -0
- package/dist/cli-command.js +66 -0
- package/dist/index.js +20 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.9
|
|
4
|
+
|
|
5
|
+
- Security: execute `ductape_cli` with an argument array and no shell, reject shell operators/substitutions, and validate quoted command input before checking the administrative subcommand allowlist.
|
|
6
|
+
|
|
3
7
|
## Unreleased
|
|
4
8
|
|
|
5
9
|
- Broadened Feature guidance from durable/event-driven workflows to synchronous or asynchronous named product capabilities.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the user-facing command string into an argv array without invoking a shell.
|
|
3
|
+
* Supports ordinary single/double quoting and backslash escaping for paths/values.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseCliCommand(command: string): string[];
|
|
6
|
+
//# sourceMappingURL=cli-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-command.d.ts","sourceRoot":"","sources":["../src/cli-command.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAmDzD"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Characters with shell control/expansion meaning are never valid in ductape_cli input. */
|
|
2
|
+
const SHELL_META = /[;&|`$<>()\r\n\0]/;
|
|
3
|
+
/**
|
|
4
|
+
* Parse the user-facing command string into an argv array without invoking a shell.
|
|
5
|
+
* Supports ordinary single/double quoting and backslash escaping for paths/values.
|
|
6
|
+
*/
|
|
7
|
+
export function parseCliCommand(command) {
|
|
8
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
9
|
+
throw new Error('A non-empty Ductape CLI command is required.');
|
|
10
|
+
}
|
|
11
|
+
if (SHELL_META.test(command)) {
|
|
12
|
+
throw new Error('Shell operators, substitutions, redirects, and control characters are not allowed.');
|
|
13
|
+
}
|
|
14
|
+
const argv = [];
|
|
15
|
+
let token = '';
|
|
16
|
+
let quote = null;
|
|
17
|
+
let tokenStarted = false;
|
|
18
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
19
|
+
const char = command[index];
|
|
20
|
+
if (quote) {
|
|
21
|
+
if (char === quote) {
|
|
22
|
+
quote = null;
|
|
23
|
+
}
|
|
24
|
+
else if (char === '\\' && quote === '"') {
|
|
25
|
+
index += 1;
|
|
26
|
+
if (index >= command.length)
|
|
27
|
+
throw new Error('Trailing escape in Ductape CLI command.');
|
|
28
|
+
token += command[index];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
token += char;
|
|
32
|
+
}
|
|
33
|
+
tokenStarted = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === "'" || char === '"') {
|
|
37
|
+
quote = char;
|
|
38
|
+
tokenStarted = true;
|
|
39
|
+
}
|
|
40
|
+
else if (char === '\\') {
|
|
41
|
+
index += 1;
|
|
42
|
+
if (index >= command.length)
|
|
43
|
+
throw new Error('Trailing escape in Ductape CLI command.');
|
|
44
|
+
token += command[index];
|
|
45
|
+
tokenStarted = true;
|
|
46
|
+
}
|
|
47
|
+
else if (/\s/.test(char)) {
|
|
48
|
+
if (tokenStarted) {
|
|
49
|
+
argv.push(token);
|
|
50
|
+
token = '';
|
|
51
|
+
tokenStarted = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
token += char;
|
|
56
|
+
tokenStarted = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (quote)
|
|
60
|
+
throw new Error('Unterminated quote in Ductape CLI command.');
|
|
61
|
+
if (tokenStarted)
|
|
62
|
+
argv.push(token);
|
|
63
|
+
if (argv.length === 0)
|
|
64
|
+
throw new Error('A non-empty Ductape CLI command is required.');
|
|
65
|
+
return argv;
|
|
66
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* the env var when provided.
|
|
11
11
|
*/
|
|
12
12
|
import { createRequire } from 'module';
|
|
13
|
-
import { execFileSync
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
|
+
import { parseCliCommand } from './cli-command.js';
|
|
14
15
|
import { homedir } from 'os';
|
|
15
16
|
import { delimiter, join } from 'path';
|
|
16
17
|
import { z } from 'zod';
|
|
@@ -1250,7 +1251,8 @@ const ADMIN_SUBCOMMANDS = [
|
|
|
1250
1251
|
];
|
|
1251
1252
|
function checkCli() {
|
|
1252
1253
|
try {
|
|
1253
|
-
const out =
|
|
1254
|
+
const out = execFileSync('ductape', ['--version'], {
|
|
1255
|
+
shell: false,
|
|
1254
1256
|
encoding: 'utf8',
|
|
1255
1257
|
timeout: 15000,
|
|
1256
1258
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1272,7 +1274,8 @@ function checkLoginState() {
|
|
|
1272
1274
|
// `ductape whoami` only reports whether a local credentials file exists. It does not validate
|
|
1273
1275
|
// the stored token, so an expired token would be cached as authenticated and fail later with 401.
|
|
1274
1276
|
// Use a harmless authenticated read to validate the credential against the API.
|
|
1275
|
-
|
|
1277
|
+
execFileSync('ductape', ['workspaces', 'list', '--json'], {
|
|
1278
|
+
shell: false,
|
|
1276
1279
|
encoding: 'utf8',
|
|
1277
1280
|
timeout: 10000,
|
|
1278
1281
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1293,7 +1296,8 @@ function syncWorkspace() {
|
|
|
1293
1296
|
if (!target)
|
|
1294
1297
|
return;
|
|
1295
1298
|
try {
|
|
1296
|
-
|
|
1299
|
+
execFileSync('ductape', ['workspaces', 'use', target], {
|
|
1300
|
+
shell: false,
|
|
1297
1301
|
encoding: 'utf8',
|
|
1298
1302
|
timeout: 10000,
|
|
1299
1303
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1306,7 +1310,14 @@ function syncWorkspace() {
|
|
|
1306
1310
|
}
|
|
1307
1311
|
}
|
|
1308
1312
|
function runCli(command) {
|
|
1309
|
-
|
|
1313
|
+
let argv;
|
|
1314
|
+
try {
|
|
1315
|
+
argv = parseCliCommand(command);
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
return { success: false, output: `Rejected unsafe ductape_cli command: ${error.message}` };
|
|
1319
|
+
}
|
|
1320
|
+
const first = argv[0];
|
|
1310
1321
|
if (!ADMIN_SUBCOMMANDS.includes(first)) {
|
|
1311
1322
|
return {
|
|
1312
1323
|
success: false,
|
|
@@ -1314,13 +1325,13 @@ function runCli(command) {
|
|
|
1314
1325
|
};
|
|
1315
1326
|
}
|
|
1316
1327
|
// Auto-inject --workspace on login if DUCTAPE_WORKSPACE is set and caller hasn't specified one
|
|
1317
|
-
let finalCommand = command;
|
|
1318
1328
|
const ws = process.env.DUCTAPE_WORKSPACE;
|
|
1319
|
-
if (first === 'login' && ws && !
|
|
1320
|
-
|
|
1329
|
+
if (first === 'login' && ws && !argv.includes('--workspace') && !argv.includes('--skip-workspace-select')) {
|
|
1330
|
+
argv.push('--workspace', ws);
|
|
1321
1331
|
}
|
|
1322
1332
|
try {
|
|
1323
|
-
const output =
|
|
1333
|
+
const output = execFileSync('ductape', argv, {
|
|
1334
|
+
shell: false,
|
|
1324
1335
|
encoding: 'utf8',
|
|
1325
1336
|
// Must exceed the proxy's operation timeout so stderr can preserve the structured timeout
|
|
1326
1337
|
// instead of this wrapper killing the CLI first and reducing it to "(no data)".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ductape/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "MCP server that exposes Ductape SDK operations via the backend proxy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
|
-
"test": "npm run build && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.mjs",
|
|
18
|
+
"test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.mjs",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx src/index.ts"
|
|
21
21
|
},
|