@ductape/mcp 0.2.9 → 0.2.11
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 +50 -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';
|
|
@@ -234,6 +235,36 @@ There are THREE categories of operations. Use the right tool for each:
|
|
|
234
235
|
- For cloud-linked envs, set config.cloud to the connection tag for that env and
|
|
235
236
|
omit raw credentials; the cloud connection must exist for that env too.
|
|
236
237
|
|
|
238
|
+
⚠ PERSIST AGAINST AN ALREADY-REGISTERED COMPONENT TAG CAN SILENTLY NO-OP:
|
|
239
|
+
import-persist-all / provision-persist-all call the component's create method first and only
|
|
240
|
+
fall back to update if create throws. Depending on platform version, create against an existing
|
|
241
|
+
tag can resolve successfully without changing anything instead of throwing — so the response can
|
|
242
|
+
report success (and, for provision, a real cloud resource gets created) while the Ductape
|
|
243
|
+
component record is left completely unchanged, still pointing at whatever it pointed at before.
|
|
244
|
+
Never trust a success response alone when persisting against an existing tag:
|
|
245
|
+
1. ALWAYS re-fetch the component immediately after — ductape_cli("resources <type> get -t <tag> --json")
|
|
246
|
+
2. Diff the fields you meant to change (e.g. envs[].config.cloud) against what you intended.
|
|
247
|
+
3. If unchanged, treat the persist as failed even though it reported success, and say so —
|
|
248
|
+
do not retry the same call in a loop; report it and ask before continuing.
|
|
249
|
+
|
|
250
|
+
⚠ DUCTAPE NEVER DEPROVISIONS REAL CLOUD RESOURCES — THIS IS INTENTIONAL, NOT A GAP:
|
|
251
|
+
There is no verb, flag, or code path anywhere in Ductape (CLI, SDK, or backend) that deletes an
|
|
252
|
+
actual cloud resource (a GCS/S3 bucket, a Pub/Sub/SNS topic, a database instance, etc.). This is a
|
|
253
|
+
deliberate product decision — auto-deprovisioning real infrastructure risks destroying real data,
|
|
254
|
+
so that responsibility is left entirely to the human operator via the cloud provider's own console
|
|
255
|
+
or CLI (gcloud, aws, az, …). Concretely:
|
|
256
|
+
- ductape_cli("resources <type> delete -t <tag>") only clears Ductape's own catalog record
|
|
257
|
+
(soft delete). It never touches the underlying cloud resource — that resource still exists and
|
|
258
|
+
still costs money/still needs manual cleanup after this call succeeds.
|
|
259
|
+
- provision-persist(-all) and import-persist(-all) create/link real cloud resources but there
|
|
260
|
+
is no symmetric "deprovision" or "cloud resources delete" operation — attempting
|
|
261
|
+
ductape_cli("cloud resources delete ...") returns a clear error explaining this by design.
|
|
262
|
+
- If a provisioning attempt fails partway through, or a component is deleted, any real cloud
|
|
263
|
+
resources it created are orphaned and will keep existing (and accruing cost) until the operator
|
|
264
|
+
deletes them manually. Surface this to the user explicitly rather than treating it as something
|
|
265
|
+
this tooling can clean up — do not propose scripting cloud-provider deletes on the user's behalf
|
|
266
|
+
unless they explicitly ask for that as a separate, one-off action outside of Ductape.
|
|
267
|
+
|
|
237
268
|
2. RUNTIME OPERATIONS (run, dispatch, execute, start, send, produce, query, insert, update, delete…)
|
|
238
269
|
The "input" field shape is product- and operation-specific — it is NOT derivable from Joi validators.
|
|
239
270
|
It is defined by how the product's action/feature/session/quota/etc. was configured in Ductape.
|
|
@@ -1250,7 +1281,8 @@ const ADMIN_SUBCOMMANDS = [
|
|
|
1250
1281
|
];
|
|
1251
1282
|
function checkCli() {
|
|
1252
1283
|
try {
|
|
1253
|
-
const out =
|
|
1284
|
+
const out = execFileSync('ductape', ['--version'], {
|
|
1285
|
+
shell: false,
|
|
1254
1286
|
encoding: 'utf8',
|
|
1255
1287
|
timeout: 15000,
|
|
1256
1288
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1272,7 +1304,8 @@ function checkLoginState() {
|
|
|
1272
1304
|
// `ductape whoami` only reports whether a local credentials file exists. It does not validate
|
|
1273
1305
|
// the stored token, so an expired token would be cached as authenticated and fail later with 401.
|
|
1274
1306
|
// Use a harmless authenticated read to validate the credential against the API.
|
|
1275
|
-
|
|
1307
|
+
execFileSync('ductape', ['workspaces', 'list', '--json'], {
|
|
1308
|
+
shell: false,
|
|
1276
1309
|
encoding: 'utf8',
|
|
1277
1310
|
timeout: 10000,
|
|
1278
1311
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1293,7 +1326,8 @@ function syncWorkspace() {
|
|
|
1293
1326
|
if (!target)
|
|
1294
1327
|
return;
|
|
1295
1328
|
try {
|
|
1296
|
-
|
|
1329
|
+
execFileSync('ductape', ['workspaces', 'use', target], {
|
|
1330
|
+
shell: false,
|
|
1297
1331
|
encoding: 'utf8',
|
|
1298
1332
|
timeout: 10000,
|
|
1299
1333
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1306,7 +1340,14 @@ function syncWorkspace() {
|
|
|
1306
1340
|
}
|
|
1307
1341
|
}
|
|
1308
1342
|
function runCli(command) {
|
|
1309
|
-
|
|
1343
|
+
let argv;
|
|
1344
|
+
try {
|
|
1345
|
+
argv = parseCliCommand(command);
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
return { success: false, output: `Rejected unsafe ductape_cli command: ${error.message}` };
|
|
1349
|
+
}
|
|
1350
|
+
const first = argv[0];
|
|
1310
1351
|
if (!ADMIN_SUBCOMMANDS.includes(first)) {
|
|
1311
1352
|
return {
|
|
1312
1353
|
success: false,
|
|
@@ -1314,13 +1355,13 @@ function runCli(command) {
|
|
|
1314
1355
|
};
|
|
1315
1356
|
}
|
|
1316
1357
|
// Auto-inject --workspace on login if DUCTAPE_WORKSPACE is set and caller hasn't specified one
|
|
1317
|
-
let finalCommand = command;
|
|
1318
1358
|
const ws = process.env.DUCTAPE_WORKSPACE;
|
|
1319
|
-
if (first === 'login' && ws && !
|
|
1320
|
-
|
|
1359
|
+
if (first === 'login' && ws && !argv.includes('--workspace') && !argv.includes('--skip-workspace-select')) {
|
|
1360
|
+
argv.push('--workspace', ws);
|
|
1321
1361
|
}
|
|
1322
1362
|
try {
|
|
1323
|
-
const output =
|
|
1363
|
+
const output = execFileSync('ductape', argv, {
|
|
1364
|
+
shell: false,
|
|
1324
1365
|
encoding: 'utf8',
|
|
1325
1366
|
// Must exceed the proxy's operation timeout so stderr can preserve the structured timeout
|
|
1326
1367
|
// 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.11",
|
|
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
|
},
|