dsh-switchman 1.3.1 → 1.6.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/README.de.md +56 -34
- package/README.es.md +53 -31
- package/README.fr.md +52 -30
- package/README.it.md +47 -25
- package/README.ja.md +54 -32
- package/README.ko.md +55 -33
- package/README.md +53 -31
- package/README.pt.md +47 -25
- package/README.ru.md +52 -30
- package/README.zh-TW.md +52 -30
- package/README.zh.md +46 -24
- package/client.js +2344 -40
- package/cordis.patch.yml +24 -8
- package/host/config.js +31 -0
- package/host/context-watch.js +424 -84
- package/host/dispatch.js +107 -6
- package/host/handover-state.js +38 -0
- package/host/lang.js +211 -36
- package/host/lib/dsh-home.js +18 -0
- package/host/routes.js +143 -8
- package/host/subagent-model.js +147 -0
- package/host/subagents.js +160 -0
- package/host/teams.js +583 -0
- package/host/ui-locale.js +35 -0
- package/host/vision.js +367 -0
- package/index.js +40 -8
- package/package.json +1 -1
- package/skills/db-query/package.json +1 -1
- package/skills/db-query/scripts/lib/cli.js +23 -23
- package/skills/db-query/scripts/lib/config.js +78 -61
- package/skills/db-query/scripts/lib/mysql-readonly.js +167 -158
- package/skills/db-query/scripts/lib/output.js +56 -55
- package/skills/db-query/scripts/lib/redis-readonly.js +116 -111
- package/skills/db-query/scripts/mysql-query.js +46 -42
- package/skills/db-query/scripts/redis-query.js +32 -29
- package/skills/db-query/scripts/setup.sh +16 -16
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* dotenv
|
|
4
|
+
* Configuration: env file / environment variables / sanitizing / safe error formatting
|
|
5
|
+
* Only dotenv.parse() is used so process.env stays untouched; env file
|
|
6
|
+
* permissions are checked on POSIX
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const fs = require('fs');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const dotenv = require('dotenv');
|
|
11
12
|
|
|
12
|
-
// ──
|
|
13
|
+
// ── Env file loading (does not touch process.env) ──
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @
|
|
16
|
+
* Load the given env file and return key/value pairs
|
|
17
|
+
* Never reads .env from cwd to avoid accidental leaks; rejects outright when
|
|
18
|
+
* group/other can read it (credential safety)
|
|
19
|
+
* @param {string|undefined} envFile - env file path
|
|
20
|
+
* @returns {object} key/value pairs
|
|
19
21
|
*/
|
|
20
22
|
function loadEnvFile(envFile) {
|
|
21
23
|
if (!envFile) return {};
|
|
22
24
|
const resolved = path.resolve(envFile);
|
|
23
25
|
if (!fs.existsSync(resolved)) {
|
|
24
|
-
throw new Error(`
|
|
26
|
+
throw new Error(`Env file not found: ${resolved}`);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
//
|
|
29
|
+
// Check group/other permissions on POSIX: if other local users can read the
|
|
30
|
+
// file the password leaks — reject outright
|
|
28
31
|
try {
|
|
29
32
|
const stat = fs.statSync(resolved);
|
|
30
33
|
// eslint-disable-next-line no-bitwise
|
|
31
34
|
if ((stat.mode & 0o077) !== 0) {
|
|
32
|
-
throw new ConfigError(`
|
|
35
|
+
throw new ConfigError(`Env file ${resolved} permissions too broad (accessible by group/other), run chmod 600`);
|
|
33
36
|
}
|
|
34
37
|
} catch (e) {
|
|
35
|
-
//
|
|
36
|
-
|
|
38
|
+
// Only skip the permission-check error on Windows and other non-POSIX
|
|
39
|
+
// systems; rethrow everything else (permissions too broad)
|
|
40
|
+
if (e.message.includes('permissions too broad')) throw e;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const content = fs.readFileSync(resolved, 'utf8');
|
|
@@ -41,28 +45,30 @@ function loadEnvFile(envFile) {
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
|
-
*
|
|
45
|
-
* @param {object} envPairs - dotenv.parse()
|
|
46
|
-
* @param {string} envKey -
|
|
47
|
-
* @param {*} cliValue - CLI
|
|
48
|
-
* @returns {*} CLI
|
|
48
|
+
* Read a config value with SKILL.md precedence: CLI > env file > process env > undefined
|
|
49
|
+
* @param {object} envPairs - dotenv.parse() result (explicit --env-file, permission-controlled)
|
|
50
|
+
* @param {string} envKey - environment variable name (e.g. DB_QUERY_MYSQL_HOST)
|
|
51
|
+
* @param {*} cliValue - value from the CLI; undefined means not given on the CLI
|
|
52
|
+
* @returns {*} CLI first, then env file, then process env, otherwise undefined
|
|
49
53
|
*/
|
|
50
54
|
function resolveEnv(envPairs, envKey, cliValue) {
|
|
51
55
|
if (cliValue !== undefined) return cliValue;
|
|
52
|
-
//
|
|
53
|
-
//
|
|
56
|
+
// Env file first (explicitly given, permission-controlled 0600), then the
|
|
57
|
+
// process environment (loadEnvFile deliberately leaves process.env
|
|
58
|
+
// untouched, so fall back to it here or `export DB_QUERY_*` would not work)
|
|
54
59
|
if (envPairs[envKey] !== undefined) return envPairs[envKey];
|
|
55
60
|
if (process.env[envKey] !== undefined) return process.env[envKey];
|
|
56
61
|
return undefined;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
65
|
+
* Parse a number from env/CLI and validate min/max bounds
|
|
66
|
+
* Every source (CLI / env file / environment) runs the same bounds check so
|
|
67
|
+
* an env file cannot bypass hard limits
|
|
62
68
|
*
|
|
63
|
-
* @param {object} envPairs - dotenv.parse()
|
|
64
|
-
* @param {string} envKey -
|
|
65
|
-
* @param {*} cliValue - CLI
|
|
69
|
+
* @param {object} envPairs - dotenv.parse() result
|
|
70
|
+
* @param {string} envKey - environment variable name
|
|
71
|
+
* @param {*} cliValue - value from the CLI (bounds already checked by cli.js)
|
|
66
72
|
* @param {object} bounds - { min?: number, max?: number }
|
|
67
73
|
* @returns {number}
|
|
68
74
|
*/
|
|
@@ -71,29 +77,31 @@ class ConfigError extends Error {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
function resolveEnvNumber(envPairs, envKey, cliValue, bounds) {
|
|
74
|
-
// CLI
|
|
80
|
+
// CLI source: cli.js coerceValue already checked the bounds, return as-is
|
|
75
81
|
if (cliValue !== undefined) return cliValue;
|
|
76
|
-
// env
|
|
77
|
-
// env
|
|
82
|
+
// Env file first, then process env (same precedence as resolveEnv, per the
|
|
83
|
+
// SKILL.md contract); env-sourced values are parsed and bounds-checked
|
|
84
|
+
// uniformly so env cannot bypass hard limits
|
|
78
85
|
const raw = envPairs[envKey] !== undefined ? envPairs[envKey] : process.env[envKey];
|
|
79
86
|
if (raw === undefined) return undefined;
|
|
80
87
|
const n = Number(raw);
|
|
81
88
|
if (!Number.isInteger(n)) {
|
|
82
|
-
throw new ConfigError(
|
|
89
|
+
throw new ConfigError(`Environment variable ${envKey}=${raw} is not a valid integer`);
|
|
83
90
|
}
|
|
84
91
|
if (bounds && bounds.min !== undefined && n < bounds.min) {
|
|
85
|
-
throw new ConfigError(
|
|
92
|
+
throw new ConfigError(`Environment variable ${envKey}=${n} is below the minimum ${bounds.min}`);
|
|
86
93
|
}
|
|
87
94
|
if (bounds && bounds.max !== undefined && n > bounds.max) {
|
|
88
|
-
throw new ConfigError(
|
|
95
|
+
throw new ConfigError(`Environment variable ${envKey}=${n} exceeds the maximum ${bounds.max}`);
|
|
89
96
|
}
|
|
90
97
|
return n;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
// ──
|
|
100
|
+
// ── Connection target sanitizing ──
|
|
94
101
|
|
|
95
102
|
/**
|
|
96
|
-
*
|
|
103
|
+
* Build sanitized connection target info; never includes the password or a
|
|
104
|
+
* password-bearing URI
|
|
97
105
|
* @param {object} opts - { host, port, user, database }
|
|
98
106
|
* @returns {string}
|
|
99
107
|
*/
|
|
@@ -104,14 +112,16 @@ function sanitizeTarget(opts) {
|
|
|
104
112
|
return parts.join(', ');
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
// ──
|
|
115
|
+
// ── Safe error formatting ──
|
|
108
116
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
117
|
+
// Mask passwords inlined in URLs/connection strings (://user:password@host → ://user:***@host).
|
|
118
|
+
// The username segment uses * to also cover the standard no-username Redis
|
|
119
|
+
// form (://:password@host); defensive sanitizing
|
|
111
120
|
const URL_CRED_PATTERN = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/\s:@]*:)[^/\s@]+(@)/g;
|
|
112
121
|
|
|
113
122
|
/**
|
|
114
|
-
*
|
|
123
|
+
* Take the first line of an error message, sanitized and truncated: mask
|
|
124
|
+
* inline credentials + cut at 200 chars to prevent accidental leaks
|
|
115
125
|
* @param {string} s
|
|
116
126
|
* @returns {string}
|
|
117
127
|
*/
|
|
@@ -121,27 +131,28 @@ function sanitizeErrorLine(s) {
|
|
|
121
131
|
return masked.length > 200 ? masked.slice(0, 200) + '…' : masked;
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
//
|
|
134
|
+
// Connection/auth/handshake error codes — classified as "connection failure",
|
|
135
|
+
// clearly distinct from query execution errors
|
|
125
136
|
const CONNECTION_CODES = new Set([
|
|
126
|
-
// Node
|
|
137
|
+
// Node network and system errors
|
|
127
138
|
'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND',
|
|
128
139
|
'EHOSTUNREACH', 'ENETUNREACH', 'EPIPE', 'ECONNABORTED', 'EDESTADDRREQ',
|
|
129
|
-
// DNS
|
|
140
|
+
// DNS resolution errors
|
|
130
141
|
'EAI_AGAIN', 'EAI_NONAME', 'EAI_SERVICE', 'EAI_FAIL', 'EAI_BADFLAGS',
|
|
131
|
-
// TLS
|
|
142
|
+
// TLS errors
|
|
132
143
|
'ERR_TLS_CERT_ALTNAME_INVALID', 'CERT_HAS_EXPIRED',
|
|
133
144
|
'DEPTH_ZERO_SELF_SIGNED_CERT', 'ERR_TLS_INVALID_PROTOCOL_METHOD',
|
|
134
145
|
'ERR_TLS_PROTOCOL_VERSION_CONFLICT',
|
|
135
|
-
// mysql2
|
|
146
|
+
// mysql2 connection/handshake/db-selection stage
|
|
136
147
|
'HANDSHAKE_ERROR', 'PROTOCOL_CONNECTION_LOST',
|
|
137
148
|
'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR', 'PROTOCOL_SEQUENCE_TIMEOUT',
|
|
138
149
|
'ER_ACCESS_DENIED_ERROR', 'ER_DBACCESS_DENIED_ERROR',
|
|
139
150
|
'ER_BAD_DB_ERROR', 'ER_BAD_HOST_ERROR',
|
|
140
|
-
// Redis
|
|
151
|
+
// Redis auth stage
|
|
141
152
|
'WRONGPASS', 'NOAUTH',
|
|
142
153
|
]);
|
|
143
154
|
|
|
144
|
-
// node-redis
|
|
155
|
+
// node-redis command execution error codes (distinct from the connection stage)
|
|
145
156
|
const REDIS_EXEC_CODES = new Set([
|
|
146
157
|
'ERR', 'WRONGTYPE', 'WRONGINT', 'WRONGBIT', 'WRONGKEY',
|
|
147
158
|
'EXECABORT', 'NOREPLICAS', 'MISCONF', 'BUSYGROUP', 'NOGROUP',
|
|
@@ -150,14 +161,18 @@ const REDIS_EXEC_CODES = new Set([
|
|
|
150
161
|
]);
|
|
151
162
|
|
|
152
163
|
/**
|
|
153
|
-
*
|
|
164
|
+
* Format a database error safely so a triaging agent can see the real failure
|
|
165
|
+
* at a glance.
|
|
154
166
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
167
|
+
* Key security premise: messages of mysql2 ER_* / node-redis command errors
|
|
168
|
+
* come from the database server and contain no client credentials (the server
|
|
169
|
+
* never sees the client password); Node network errors such as
|
|
170
|
+
* "connect ECONNREFUSED 127.0.0.1:3306" contain no password either. So
|
|
171
|
+
* exposing the code plus the sanitized first message line, classified by
|
|
172
|
+
* code, is safe and lets the agent tell "cannot connect" from "bad SQL" in
|
|
173
|
+
* one step, avoiding pointless bisection. Only pathological errors with
|
|
174
|
+
* neither code nor message get a neutral fallback, no longer misreported as
|
|
175
|
+
* "connection failure".
|
|
161
176
|
* @param {Error} err
|
|
162
177
|
* @returns {string}
|
|
163
178
|
*/
|
|
@@ -168,29 +183,31 @@ function safeMessage(err) {
|
|
|
168
183
|
if (code) {
|
|
169
184
|
const codeStr = String(code);
|
|
170
185
|
if (CONNECTION_CODES.has(codeStr)) {
|
|
171
|
-
return
|
|
186
|
+
return `Connection failed [${codeStr}]: ${line}`;
|
|
172
187
|
}
|
|
173
|
-
// mysql2
|
|
188
|
+
// mysql2 server-side execution errors: ER_BAD_FIELD_ERROR(1054)/ER_PARSE_ERROR/ER_NO_SUCH_TABLE etc.
|
|
174
189
|
if (/^ER_/.test(codeStr)) {
|
|
175
|
-
return
|
|
190
|
+
return `Query execution error [${codeStr}]: ${line}`;
|
|
176
191
|
}
|
|
177
|
-
// node-redis
|
|
192
|
+
// node-redis command execution errors
|
|
178
193
|
if (REDIS_EXEC_CODES.has(codeStr)) {
|
|
179
|
-
return
|
|
194
|
+
return `Query execution error [${codeStr}]: ${line}`;
|
|
180
195
|
}
|
|
181
|
-
//
|
|
196
|
+
// Unknown code: still expose code + first line (sanitized and
|
|
197
|
+
// truncated); more useful for triage than "details hidden"
|
|
182
198
|
return `[${codeStr}] ${line}`;
|
|
183
199
|
}
|
|
184
200
|
|
|
185
|
-
//
|
|
201
|
+
// No code: cannot classify, use neutral wording instead of a misleading
|
|
202
|
+
// "connection failure"
|
|
186
203
|
const name = err.name || 'Error';
|
|
187
|
-
return `${name}: ${line || '
|
|
204
|
+
return `${name}: ${line || '(no error message; check the SQL/command and connection parameters)'}`;
|
|
188
205
|
}
|
|
189
206
|
|
|
190
|
-
// ── TLS
|
|
207
|
+
// ── TLS options ──
|
|
191
208
|
|
|
192
209
|
/**
|
|
193
|
-
*
|
|
210
|
+
* Build TLS options from a ca-file path
|
|
194
211
|
* @param {string|undefined} caFile
|
|
195
212
|
* @returns {object|undefined}
|
|
196
213
|
*/
|
|
@@ -198,7 +215,7 @@ function buildTlsOptions(caFile) {
|
|
|
198
215
|
if (!caFile) return undefined;
|
|
199
216
|
const resolved = path.resolve(caFile);
|
|
200
217
|
if (!fs.existsSync(resolved)) {
|
|
201
|
-
throw new Error(`CA
|
|
218
|
+
throw new Error(`CA certificate file not found: ${resolved}`);
|
|
202
219
|
}
|
|
203
220
|
const ca = fs.readFileSync(resolved);
|
|
204
221
|
return { ca, rejectUnauthorized: true };
|