taurusdb-core 0.4.0 → 0.5.0-rc.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -1
  3. package/dist/audit/jsonl-writer.d.ts +47 -0
  4. package/dist/audit/jsonl-writer.js +124 -0
  5. package/dist/auth/sql-profile-loader/env-source.js +10 -0
  6. package/dist/auth/sql-profile-loader/parsing.js +10 -1
  7. package/dist/auth/sql-profile-loader/runtime-override.d.ts +1 -0
  8. package/dist/auth/sql-profile-loader/runtime-override.js +19 -1
  9. package/dist/auth/sql-profile-loader/types.d.ts +7 -0
  10. package/dist/capability/probe.js +3 -1
  11. package/dist/cloud/auth.d.ts +1 -0
  12. package/dist/cloud/auth.js +37 -0
  13. package/dist/config/env.js +14 -0
  14. package/dist/config/schema.d.ts +68 -0
  15. package/dist/config/schema.js +17 -0
  16. package/dist/context/datasource-resolver.js +7 -0
  17. package/dist/context/session-context.d.ts +7 -0
  18. package/dist/diagnostics/replication-lag.d.ts +4 -0
  19. package/dist/diagnostics/replication-lag.js +138 -0
  20. package/dist/diagnostics/types.d.ts +5 -1
  21. package/dist/diagnostics/types.js +7 -7
  22. package/dist/engine/runtime.d.ts +2 -2
  23. package/dist/engine/runtime.js +8 -14
  24. package/dist/engine/types.d.ts +3 -2
  25. package/dist/engine.d.ts +4 -3
  26. package/dist/engine.js +34 -1
  27. package/dist/executor/bounded-sql.d.ts +1 -0
  28. package/dist/executor/bounded-sql.js +10 -0
  29. package/dist/executor/concurrency-limiter.d.ts +15 -0
  30. package/dist/executor/concurrency-limiter.js +67 -0
  31. package/dist/executor/connection-pool.d.ts +3 -2
  32. package/dist/executor/connection-pool.js +36 -24
  33. package/dist/executor/sql-executor.d.ts +3 -0
  34. package/dist/executor/sql-executor.js +52 -9
  35. package/dist/executor/types.d.ts +5 -1
  36. package/dist/index.d.ts +6 -3
  37. package/dist/index.js +4 -1
  38. package/dist/safety/confirmation-store.d.ts +30 -7
  39. package/dist/safety/confirmation-store.js +154 -30
  40. package/dist/safety/guardrail.d.ts +3 -0
  41. package/dist/safety/guardrail.js +16 -6
  42. package/dist/safety/redaction.d.ts +6 -0
  43. package/dist/safety/redaction.js +47 -6
  44. package/dist/safety/sql-classifier.d.ts +1 -0
  45. package/dist/safety/sql-classifier.js +1 -0
  46. package/dist/safety/sql-validator.d.ts +1 -0
  47. package/dist/safety/sql-validator.js +24 -0
  48. package/dist/schema/adapters/mysql.js +3 -1
  49. package/dist/taurus/flashback.js +4 -10
  50. package/dist/taurus/recycle-bin.js +2 -8
  51. package/dist/utils/formatter.d.ts +6 -2
  52. package/dist/utils/formatter.js +7 -3
  53. package/dist/utils/logger.js +8 -0
  54. package/dist/utils/mysql-identifier.d.ts +1 -0
  55. package/dist/utils/mysql-identifier.js +9 -0
  56. package/package.json +13 -2
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  const DEFAULT_MAX_ROWS = 200;
3
3
  const DEFAULT_MAX_COLUMNS = 50;
4
4
  const DEFAULT_MAX_FIELD_CHARS = 2048;
5
+ const DEFAULT_MAX_RESULT_BYTES = 1048576;
6
+ const DEFAULT_MAX_BLOB_BYTES = 65536;
5
7
  const DEFAULT_SENSITIVE_STRATEGY = "mask";
6
8
  const SENSITIVE_COLUMN_PATTERNS = [
7
9
  /password|passwd|secret/i,
@@ -58,6 +60,14 @@ function isSensitiveColumn(name, explicitSensitive) {
58
60
  const base = candidates[candidates.length - 1];
59
61
  return SENSITIVE_COLUMN_PATTERNS.some((pattern) => pattern.test(base));
60
62
  }
63
+ export function hasSensitiveColumnReference(columns) {
64
+ for (const column of columns) {
65
+ if (isSensitiveColumn(column, new Set())) {
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ }
61
71
  function maskValue(columnName, value) {
62
72
  if (value === null || value === undefined) {
63
73
  return value;
@@ -87,7 +97,19 @@ function hashValue(value) {
87
97
  const digest = createHash("sha256").update(plain).digest("hex").slice(0, 12);
88
98
  return `[HASH:${digest}]`;
89
99
  }
90
- function truncateFieldValue(value, maxFieldChars) {
100
+ function truncateFieldValue(value, maxFieldChars, maxBlobBytes) {
101
+ if (Buffer.isBuffer(value)) {
102
+ if (value.byteLength > maxBlobBytes) {
103
+ return {
104
+ value: `[BINARY:${value.byteLength} bytes; omitted by max_blob_bytes]`,
105
+ truncated: true,
106
+ };
107
+ }
108
+ return {
109
+ value: `[BINARY:${value.byteLength} bytes;base64:${value.toString("base64")}]`,
110
+ truncated: false,
111
+ };
112
+ }
91
113
  if (typeof value !== "string") {
92
114
  return { value, truncated: false };
93
115
  }
@@ -104,12 +126,14 @@ class DefaultResultRedactor {
104
126
  const maxRows = asPositiveInt(policy.maxRows, DEFAULT_MAX_ROWS);
105
127
  const maxColumns = asPositiveInt(policy.maxColumns, DEFAULT_MAX_COLUMNS);
106
128
  const maxFieldChars = asPositiveInt(policy.maxFieldChars, DEFAULT_MAX_FIELD_CHARS);
129
+ const maxResultBytes = asPositiveInt(policy.maxResultBytes, DEFAULT_MAX_RESULT_BYTES);
130
+ const maxBlobBytes = asPositiveInt(policy.maxBlobBytes, DEFAULT_MAX_BLOB_BYTES);
107
131
  const sensitiveStrategy = policy.sensitiveStrategy ?? DEFAULT_SENSITIVE_STRATEGY;
108
132
  const explicitSensitive = buildSensitiveSet(policy.sensitiveColumns);
109
133
  const sourceColumns = Array.isArray(raw.columns) ? raw.columns : [];
110
134
  const sourceRows = Array.isArray(raw.rows) ? raw.rows : [];
111
135
  const originalRowCount = asNonNegativeInt(raw.rowCount, sourceRows.length);
112
- const rowTruncated = sourceRows.length > maxRows || originalRowCount > maxRows;
136
+ let rowTruncated = sourceRows.length > maxRows || originalRowCount > maxRows;
113
137
  const columnTruncated = sourceColumns.length > maxColumns;
114
138
  const rowLimited = sourceRows.slice(0, maxRows);
115
139
  const columnLimited = sourceColumns.slice(0, maxColumns);
@@ -120,7 +144,8 @@ class DefaultResultRedactor {
120
144
  const droppedColumns = [];
121
145
  for (let index = 0; index < columnLimited.length; index += 1) {
122
146
  const column = columnLimited[index];
123
- const sensitive = isSensitiveColumn(column.name, explicitSensitive);
147
+ const sensitive = policy.maskAllColumns === true ||
148
+ isSensitiveColumn(column.name, explicitSensitive);
124
149
  if (sensitive && sensitiveStrategy === "drop") {
125
150
  droppedColumns.push(column.name);
126
151
  continue;
@@ -151,7 +176,7 @@ class DefaultResultRedactor {
151
176
  }
152
177
  }
153
178
  else {
154
- const truncated = truncateFieldValue(rawValue, maxFieldChars);
179
+ const truncated = truncateFieldValue(rawValue, maxFieldChars, maxBlobBytes);
155
180
  value = truncated.value;
156
181
  if (truncated.truncated) {
157
182
  truncatedColumns.add(column.name);
@@ -162,15 +187,31 @@ class DefaultResultRedactor {
162
187
  return outputRow;
163
188
  });
164
189
  const fieldTruncated = truncatedColumns.size > 0;
190
+ let returnedBytes = Buffer.byteLength(JSON.stringify({ columns: keepColumns, rows: [] }), "utf8");
191
+ let byteTruncated = false;
192
+ const byteLimitedRows = [];
193
+ for (const row of outputRows) {
194
+ const rowBytes = Buffer.byteLength(JSON.stringify(row), "utf8");
195
+ const delimiterBytes = byteLimitedRows.length > 0 ? 1 : 0;
196
+ if (returnedBytes + delimiterBytes + rowBytes > maxResultBytes) {
197
+ byteTruncated = true;
198
+ rowTruncated = true;
199
+ break;
200
+ }
201
+ byteLimitedRows.push(row);
202
+ returnedBytes += delimiterBytes + rowBytes;
203
+ }
165
204
  return {
166
205
  columns: keepColumns,
167
- rows: outputRows,
206
+ rows: byteLimitedRows,
168
207
  rowCount: originalRowCount,
169
208
  originalRowCount,
170
- truncated: rowTruncated || columnTruncated || fieldTruncated,
209
+ truncated: rowTruncated || columnTruncated || fieldTruncated || byteTruncated,
171
210
  rowTruncated,
172
211
  columnTruncated,
173
212
  fieldTruncated,
213
+ byteTruncated,
214
+ returnedBytes,
174
215
  redactedColumns: keepColumns
175
216
  .map((column) => column.name)
176
217
  .filter((name) => redactedColumns.has(name)),
@@ -8,6 +8,7 @@ export interface SqlClassification {
8
8
  sqlHash: string;
9
9
  isMultiStatement: boolean;
10
10
  referencedTables: string[];
11
+ referencedSchemas: string[];
11
12
  referencedColumns: string[];
12
13
  hasWhere: boolean;
13
14
  hasLimit: boolean;
@@ -32,6 +32,7 @@ export function classifySql(ast, normalized, engine) {
32
32
  sqlHash: normalized.sqlHash,
33
33
  isMultiStatement: ast.isMultiStatement,
34
34
  referencedTables: extractTables(ast),
35
+ referencedSchemas: dedupeCaseInsensitive(ast.tables.flatMap((table) => (table.schema ? [table.schema] : []))),
35
36
  referencedColumns: extractColumns(ast),
36
37
  hasWhere: ast.where !== undefined,
37
38
  hasLimit: ast.limit !== undefined,
@@ -16,4 +16,5 @@ export interface ExplainRiskSummary {
16
16
  riskHints: string[];
17
17
  }
18
18
  export declare function validateToolScope(toolName: string, cls: SqlClassification): ValidationResult;
19
+ export declare function validateDatabaseScope(cls: SqlClassification, database: string | undefined): ValidationResult;
19
20
  export declare function validateStaticRules(cls: SqlClassification): ValidationResult;
@@ -1,6 +1,7 @@
1
1
  const READONLY_STATEMENTS = new Set(["select", "show", "explain", "describe"]);
2
2
  const MUTATION_STATEMENTS = new Set(["insert", "update", "delete"]);
3
3
  const STAR_COLUMN_PATTERN = /(^|\.)(\*|\(\.\*\))$/;
4
+ const READONLY_SIDE_EFFECT_PATTERN = /\b(?:INTO\s+(?:OUTFILE|DUMPFILE)|FOR\s+UPDATE|LOCK\s+IN\s+SHARE\s+MODE|GET_LOCK\s*\(|RELEASE_LOCK\s*\(|SLEEP\s*\(|BENCHMARK\s*\(|LOAD_FILE\s*\()/i;
4
5
  function allow(riskLevel = "low") {
5
6
  return {
6
7
  action: "allow",
@@ -75,6 +76,23 @@ export function validateToolScope(toolName, cls) {
75
76
  }
76
77
  return allow("low");
77
78
  }
79
+ export function validateDatabaseScope(cls, database) {
80
+ const schemas = cls.referencedSchemas ?? [];
81
+ if (schemas.length === 0) {
82
+ return allow("low");
83
+ }
84
+ if (!database) {
85
+ return block(["D001"], ["SQL uses an explicit database but the session has no bound database."]);
86
+ }
87
+ const expected = database.toLowerCase();
88
+ const outOfScope = schemas.filter((schema) => schema.toLowerCase() !== expected);
89
+ if (outOfScope.length > 0) {
90
+ return block(["D002"], [
91
+ `SQL references database(s) outside the bound database "${database}": ${outOfScope.join(", ")}.`,
92
+ ]);
93
+ }
94
+ return allow("low");
95
+ }
78
96
  export function validateStaticRules(cls) {
79
97
  const reasonCodes = [];
80
98
  const riskHints = [];
@@ -117,7 +135,13 @@ export function validateStaticRules(cls) {
117
135
  escalateToConfirm("R006", "Mutation SQL with WHERE requires confirmation.");
118
136
  }
119
137
  }
138
+ if (cls.statementType === "insert") {
139
+ escalateToConfirm("R006", "INSERT, REPLACE, and UPSERT statements require confirmation.");
140
+ }
120
141
  if (cls.statementType === "select") {
142
+ if (READONLY_SIDE_EFFECT_PATTERN.test(cls.normalizedSql)) {
143
+ escalateToBlock("R009", "SELECT statements with side effects or blocking functions are blocked.");
144
+ }
121
145
  if (!cls.hasLimit && !cls.hasAggregate) {
122
146
  escalateToMediumAllow("R007", "Detail SELECT without LIMIT has medium risk.");
123
147
  }
@@ -272,7 +272,9 @@ export class MySqlSchemaAdapter {
272
272
  }));
273
273
  }
274
274
  async queryObjects(ctx, sql) {
275
- const session = await this.connectionPool.acquire(ctx.datasource, "ro");
275
+ const session = await this.connectionPool.acquire(ctx.datasource, "ro", {
276
+ database: ctx.database,
277
+ });
276
278
  try {
277
279
  const result = await session.execute(sql, { timeoutMs: ctx.limits.timeoutMs });
278
280
  return rowsToObjects(result);
@@ -1,6 +1,6 @@
1
1
  import { createSqlParser } from "../safety/parser/index.js";
2
2
  import { normalizeSql } from "../utils/hash.js";
3
- const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
3
+ import { quoteMysqlIdentifier } from "../utils/mysql-identifier.js";
4
4
  const SQL_TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.\d{1,6})?$/;
5
5
  const RELATIVE_DURATION_PATTERN = /(\d+)\s*(ms|milliseconds?|s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)/gi;
6
6
  const UNIT_TO_MS = {
@@ -26,12 +26,6 @@ function normalizeDurationUnit(unit) {
26
26
  }
27
27
  return "d";
28
28
  }
29
- function quoteIdentifier(identifier, fieldName) {
30
- if (!IDENTIFIER_PATTERN.test(identifier)) {
31
- throw new Error(`Invalid ${fieldName}: "${identifier}".`);
32
- }
33
- return `\`${identifier}\``;
34
- }
35
29
  export function normalizeFlashbackWhereClause(where) {
36
30
  const parser = createSqlParser("mysql");
37
31
  const candidate = parser.normalize(`SELECT 1 FROM placeholder WHERE (${where})`);
@@ -128,10 +122,10 @@ export function resolveFlashbackTimestamp(asOf, now = Date.now) {
128
122
  throw new Error("Flashback query requires either as_of.timestamp or as_of.relative.");
129
123
  }
130
124
  export function buildFlashbackSql(input, defaultDatabase, now = Date.now) {
131
- const database = quoteIdentifier(input.database ?? defaultDatabase, "database");
132
- const table = quoteIdentifier(input.table, "table");
125
+ const database = quoteMysqlIdentifier(input.database ?? defaultDatabase, "database");
126
+ const table = quoteMysqlIdentifier(input.table, "table");
133
127
  const columns = input.columns && input.columns.length > 0
134
- ? input.columns.map((column) => quoteIdentifier(column, "column")).join(", ")
128
+ ? input.columns.map((column) => quoteMysqlIdentifier(column, "column")).join(", ")
135
129
  : "*";
136
130
  const timestamp = resolveFlashbackTimestamp(input.asOf, now);
137
131
  const clauses = [
@@ -1,12 +1,6 @@
1
+ import { quoteMysqlIdentifier } from "../utils/mysql-identifier.js";
1
2
  export const RECYCLE_BIN_DATABASE = "__recyclebin__";
2
- const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
3
3
  const RECYCLE_TABLE_PATTERN = /^[A-Za-z0-9_$@.-]+$/;
4
- function quoteIdentifier(identifier, fieldName) {
5
- if (!SIMPLE_IDENTIFIER_PATTERN.test(identifier)) {
6
- throw new Error(`Invalid ${fieldName}: "${identifier}".`);
7
- }
8
- return `\`${identifier}\``;
9
- }
10
4
  function quoteRecycleTableName(table) {
11
5
  if (!RECYCLE_TABLE_PATTERN.test(table)) {
12
6
  throw new Error(`Invalid recycle_table: "${table}".`);
@@ -45,7 +39,7 @@ export function buildRestoreRecycleBinTableSql(input) {
45
39
  if (!input.destinationDatabase || !input.destinationTable) {
46
40
  throw new Error("insert_select restore requires destination_database and destination_table. Create the destination table with a compatible structure before calling this tool.");
47
41
  }
48
- return `INSERT INTO ${quoteIdentifier(input.destinationDatabase, "destination_database")}.${quoteIdentifier(input.destinationTable, "destination_table")} SELECT * FROM \`${RECYCLE_BIN_DATABASE}\`.${quoteRecycleTableName(input.recycleTable)}`;
42
+ return `INSERT INTO ${quoteMysqlIdentifier(input.destinationDatabase, "destination_database")}.${quoteMysqlIdentifier(input.destinationTable, "destination_table")} SELECT * FROM \`${RECYCLE_BIN_DATABASE}\`.${quoteRecycleTableName(input.recycleTable)}`;
49
43
  }
50
44
  throw new Error(`Unsupported restore method: ${method}.`);
51
45
  }
@@ -11,6 +11,8 @@ export declare const ErrorCode: {
11
11
  readonly QUERY_CANCELLED: "QUERY_CANCELLED";
12
12
  readonly CONNECTION_FAILED: "CONNECTION_FAILED";
13
13
  readonly RESULT_TOO_LARGE: "RESULT_TOO_LARGE";
14
+ readonly AUDIT_FAILED: "AUDIT_FAILED";
15
+ readonly SERVER_BUSY: "SERVER_BUSY";
14
16
  readonly UNSUPPORTED_FEATURE: "UNSUPPORTED_FEATURE";
15
17
  };
16
18
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
@@ -53,7 +55,8 @@ export type FormatBlockedOptions = {
53
55
  details?: Record<string, unknown>;
54
56
  };
55
57
  export type FormatConfirmationRequiredOptions = {
56
- confirmationToken: string;
58
+ approvalRequest: string;
59
+ requestId: string;
57
60
  metadata: ResponseMetadata;
58
61
  summary?: string;
59
62
  message?: string;
@@ -64,7 +67,8 @@ export declare function formatSuccess<T>(data: T, options: FormatSuccessOptions)
64
67
  export declare function formatError<T = unknown>(options: FormatErrorOptions<T>): ToolResponse<T>;
65
68
  export declare function formatBlocked(options: FormatBlockedOptions): ToolResponse;
66
69
  export declare function formatConfirmationRequired(options: FormatConfirmationRequiredOptions): ToolResponse<{
67
- confirmation_token: string;
70
+ approval_request: string;
71
+ request_id: string;
68
72
  risk_level?: string;
69
73
  sql_hash?: string;
70
74
  }>;
@@ -10,6 +10,8 @@ export const ErrorCode = {
10
10
  QUERY_CANCELLED: "QUERY_CANCELLED",
11
11
  CONNECTION_FAILED: "CONNECTION_FAILED",
12
12
  RESULT_TOO_LARGE: "RESULT_TOO_LARGE",
13
+ AUDIT_FAILED: "AUDIT_FAILED",
14
+ SERVER_BUSY: "SERVER_BUSY",
13
15
  UNSUPPORTED_FEATURE: "UNSUPPORTED_FEATURE",
14
16
  };
15
17
  export function formatSuccess(data, options) {
@@ -47,12 +49,14 @@ export function formatBlocked(options) {
47
49
  export function formatConfirmationRequired(options) {
48
50
  return formatError({
49
51
  code: ErrorCode.CONFIRMATION_REQUIRED,
50
- message: options.message ?? "Re-run the same SQL with confirmation_token to continue.",
51
- summary: options.summary ?? "This SQL will modify data and requires explicit confirmation.",
52
+ message: options.message ??
53
+ "An external operator must sign approval_request with `taurusdb-mcp approve` before retrying with approval_token.",
54
+ summary: options.summary ?? "This SQL will modify data and requires external human approval.",
52
55
  retryable: true,
53
56
  metadata: options.metadata,
54
57
  data: {
55
- confirmation_token: options.confirmationToken,
58
+ approval_request: options.approvalRequest,
59
+ request_id: options.requestId,
56
60
  risk_level: options.riskLevel,
57
61
  sql_hash: options.sqlHash,
58
62
  },
@@ -8,6 +8,14 @@ const REDACT_PATHS = [
8
8
  "secret",
9
9
  "token",
10
10
  "*.token",
11
+ "approval_token",
12
+ "*.approval_token",
13
+ "approval_request",
14
+ "*.approval_request",
15
+ "authorization",
16
+ "*.authorization",
17
+ "headers.authorization",
18
+ "*.headers.authorization",
11
19
  "accessKeyId",
12
20
  "*.accessKeyId",
13
21
  "secretAccessKey",
@@ -0,0 +1 @@
1
+ export declare function quoteMysqlIdentifier(identifier: string, fieldName?: string): string;
@@ -0,0 +1,9 @@
1
+ export function quoteMysqlIdentifier(identifier, fieldName = "identifier") {
2
+ if (identifier.length === 0) {
3
+ throw new Error(`Invalid ${fieldName}: value cannot be empty.`);
4
+ }
5
+ if (identifier.includes("\0")) {
6
+ throw new Error(`Invalid ${fieldName}: NUL bytes are not allowed.`);
7
+ }
8
+ return `\`${identifier.replace(/`/g, "``")}\``;
9
+ }
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "taurusdb-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0-rc.1",
4
4
  "description": "Shared TaurusDB data-plane engine for schema, SQL execution, guardrails, and diagnostics.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/youweichen0208/taurus-mcp-server.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "homepage": "https://github.com/youweichen0208/taurus-mcp-server#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/youweichen0208/taurus-mcp-server/issues"
14
+ },
5
15
  "type": "module",
6
16
  "main": "dist/index.js",
7
17
  "types": "dist/index.d.ts",
@@ -16,7 +26,8 @@
16
26
  "guardrails"
17
27
  ],
18
28
  "publishConfig": {
19
- "access": "public"
29
+ "access": "public",
30
+ "provenance": true
20
31
  },
21
32
  "files": [
22
33
  "dist"