@yejiming/dsh-data-agent 0.0.1 → 0.0.2

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/lib/routes.js CHANGED
@@ -1,5 +1,5 @@
1
- import { d as parseColumns, f as parseListing, i as DEFAULT_MAX_RESULT_CHARS, m as tableListingSql, o as DEFAULT_QUERY_TIMEOUT_MS, p as parseTableListing, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS, u as metadataQuery } from "./defaults-D__D30ED.js";
2
- import { t as runClientQuery } from "./query-DAjhNTo8.js";
1
+ import { d as metadataQuery, f as parseColumns, g as tableListingSql, h as sanitizeIdentifier, i as DEFAULT_MAX_RESULT_CHARS, l as classifyStatement, m as parseTableListing, o as DEFAULT_QUERY_TIMEOUT_MS, p as parseListing, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-Dgu2B2Yq.js";
2
+ import { t as runClientQuery } from "./query-vK9dr7Z6.js";
3
3
  import { resolve } from "node:path";
4
4
  import z from "schemastery";
5
5
  //#region src/routes.ts
@@ -20,7 +20,8 @@ const Config = z.object({
20
20
  introspectMaxTables: z.number().step(1).min(1).default(500),
21
21
  maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
22
22
  queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
23
- maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS)
23
+ maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
24
+ readonly: z.boolean().default(false)
24
25
  });
25
26
  /**
26
27
  * Validate an untrusted /connect body; sqlite paths resolve to absolute
@@ -51,6 +52,8 @@ function validateConnectBody(value, cwd = process.cwd()) {
51
52
  if (user !== void 0 && typeof user !== "string") throw new Error("user 必须是字符串");
52
53
  const password = candidate.password;
53
54
  if (password !== void 0 && typeof password !== "string") throw new Error("password 必须是字符串");
55
+ const readonly = candidate.readonly;
56
+ if (readonly !== void 0 && typeof readonly !== "boolean") throw new Error("readonly 必须是布尔值");
54
57
  const connection = {
55
58
  type,
56
59
  database
@@ -59,6 +62,7 @@ function validateConnectBody(value, cwd = process.cwd()) {
59
62
  if (port !== void 0) connection.port = port;
60
63
  if (typeof user === "string" && user.length > 0) connection.user = user;
61
64
  if (typeof password === "string" && password.length > 0) connection.password = password;
65
+ if (readonly !== void 0) connection.readonly = readonly;
62
66
  return {
63
67
  sessionId,
64
68
  type,
@@ -66,15 +70,14 @@ function validateConnectBody(value, cwd = process.cwd()) {
66
70
  ...connection.host !== void 0 ? { host: connection.host } : {},
67
71
  ...connection.port !== void 0 ? { port: connection.port } : {},
68
72
  ...connection.user !== void 0 ? { user: connection.user } : {},
69
- ...connection.password !== void 0 ? { password: connection.password } : {}
73
+ ...connection.password !== void 0 ? { password: connection.password } : {},
74
+ ...connection.readonly !== void 0 ? { readonly: connection.readonly } : {}
70
75
  };
71
76
  }
72
- /** Identifier whitelist for schema/table names in metadata queries. */
73
- const IDENTIFIER_PATTERN = /^[A-Za-z0-9_$#.-]+$/;
74
- /** Validate one schema/table identifier (rejects any injection-shaped input). */
75
- function requireIdentifier(value, label) {
77
+ /** Validate one schema/table identifier: reuse the clients' sanitizer (validation + quoting). */
78
+ function requireIdentifier(type, value, label) {
76
79
  if (value === null || value.length === 0) throw new Error(`${label} 不能为空`);
77
- if (!IDENTIFIER_PATTERN.test(value)) throw new Error(`${label} 含非法字符(仅允许字母、数字与 _ $ # . -)`);
80
+ sanitizeIdentifier(type, value);
78
81
  return value;
79
82
  }
80
83
  /**
@@ -148,7 +151,8 @@ function apply(ctx, config) {
148
151
  ...request.host !== void 0 ? { host: request.host } : {},
149
152
  ...request.port !== void 0 ? { port: request.port } : {},
150
153
  ...request.user !== void 0 ? { user: request.user } : {},
151
- ...request.password !== void 0 ? { password: request.password } : {}
154
+ ...request.password !== void 0 ? { password: request.password } : {},
155
+ ...request.readonly !== void 0 ? { readonly: request.readonly } : {}
152
156
  };
153
157
  const listing = await runClientQuery(scope, connection, tableListingSql(connection.type, connection), connectOptions, new AbortController().signal, true);
154
158
  if (listing.exitCode !== 0) {
@@ -199,7 +203,7 @@ function apply(ctx, config) {
199
203
  const sessionId = url.searchParams.get("sessionId") ?? "";
200
204
  if (sessionId.length === 0) throw new Error("sessionId 不能为空");
201
205
  const connection = requireConnection(sessionId);
202
- const schema = connection.type === "sqlite" ? void 0 : requireIdentifier(url.searchParams.get("schema"), "schema");
206
+ const schema = connection.type === "sqlite" ? void 0 : requireIdentifier(connection.type, url.searchParams.get("schema"), "schema");
203
207
  const stdout = await runMetadata(connection, "tables", schema);
204
208
  writeJson(200, {
205
209
  ok: true,
@@ -211,8 +215,8 @@ function apply(ctx, config) {
211
215
  const sessionId = url.searchParams.get("sessionId") ?? "";
212
216
  if (sessionId.length === 0) throw new Error("sessionId 不能为空");
213
217
  const connection = requireConnection(sessionId);
214
- const schema = connection.type === "sqlite" ? void 0 : requireIdentifier(url.searchParams.get("schema"), "schema");
215
- const table = requireIdentifier(url.searchParams.get("table"), "table");
218
+ const schema = connection.type === "sqlite" ? void 0 : requireIdentifier(connection.type, url.searchParams.get("schema"), "schema");
219
+ const table = requireIdentifier(connection.type, url.searchParams.get("table"), "table");
216
220
  const stdout = await runMetadata(connection, "describe", schema, table);
217
221
  writeJson(200, {
218
222
  ok: true,
@@ -228,6 +232,7 @@ function apply(ctx, config) {
228
232
  if (typeof sql !== "string" || sql.trim().length === 0) throw new Error("sql 必须是非空字符串");
229
233
  if (sql.length > config.maxQueryChars) throw new Error(`sql 超过长度上限(${config.maxQueryChars} 字符)`);
230
234
  const connection = requireConnection(sessionId);
235
+ if ((connection.readonly ?? config.readonly) && classifyStatement(sql, connection.type) === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
231
236
  writeJson(200, {
232
237
  ok: true,
233
238
  result: await runClientQuery(scope, connection, sql, queryOptions, new AbortController().signal)
package/lib/tool.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as DEFAULT_MAX_RESULT_CHARS, l as clientsSchema, o as DEFAULT_QUERY_TIMEOUT_MS } from "./defaults-D__D30ED.js";
2
- import { t as runClientQuery } from "./query-DAjhNTo8.js";
1
+ import { i as DEFAULT_MAX_RESULT_CHARS, l as classifyStatement, o as DEFAULT_QUERY_TIMEOUT_MS, u as clientsSchema } from "./defaults-Dgu2B2Yq.js";
2
+ import { t as runClientQuery } from "./query-vK9dr7Z6.js";
3
3
  import z from "schemastery";
4
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
5
  //#region src/tool.ts
@@ -16,6 +16,7 @@ const Config = z.object({
16
16
  queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
17
17
  maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
18
18
  maxRows: z.number().step(1).min(1).default(100),
19
+ readonly: z.boolean().default(false),
19
20
  clients: clientsSchema
20
21
  });
21
22
  /** One-line sqlcmd label for the terminal card (newlines collapsed). */
@@ -42,6 +43,7 @@ function apply(ctx, config) {
42
43
  queryTimeoutMs: config.queryTimeoutMs,
43
44
  maxResultChars: config.maxResultChars,
44
45
  maxRows: config.maxRows,
46
+ readonly: config.readonly,
45
47
  clients: config.clients
46
48
  };
47
49
  ctx.tools.register(defineTool({
@@ -95,6 +97,7 @@ function apply(ctx, config) {
95
97
  if (sessionId === void 0) throw new Error("sqlcmd: 缺少会话上下文(agent loop 未注入)");
96
98
  const connection = ctx.dataAgentConnections.getWithSecret(sessionId);
97
99
  if (connection === void 0) throw new Error("请先在「数据库」标签页连接数据库,再使用 sqlcmd(未找到当前会话的连接)");
100
+ if ((connection.readonly ?? resolved.readonly) && classifyStatement(args.sql, connection.type) === "write") throw new Error("当前连接为只读模式,sqlcmd 拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
98
101
  return runClientQuery(ctx, connection, args.sql, {
99
102
  clients: resolved.clients,
100
103
  timeoutMs: resolved.queryTimeoutMs,
@@ -15,6 +15,10 @@ export declare const zh: {
15
15
  'form.port': string;
16
16
  'form.user': string;
17
17
  'form.password': string;
18
+ 'form.rememberPassword': string;
19
+ 'form.rememberPassword.hint': string;
20
+ 'form.readonly': string;
21
+ 'form.readonly.hint': string;
18
22
  'form.database': string;
19
23
  'form.database.oracle': string;
20
24
  'form.database.hive': string;
@@ -62,6 +66,10 @@ export declare const en: {
62
66
  'form.port': string;
63
67
  'form.user': string;
64
68
  'form.password': string;
69
+ 'form.rememberPassword': string;
70
+ 'form.rememberPassword.hint': string;
71
+ 'form.readonly': string;
72
+ 'form.readonly.hint': string;
65
73
  'form.database': string;
66
74
  'form.database.oracle': string;
67
75
  'form.database.hive': string;
@@ -12,14 +12,17 @@
12
12
  import type { DatabaseType } from './DataAgentWorkbench.tsx';
13
13
  /** localStorage key holding the most recent connection configuration. */
14
14
  export declare const CONNECTION_STORAGE_KEY = "dsh-data-agent.connection.v1";
15
- /** The persisted connection configuration (password included). */
15
+ /** The persisted connection configuration. */
16
16
  export interface SavedConnection {
17
17
  type: DatabaseType;
18
18
  host?: string;
19
19
  port?: number;
20
20
  user?: string;
21
21
  database: string;
22
+ /** Present only when the user explicitly opted in to persist the password. */
22
23
  password?: string;
24
+ /** Opt-in flag; when true, {@link saveConnection} may write `password`. */
25
+ persistPassword?: boolean;
23
26
  /** Diagnostic timestamp of the save. */
24
27
  savedAt: string;
25
28
  }
@@ -12,6 +12,21 @@
12
12
  * @module @yejiming/dsh-data-agent/clients
13
13
  */
14
14
  import type { DatabaseConnection, DatabaseType } from './connections.ts';
15
+ /**
16
+ * Classify a SQL text as a read or write statement by its FIRST effective
17
+ * token (a conservative read whitelist, not a parser). `with` is read only
18
+ * when its body's first token is `select`. `pragma` is read-only for SQLite.
19
+ */
20
+ export declare function classifyStatement(sql: string, type: DatabaseType): 'read' | 'write';
21
+ /**
22
+ * Validate and quote one schema/table identifier for a safe metadata query.
23
+ * Identifiers are restricted to `[A-Za-z0-9_$]+` and then wrapped per type:
24
+ * backticks (mysql/hive/impala) or double quotes (postgres/oracle/sqlite),
25
+ * with the wrapping quote doubled for any interior occurrence. Rejects any
26
+ * input that could cross the identifier boundary (`#`, `--`, `;`, `'`, `` ` ``,
27
+ * `"`, `.`, `-` are all refused).
28
+ */
29
+ export declare function sanitizeIdentifier(type: DatabaseType, identifier: string): string;
15
30
  /** One deployment override for a database type's CLI client. */
16
31
  export interface ClientConfig {
17
32
  /** Executable name (resolved through PATH) or absolute path. */
@@ -37,6 +37,8 @@ export interface DatabaseConnection {
37
37
  database: string;
38
38
  /** In-memory only; never exposed through {@link DataAgentConnections.get}. */
39
39
  password?: string;
40
+ /** Optional per-session read-only guard (defaults to the plugin's `readonly`). */
41
+ readonly?: boolean;
40
42
  tables?: string[];
41
43
  }
42
44
  /** Password-free view of one connection (the wire/UI face). */
@@ -46,6 +48,8 @@ export interface ConnectionSummary {
46
48
  port?: number;
47
49
  user?: string;
48
50
  database: string;
51
+ /** Present only when the connection explicitly set it. */
52
+ readonly?: boolean;
49
53
  tables?: string[];
50
54
  }
51
55
  /** The host-plane connection store service (`ctx.dataAgentConnections`). */
@@ -44,6 +44,8 @@ export interface SeededConnectionConfig {
44
44
  port?: number;
45
45
  user?: string;
46
46
  database: string;
47
+ /** Optional per-seed read-only guard. */
48
+ readonly?: boolean;
47
49
  }
48
50
  /** Required plugin configuration (loader schema with deployment defaults). */
49
51
  export interface Config {
@@ -59,6 +61,8 @@ export interface Config {
59
61
  queryTimeoutMs: number;
60
62
  /** In-memory cap on sqlcmd captured output. */
61
63
  maxResultChars: number;
64
+ /** Default read-only guard: true rejects write statements in sqlcmd//query. */
65
+ readonly: boolean;
62
66
  /** CLI client overrides keyed by database type. */
63
67
  clients: ClientsConfig;
64
68
  /** Config-seeded connections keyed by session id (`'*'` = wildcard default). */
@@ -72,6 +76,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
72
76
  introspectMaxTables: import("@deepseek-ai/schemastery").default<number, number>;
73
77
  queryTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
74
78
  maxResultChars: import("@deepseek-ai/schemastery").default<number, number>;
79
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
75
80
  clients: import("@deepseek-ai/schemastery").default<import("@deepseek-ai/cosmokit").Dict<{
76
81
  command?: string | null | undefined;
77
82
  args?: string[] | null | undefined;
@@ -85,12 +90,14 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
85
90
  port?: number | null | undefined;
86
91
  user?: string | null | undefined;
87
92
  database?: string | null | undefined;
93
+ readonly?: boolean | null | undefined;
88
94
  } & import("cosmokit").Dict, string>, import("@deepseek-ai/cosmokit").Dict<Schemastery.ObjectT<{
89
95
  type: import("@deepseek-ai/schemastery").default<"mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala", "mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala">;
90
96
  host: import("@deepseek-ai/schemastery").default<string, string>;
91
97
  port: import("@deepseek-ai/schemastery").default<number, number>;
92
98
  user: import("@deepseek-ai/schemastery").default<string, string>;
93
99
  database: import("@deepseek-ai/schemastery").default<string, string>;
100
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
94
101
  }>, string>>;
95
102
  }>, Schemastery.ObjectT<{
96
103
  presetId: import("@deepseek-ai/schemastery").default<string, string>;
@@ -99,6 +106,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
99
106
  introspectMaxTables: import("@deepseek-ai/schemastery").default<number, number>;
100
107
  queryTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
101
108
  maxResultChars: import("@deepseek-ai/schemastery").default<number, number>;
109
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
102
110
  clients: import("@deepseek-ai/schemastery").default<import("@deepseek-ai/cosmokit").Dict<{
103
111
  command?: string | null | undefined;
104
112
  args?: string[] | null | undefined;
@@ -112,12 +120,14 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
112
120
  port?: number | null | undefined;
113
121
  user?: string | null | undefined;
114
122
  database?: string | null | undefined;
123
+ readonly?: boolean | null | undefined;
115
124
  } & import("cosmokit").Dict, string>, import("@deepseek-ai/cosmokit").Dict<Schemastery.ObjectT<{
116
125
  type: import("@deepseek-ai/schemastery").default<"mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala", "mysql" | "postgres" | "sqlite" | "oracle" | "hive" | "impala">;
117
126
  host: import("@deepseek-ai/schemastery").default<string, string>;
118
127
  port: import("@deepseek-ai/schemastery").default<number, number>;
119
128
  user: import("@deepseek-ai/schemastery").default<string, string>;
120
129
  database: import("@deepseek-ai/schemastery").default<string, string>;
130
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
121
131
  }>, string>>;
122
132
  }>>;
123
133
  /**
@@ -63,6 +63,8 @@ export interface Config {
63
63
  queryTimeoutMs: number;
64
64
  /** Cap on one /query SQL text length. */
65
65
  maxQueryChars: number;
66
+ /** Read-only guard: true rejects write statements in /query. */
67
+ readonly: boolean;
66
68
  }
67
69
  /** Loader schema with deployment defaults (no library defaults). */
68
70
  export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
@@ -71,12 +73,14 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
71
73
  maxResultChars: import("@deepseek-ai/schemastery").default<number, number>;
72
74
  queryTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
73
75
  maxQueryChars: import("@deepseek-ai/schemastery").default<number, number>;
76
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
74
77
  }>, Schemastery.ObjectT<{
75
78
  connectTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
76
79
  introspectMaxTables: import("@deepseek-ai/schemastery").default<number, number>;
77
80
  maxResultChars: import("@deepseek-ai/schemastery").default<number, number>;
78
81
  queryTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
79
82
  maxQueryChars: import("@deepseek-ai/schemastery").default<number, number>;
83
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
80
84
  }>>;
81
85
  /** The connection request wire body (validated in the /connect handler). */
82
86
  export interface ConnectRequestBody {
@@ -87,6 +91,7 @@ export interface ConnectRequestBody {
87
91
  user?: string;
88
92
  database: string;
89
93
  password?: string;
94
+ readonly?: boolean;
90
95
  }
91
96
  /**
92
97
  * Validate an untrusted /connect body; sqlite paths resolve to absolute
@@ -26,6 +26,8 @@ export interface Config {
26
26
  maxResultChars: number;
27
27
  /** Row-count guidance injected into the tool description. */
28
28
  maxRows: number;
29
+ /** Read-only guard: true rejects write statements. */
30
+ readonly: boolean;
29
31
  /** CLI client overrides keyed by database type. */
30
32
  clients: Partial<Record<string, ClientConfig>>;
31
33
  }
@@ -34,6 +36,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
34
36
  queryTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
35
37
  maxResultChars: import("@deepseek-ai/schemastery").default<number, number>;
36
38
  maxRows: import("@deepseek-ai/schemastery").default<number, number>;
39
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
37
40
  clients: import("@deepseek-ai/schemastery").default<import("@deepseek-ai/cosmokit").Dict<{
38
41
  command?: string | null | undefined;
39
42
  args?: string[] | null | undefined;
@@ -45,6 +48,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
45
48
  queryTimeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
46
49
  maxResultChars: import("@deepseek-ai/schemastery").default<number, number>;
47
50
  maxRows: import("@deepseek-ai/schemastery").default<number, number>;
51
+ readonly: import("@deepseek-ai/schemastery").default<boolean, boolean>;
48
52
  clients: import("@deepseek-ai/schemastery").default<import("@deepseek-ai/cosmokit").Dict<{
49
53
  command?: string | null | undefined;
50
54
  args?: string[] | null | undefined;
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@yejiming/dsh-data-agent",
3
3
  "description": "Data Agent for the dsh web GUI: session-scoped database connections (MySQL/PostgreSQL/SQLite), the sqlcmd tool, the data-agent agent preset, and the database conversation-view tab",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
5
8
  "type": "module",
6
9
  "main": "lib/index.js",
7
10
  "types": "lib/types/index.d.ts",