taurusdb-core 0.3.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 (68) 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 +11 -2
  7. package/dist/auth/sql-profile-loader/runtime-override.js +10 -2
  8. package/dist/auth/sql-profile-loader/types.d.ts +5 -0
  9. package/dist/capability/probe.js +3 -1
  10. package/dist/cloud/auth.d.ts +8 -0
  11. package/dist/cloud/auth.js +61 -3
  12. package/dist/cloud/csms.d.ts +7 -0
  13. package/dist/cloud/csms.js +60 -0
  14. package/dist/cloud/keychain.d.ts +28 -0
  15. package/dist/cloud/keychain.js +200 -0
  16. package/dist/cloud/kms.d.ts +7 -0
  17. package/dist/cloud/kms.js +86 -0
  18. package/dist/config/env.js +20 -0
  19. package/dist/config/schema.d.ts +88 -0
  20. package/dist/config/schema.js +21 -0
  21. package/dist/context/datasource-resolver.js +7 -0
  22. package/dist/context/session-context.d.ts +7 -0
  23. package/dist/diagnostics/metrics-source.d.ts +3 -0
  24. package/dist/diagnostics/metrics-source.js +7 -2
  25. package/dist/diagnostics/replication-lag.d.ts +4 -0
  26. package/dist/diagnostics/replication-lag.js +138 -0
  27. package/dist/diagnostics/slow-sql-source/das-source.d.ts +3 -0
  28. package/dist/diagnostics/slow-sql-source/das-source.js +3 -0
  29. package/dist/diagnostics/slow-sql-source/factory.js +9 -2
  30. package/dist/diagnostics/slow-sql-source/taurus-api-source.d.ts +3 -0
  31. package/dist/diagnostics/slow-sql-source/taurus-api-source.js +3 -0
  32. package/dist/diagnostics/types.d.ts +5 -1
  33. package/dist/diagnostics/types.js +7 -7
  34. package/dist/engine/runtime.d.ts +2 -2
  35. package/dist/engine/runtime.js +8 -14
  36. package/dist/engine/types.d.ts +3 -2
  37. package/dist/engine.d.ts +4 -3
  38. package/dist/engine.js +44 -2
  39. package/dist/executor/bounded-sql.d.ts +1 -0
  40. package/dist/executor/bounded-sql.js +10 -0
  41. package/dist/executor/concurrency-limiter.d.ts +15 -0
  42. package/dist/executor/concurrency-limiter.js +67 -0
  43. package/dist/executor/connection-pool.d.ts +3 -2
  44. package/dist/executor/connection-pool.js +36 -24
  45. package/dist/executor/sql-executor.d.ts +3 -0
  46. package/dist/executor/sql-executor.js +52 -9
  47. package/dist/executor/types.d.ts +5 -1
  48. package/dist/index.d.ts +13 -4
  49. package/dist/index.js +7 -1
  50. package/dist/safety/confirmation-store.d.ts +30 -7
  51. package/dist/safety/confirmation-store.js +154 -30
  52. package/dist/safety/guardrail.d.ts +3 -0
  53. package/dist/safety/guardrail.js +16 -6
  54. package/dist/safety/redaction.d.ts +6 -0
  55. package/dist/safety/redaction.js +47 -6
  56. package/dist/safety/sql-classifier.d.ts +1 -0
  57. package/dist/safety/sql-classifier.js +1 -0
  58. package/dist/safety/sql-validator.d.ts +1 -0
  59. package/dist/safety/sql-validator.js +24 -0
  60. package/dist/schema/adapters/mysql.js +3 -1
  61. package/dist/taurus/flashback.js +4 -10
  62. package/dist/taurus/recycle-bin.js +2 -8
  63. package/dist/utils/formatter.d.ts +6 -2
  64. package/dist/utils/formatter.js +7 -3
  65. package/dist/utils/logger.js +8 -0
  66. package/dist/utils/mysql-identifier.d.ts +1 -0
  67. package/dist/utils/mysql-identifier.js +9 -0
  68. package/package.json +14 -3
@@ -1,4 +1,4 @@
1
- import { fetchHuaweiCloud } from "../cloud/auth.js";
1
+ import { fetchHuaweiCloud, getHuaweiCloudAuthFromConfig } from "../cloud/auth.js";
2
2
  const TAURUS_CES_METRICS = {
3
3
  cpu_util: "gaussdb_mysql001_cpu_util",
4
4
  mem_util: "gaussdb_mysql002_mem_util",
@@ -132,6 +132,7 @@ export class CesMetricsSource {
132
132
  accessKeyId;
133
133
  secretAccessKey;
134
134
  securityToken;
135
+ credentialProvider;
135
136
  namespace;
136
137
  instanceDimension;
137
138
  nodeDimension;
@@ -149,6 +150,7 @@ export class CesMetricsSource {
149
150
  this.accessKeyId = options.accessKeyId;
150
151
  this.secretAccessKey = options.secretAccessKey;
151
152
  this.securityToken = options.securityToken;
153
+ this.credentialProvider = options.credentialProvider;
152
154
  this.namespace = options.namespace;
153
155
  this.instanceDimension = options.instanceDimension;
154
156
  this.nodeDimension = options.nodeDimension;
@@ -228,6 +230,7 @@ export class CesMetricsSource {
228
230
  accessKeyId: this.accessKeyId,
229
231
  secretAccessKey: this.secretAccessKey,
230
232
  securityToken: this.securityToken,
233
+ credentialProvider: this.credentialProvider,
231
234
  },
232
235
  fetchImpl: (input, init) => this.fetchImpl(input, {
233
236
  ...init,
@@ -257,7 +260,8 @@ export function createMetricsSource(config) {
257
260
  !ces.projectId ||
258
261
  !ces.instanceId ||
259
262
  (!ces.authToken &&
260
- !(config.cloud?.accessKeyId && config.cloud?.secretAccessKey))) {
263
+ !(config.cloud?.accessKeyId && config.cloud?.secretAccessKey) &&
264
+ !config.cloud?.keychainService)) {
261
265
  return undefined;
262
266
  }
263
267
  return new CesMetricsSource({
@@ -269,6 +273,7 @@ export function createMetricsSource(config) {
269
273
  accessKeyId: config.cloud?.accessKeyId,
270
274
  secretAccessKey: config.cloud?.secretAccessKey,
271
275
  securityToken: config.cloud?.securityToken,
276
+ credentialProvider: getHuaweiCloudAuthFromConfig(config).credentialProvider,
272
277
  namespace: ces.namespace,
273
278
  instanceDimension: ces.instanceDimension,
274
279
  nodeDimension: ces.nodeDimension,
@@ -0,0 +1,4 @@
1
+ import type { SessionContext } from "../context/session-context.js";
2
+ import type { SqlExecutor } from "../executor/sql-executor.js";
3
+ import type { DiagnoseReplicationLagInput, DiagnosticResult } from "./types.js";
4
+ export declare function diagnoseReplicationLag(executor: SqlExecutor, input: DiagnoseReplicationLagInput, ctx: SessionContext): Promise<DiagnosticResult>;
@@ -0,0 +1,138 @@
1
+ function rowsAsObjects(result) {
2
+ return result.rows.map((row) => Object.fromEntries(result.columns.map((column, index) => [column.name, row[index]])));
3
+ }
4
+ function text(row, ...names) {
5
+ for (const name of names) {
6
+ const value = row[name];
7
+ if (value !== undefined && value !== null && String(value).trim()) {
8
+ return String(value).trim();
9
+ }
10
+ }
11
+ return undefined;
12
+ }
13
+ function number(row, ...names) {
14
+ const value = text(row, ...names);
15
+ if (value === undefined) {
16
+ return undefined;
17
+ }
18
+ const parsed = Number(value);
19
+ return Number.isFinite(parsed) ? parsed : undefined;
20
+ }
21
+ async function readReplicationStatus(executor, ctx) {
22
+ try {
23
+ return rowsAsObjects(await executor.executeReadonly("SHOW REPLICA STATUS", ctx, {
24
+ maxRows: 20,
25
+ maxColumns: 80,
26
+ maxFieldChars: 512,
27
+ maxResultBytes: 262144,
28
+ }));
29
+ }
30
+ catch {
31
+ return rowsAsObjects(await executor.executeReadonly("SHOW SLAVE STATUS", ctx, {
32
+ maxRows: 20,
33
+ maxColumns: 80,
34
+ maxFieldChars: 512,
35
+ maxResultBytes: 262144,
36
+ }));
37
+ }
38
+ }
39
+ export async function diagnoseReplicationLag(executor, input, ctx) {
40
+ let rows;
41
+ try {
42
+ rows = await readReplicationStatus(executor, ctx);
43
+ }
44
+ catch {
45
+ rows = [];
46
+ }
47
+ const filtered = rows.filter((row) => {
48
+ const channel = text(row, "Channel_Name");
49
+ const replicaId = text(row, "Server_Id", "Source_Server_Id", "Master_Server_Id");
50
+ return ((!input.channel || channel === input.channel) &&
51
+ (!input.replicaId || replicaId === input.replicaId));
52
+ });
53
+ const window = {
54
+ from: input.timeRange?.from,
55
+ to: input.timeRange?.to,
56
+ relative: input.timeRange?.relative,
57
+ };
58
+ if (filtered.length === 0) {
59
+ return {
60
+ tool: "diagnose_replication_lag",
61
+ status: "not_applicable",
62
+ severity: "info",
63
+ summary: "No replication channel was visible on the selected database endpoint.",
64
+ diagnosisWindow: window,
65
+ rootCauseCandidates: [],
66
+ keyFindings: [
67
+ "SHOW REPLICA STATUS and SHOW SLAVE STATUS returned no matching channel.",
68
+ ],
69
+ evidence: [],
70
+ recommendedActions: [
71
+ "Confirm that the selected endpoint is a read replica and that the read-only SQL user can inspect replication status.",
72
+ ],
73
+ limitations: [
74
+ "A primary or standalone endpoint legitimately has no replica status.",
75
+ "Cloud time-series replication metrics are not included in this data-plane-only result.",
76
+ ],
77
+ };
78
+ }
79
+ const findings = filtered.map((row) => {
80
+ const lag = number(row, "Seconds_Behind_Source", "Seconds_Behind_Master");
81
+ const io = text(row, "Replica_IO_Running", "Slave_IO_Running") ?? "unknown";
82
+ const sql = text(row, "Replica_SQL_Running", "Slave_SQL_Running") ?? "unknown";
83
+ const channel = text(row, "Channel_Name") ?? "default";
84
+ return { lag, io, sql, channel };
85
+ });
86
+ const maxLag = Math.max(...findings.map((item) => item.lag ?? 0));
87
+ const stopped = findings.some((item) => item.io.toLowerCase() === "no" || item.sql.toLowerCase() === "no");
88
+ const severity = stopped || maxLag >= 300 ? "high" : maxLag >= 30 ? "warning" : "info";
89
+ const status = stopped || maxLag >= 30 ? "inconclusive" : "ok";
90
+ return {
91
+ tool: "diagnose_replication_lag",
92
+ status,
93
+ severity,
94
+ summary: stopped
95
+ ? "At least one replication worker is not running."
96
+ : `Observed maximum replication delay of ${maxLag} seconds.`,
97
+ diagnosisWindow: window,
98
+ rootCauseCandidates: [
99
+ ...(stopped
100
+ ? [{
101
+ code: "replication_worker_stopped",
102
+ title: "Replication worker stopped",
103
+ confidence: "high",
104
+ rationale: "Replica IO or SQL worker reports No.",
105
+ }]
106
+ : []),
107
+ ...(maxLag >= 30
108
+ ? [{
109
+ code: "replication_delay",
110
+ title: "Replica apply delay",
111
+ confidence: "high",
112
+ rationale: `Seconds behind source reached ${maxLag}.`,
113
+ }]
114
+ : []),
115
+ ],
116
+ keyFindings: findings.map((item) => `Channel ${item.channel}: lag=${item.lag ?? "unknown"}s, io=${item.io}, sql=${item.sql}.`),
117
+ evidence: [{
118
+ source: "replication_status",
119
+ title: "Current replication status",
120
+ summary: `Collected ${findings.length} matching replication channel(s).`,
121
+ }],
122
+ recommendedActions: stopped
123
+ ? [
124
+ "Inspect Last_IO_Error and Last_SQL_Error directly with a privileged DBA account.",
125
+ "Do not skip replication errors until the failed event and data consistency impact are understood.",
126
+ ]
127
+ : maxLag >= 30
128
+ ? [
129
+ "Check long transactions, write bursts, DDL, and replica resource saturation.",
130
+ "Compare lag over time in Cloud Eye before changing replica topology.",
131
+ ]
132
+ : ["Continue monitoring the replication delay trend."],
133
+ limitations: [
134
+ "This result is a point-in-time data-plane snapshot.",
135
+ "Error text and relay-log SQL are intentionally not returned to the MCP client.",
136
+ ],
137
+ };
138
+ }
@@ -1,3 +1,4 @@
1
+ import { type HuaweiCloudCredentialProvider } from "../../cloud/auth.js";
1
2
  import type { SessionContext } from "../../context/session-context.js";
2
3
  import type { FindTopSlowSqlInput } from "../types.js";
3
4
  import type { ExternalSlowSqlSample, ResolveSlowSqlInput, SlowSqlSource } from "./types.js";
@@ -9,6 +10,7 @@ type DasSlowSqlSourceOptions = {
9
10
  accessKeyId?: string;
10
11
  secretAccessKey?: string;
11
12
  securityToken?: string;
13
+ credentialProvider?: HuaweiCloudCredentialProvider;
12
14
  datastoreType: "MySQL" | "TaurusDB";
13
15
  requestTimeoutMs: number;
14
16
  defaultLookbackMinutes: number;
@@ -24,6 +26,7 @@ export declare class DasSlowSqlSource implements SlowSqlSource {
24
26
  private readonly accessKeyId?;
25
27
  private readonly secretAccessKey?;
26
28
  private readonly securityToken?;
29
+ private readonly credentialProvider?;
27
30
  private readonly datastoreType;
28
31
  private readonly requestTimeoutMs;
29
32
  private readonly defaultLookbackMinutes;
@@ -9,6 +9,7 @@ export class DasSlowSqlSource {
9
9
  accessKeyId;
10
10
  secretAccessKey;
11
11
  securityToken;
12
+ credentialProvider;
12
13
  datastoreType;
13
14
  requestTimeoutMs;
14
15
  defaultLookbackMinutes;
@@ -23,6 +24,7 @@ export class DasSlowSqlSource {
23
24
  this.accessKeyId = options.accessKeyId;
24
25
  this.secretAccessKey = options.secretAccessKey;
25
26
  this.securityToken = options.securityToken;
27
+ this.credentialProvider = options.credentialProvider;
26
28
  this.datastoreType = options.datastoreType;
27
29
  this.requestTimeoutMs = options.requestTimeoutMs;
28
30
  this.defaultLookbackMinutes = options.defaultLookbackMinutes;
@@ -149,6 +151,7 @@ export class DasSlowSqlSource {
149
151
  accessKeyId: this.accessKeyId,
150
152
  secretAccessKey: this.secretAccessKey,
151
153
  securityToken: this.securityToken,
154
+ credentialProvider: this.credentialProvider,
152
155
  },
153
156
  fetchImpl: (input, init) => this.fetchImpl(input, {
154
157
  ...init,
@@ -1,3 +1,4 @@
1
+ import { getHuaweiCloudAuthFromConfig } from "../../cloud/auth.js";
1
2
  import { DasSlowSqlSource } from "./das-source.js";
2
3
  import { TaurusApiSlowSqlSource } from "./taurus-api-source.js";
3
4
  import { sortExternalSamples } from "./utils.js";
@@ -28,6 +29,7 @@ class CompositeSlowSqlSource {
28
29
  }
29
30
  }
30
31
  export function createSlowSqlSource(config) {
32
+ const credentialProvider = getHuaweiCloudAuthFromConfig(config).credentialProvider;
31
33
  const sources = [];
32
34
  const taurusApi = config.slowSqlSource?.taurusApi;
33
35
  if (taurusApi?.enabled &&
@@ -36,7 +38,8 @@ export function createSlowSqlSource(config) {
36
38
  taurusApi.instanceId &&
37
39
  taurusApi.nodeId &&
38
40
  (taurusApi.authToken ||
39
- (config.cloud?.accessKeyId && config.cloud?.secretAccessKey))) {
41
+ (config.cloud?.accessKeyId && config.cloud?.secretAccessKey) ||
42
+ config.cloud?.keychainService)) {
40
43
  sources.push(new TaurusApiSlowSqlSource({
41
44
  endpoint: taurusApi.endpoint,
42
45
  projectId: taurusApi.projectId,
@@ -46,6 +49,7 @@ export function createSlowSqlSource(config) {
46
49
  accessKeyId: config.cloud?.accessKeyId,
47
50
  secretAccessKey: config.cloud?.secretAccessKey,
48
51
  securityToken: config.cloud?.securityToken,
52
+ credentialProvider,
49
53
  language: taurusApi.language,
50
54
  requestTimeoutMs: taurusApi.requestTimeoutMs,
51
55
  defaultLookbackMinutes: taurusApi.defaultLookbackMinutes,
@@ -57,7 +61,9 @@ export function createSlowSqlSource(config) {
57
61
  das.endpoint &&
58
62
  das.projectId &&
59
63
  das.instanceId &&
60
- (das.authToken || (config.cloud?.accessKeyId && config.cloud?.secretAccessKey))) {
64
+ (das.authToken ||
65
+ (config.cloud?.accessKeyId && config.cloud?.secretAccessKey) ||
66
+ config.cloud?.keychainService)) {
61
67
  sources.push(new DasSlowSqlSource({
62
68
  endpoint: das.endpoint,
63
69
  projectId: das.projectId,
@@ -66,6 +72,7 @@ export function createSlowSqlSource(config) {
66
72
  accessKeyId: config.cloud?.accessKeyId,
67
73
  secretAccessKey: config.cloud?.secretAccessKey,
68
74
  securityToken: config.cloud?.securityToken,
75
+ credentialProvider,
69
76
  datastoreType: das.datastoreType,
70
77
  requestTimeoutMs: das.requestTimeoutMs,
71
78
  defaultLookbackMinutes: das.defaultLookbackMinutes,
@@ -1,3 +1,4 @@
1
+ import { type HuaweiCloudCredentialProvider } from "../../cloud/auth.js";
1
2
  import type { SessionContext } from "../../context/session-context.js";
2
3
  import type { FindTopSlowSqlInput } from "../types.js";
3
4
  import type { ExternalSlowSqlSample, ResolveSlowSqlInput, SlowSqlSource } from "./types.js";
@@ -10,6 +11,7 @@ type TaurusApiSlowSqlSourceOptions = {
10
11
  accessKeyId?: string;
11
12
  secretAccessKey?: string;
12
13
  securityToken?: string;
14
+ credentialProvider?: HuaweiCloudCredentialProvider;
13
15
  language: "en-us" | "zh-cn";
14
16
  requestTimeoutMs: number;
15
17
  defaultLookbackMinutes: number;
@@ -25,6 +27,7 @@ export declare class TaurusApiSlowSqlSource implements SlowSqlSource {
25
27
  private readonly accessKeyId?;
26
28
  private readonly secretAccessKey?;
27
29
  private readonly securityToken?;
30
+ private readonly credentialProvider?;
28
31
  private readonly language;
29
32
  private readonly requestTimeoutMs;
30
33
  private readonly defaultLookbackMinutes;
@@ -10,6 +10,7 @@ export class TaurusApiSlowSqlSource {
10
10
  accessKeyId;
11
11
  secretAccessKey;
12
12
  securityToken;
13
+ credentialProvider;
13
14
  language;
14
15
  requestTimeoutMs;
15
16
  defaultLookbackMinutes;
@@ -24,6 +25,7 @@ export class TaurusApiSlowSqlSource {
24
25
  this.accessKeyId = options.accessKeyId;
25
26
  this.secretAccessKey = options.secretAccessKey;
26
27
  this.securityToken = options.securityToken;
28
+ this.credentialProvider = options.credentialProvider;
27
29
  this.language = options.language;
28
30
  this.requestTimeoutMs = options.requestTimeoutMs;
29
31
  this.defaultLookbackMinutes = options.defaultLookbackMinutes;
@@ -128,6 +130,7 @@ export class TaurusApiSlowSqlSource {
128
130
  accessKeyId: this.accessKeyId,
129
131
  secretAccessKey: this.secretAccessKey,
130
132
  securityToken: this.securityToken,
133
+ credentialProvider: this.credentialProvider,
131
134
  },
132
135
  fetchImpl: (input, init) => this.fetchImpl(input, {
133
136
  ...init,
@@ -1,4 +1,4 @@
1
- export type DiagnosticToolName = "diagnose_service_latency" | "diagnose_db_hotspot" | "find_top_slow_sql" | "diagnose_slow_query" | "diagnose_connection_spike" | "diagnose_lock_contention" | "diagnose_storage_pressure";
1
+ export type DiagnosticToolName = "diagnose_service_latency" | "diagnose_db_hotspot" | "find_top_slow_sql" | "diagnose_slow_query" | "diagnose_connection_spike" | "diagnose_lock_contention" | "diagnose_replication_lag" | "diagnose_storage_pressure";
2
2
  export type DiagnosticStatus = "ok" | "inconclusive" | "not_applicable";
3
3
  export type DiagnosticSeverity = "info" | "warning" | "high" | "critical";
4
4
  export type DiagnosticEvidenceLevel = "basic" | "standard" | "full";
@@ -46,6 +46,10 @@ export interface DiagnoseStoragePressureInput extends DiagnosticBaseInput {
46
46
  scope?: "instance" | "database" | "table";
47
47
  table?: string;
48
48
  }
49
+ export interface DiagnoseReplicationLagInput extends DiagnosticBaseInput {
50
+ replicaId?: string;
51
+ channel?: string;
52
+ }
49
53
  export interface DiagnosticRootCauseCandidate {
50
54
  code: string;
51
55
  title: string;
@@ -11,29 +11,29 @@ export function createPlaceholderDiagnosticResult(tool, input, options) {
11
11
  },
12
12
  rootCauseCandidates: [
13
13
  {
14
- code: `${tool}_pending`,
14
+ code: `${tool}_insufficient_evidence`,
15
15
  title: options.candidateTitle,
16
16
  confidence: "low",
17
17
  rationale: options.candidateRationale,
18
18
  },
19
19
  ],
20
20
  keyFindings: options.keyFindings ?? [
21
- "The diagnostic contract is available, but evidence collectors have not been wired yet.",
22
- "No live control-plane or data-plane evidence was collected in this run.",
21
+ "No conclusive live control-plane or data-plane evidence matched this diagnosis.",
22
+ "The result is intentionally inconclusive rather than inferring a root cause without evidence.",
23
23
  ],
24
24
  suspiciousEntities: options.suspiciousEntities,
25
25
  evidence: options.evidence ?? [
26
26
  {
27
- source: "diagnostics_scaffold",
28
- title: "Diagnostic scaffold active",
29
- summary: "Typed inputs, outputs, and engine entrypoints are in place, but collectors and analyzers are pending.",
27
+ source: "diagnostic_limitations",
28
+ title: "No conclusive evidence",
29
+ summary: "The configured collectors returned no evidence sufficient for a root-cause conclusion.",
30
30
  },
31
31
  ],
32
32
  recommendedActions: options.recommendedActions ?? [
33
33
  "Implement the required collectors before relying on this tool for production diagnosis.",
34
34
  ],
35
35
  limitations: options.limitations ?? [
36
- "This diagnostic currently returns a scaffolded result instead of live evidence-backed analysis.",
36
+ "The configured datasource, permissions, time window, or cloud metric sources did not provide sufficient evidence.",
37
37
  ],
38
38
  };
39
39
  }
@@ -1,6 +1,6 @@
1
1
  import type { SessionContext } from "../context/session-context.js";
2
2
  import type { CancelResult, ExplainResult, MutationOptions, MutationResult, QueryResult, QueryStatus, ReadonlyOptions } from "../executor/sql-executor.js";
3
- import type { ConfirmationToken, ConfirmationValidationResult } from "../safety/confirmation-store.js";
3
+ import type { ConfirmationRequest, ConfirmationValidationResult } from "../safety/confirmation-store.js";
4
4
  import type { GuardrailDecision } from "../safety/guardrail.js";
5
5
  import { type FlashbackInput } from "../taurus/flashback.js";
6
6
  import { type RestoreRecycleBinTableInput } from "../taurus/recycle-bin.js";
@@ -14,7 +14,7 @@ export declare function listRecycleBin(engine: TaurusDBEngine, ctx: SessionConte
14
14
  export declare function restoreRecycleBinTable(engine: TaurusDBEngine, input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
15
15
  export declare function getQueryStatus(engine: TaurusDBEngine, queryId: string): Promise<QueryStatus>;
16
16
  export declare function cancelQuery(engine: TaurusDBEngine, queryId: string): Promise<CancelResult>;
17
- export declare function issueConfirmation(engine: TaurusDBEngine, input: IssueConfirmationInput): Promise<ConfirmationToken>;
17
+ export declare function issueConfirmation(engine: TaurusDBEngine, input: IssueConfirmationInput): Promise<ConfirmationRequest>;
18
18
  export declare function validateConfirmation(engine: TaurusDBEngine, token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
19
19
  export declare function handleConfirmation(engine: TaurusDBEngine, decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
20
20
  export declare function close(engine: TaurusDBEngine): Promise<void>;
@@ -3,6 +3,7 @@ import { InMemoryConfirmationStore } from "../safety/confirmation-store.js";
3
3
  import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, normalizeFlashbackWhereClause, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
4
4
  import { buildListRecycleBinSql, buildRestoreRecycleBinTableSql, recycleBinMutationOptions, recycleBinReadonlyOptions } from "../taurus/recycle-bin.js";
5
5
  import { normalizeSql, sqlHash } from "../utils/hash.js";
6
+ import { quoteMysqlIdentifier } from "../utils/mysql-identifier.js";
6
7
  function resolveConfirmationSql(input) {
7
8
  const normalized = input.normalizedSql ?? (input.sql ? normalizeSql(input.sql) : undefined);
8
9
  const hash = input.sqlHash ?? (normalized ? sqlHash(normalized) : undefined);
@@ -26,13 +27,6 @@ function hasSqlPattern(sql, pattern) {
26
27
  return pattern.test(sql);
27
28
  }
28
29
  const NO_FLASHBACK_VIEW_PATTERN = /No view available for provided TIMESTAMP/i;
29
- const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
30
- function quoteIdentifier(identifier) {
31
- if (!SIMPLE_IDENTIFIER_PATTERN.test(identifier)) {
32
- throw new Error(`Invalid identifier: "${identifier}".`);
33
- }
34
- return `\`${identifier}\``;
35
- }
36
30
  function firstRowAsObject(result) {
37
31
  if (result.rows.length === 0) {
38
32
  return undefined;
@@ -91,7 +85,7 @@ async function buildFlashbackNoViewError(engine, ctx, input, database, requested
91
85
  if (input.where?.trim()) {
92
86
  try {
93
87
  const where = normalizeFlashbackWhereClause(input.where);
94
- const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteIdentifier("updated_at")} FROM ${quoteIdentifier(database)}.${quoteIdentifier(input.table)} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
88
+ const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteMysqlIdentifier("updated_at")} FROM ${quoteMysqlIdentifier(database, "database")}.${quoteMysqlIdentifier(input.table, "table")} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
95
89
  const updatedAtRow = firstRowAsObject(updatedAtResult);
96
90
  if (typeof updatedAtRow?.updated_at === "string") {
97
91
  details.current_row_updated_at = updatedAtRow.updated_at;
@@ -341,7 +335,6 @@ export async function restoreRecycleBinTable(engine, input, ctx, opts) {
341
335
  }
342
336
  return engine.executor.executeMutation(buildRestoreRecycleBinTableSql(input), ctx, {
343
337
  ...recycleBinMutationOptions(opts),
344
- allowReadonlyFallbackForMutations: true,
345
338
  });
346
339
  }
347
340
  export async function getQueryStatus(engine, queryId) {
@@ -367,17 +360,18 @@ export async function handleConfirmation(engine, decision, ctx) {
367
360
  if (!decision.requiresConfirmation) {
368
361
  return { status: "confirmed" };
369
362
  }
370
- const token = await engine.confirmationStore.issue({
363
+ const approval = await engine.confirmationStore.issue({
371
364
  sqlHash: decision.sqlHash,
372
365
  normalizedSql: decision.normalizedSql,
373
366
  context: ctx,
374
367
  riskLevel: decision.riskLevel,
375
368
  });
376
369
  return {
377
- status: "token_issued",
378
- token: token.token,
379
- issuedAt: token.issuedAt,
380
- expiresAt: token.expiresAt,
370
+ status: "approval_required",
371
+ request: approval.request,
372
+ requestId: approval.requestId,
373
+ issuedAt: approval.issuedAt,
374
+ expiresAt: approval.expiresAt,
381
375
  };
382
376
  }
383
377
  export async function close(engine) {
@@ -31,8 +31,9 @@ export type IssueConfirmationInput = {
31
31
  export type ConfirmationOutcome = {
32
32
  status: "confirmed";
33
33
  } | {
34
- status: "token_issued";
35
- token: string;
34
+ status: "approval_required";
35
+ request: string;
36
+ requestId: string;
36
37
  issuedAt: number;
37
38
  expiresAt: number;
38
39
  };
package/dist/engine.d.ts CHANGED
@@ -6,12 +6,12 @@ import { type Config } from "./config/index.js";
6
6
  import type { DatasourceResolveInput, DatasourceResolver, SessionContext } from "./context/session-context.js";
7
7
  import { type ConnectionPool } from "./executor/connection-pool.js";
8
8
  import { type CancelResult, type ExplainResult, type MutationOptions, type MutationResult, type QueryResult, type QueryStatus, type ReadonlyOptions, type SqlExecutor } from "./executor/sql-executor.js";
9
- import { type ConfirmationStore, type ConfirmationToken, type ConfirmationValidationResult } from "./safety/confirmation-store.js";
9
+ import { type ConfirmationStore, type ConfirmationRequest, type ConfirmationValidationResult } from "./safety/confirmation-store.js";
10
10
  import { type Guardrail, type GuardrailDecision, type InspectInput } from "./safety/guardrail.js";
11
11
  import { type DatabaseInfo, type SchemaIntrospector, type TableInfo, type TableSchema } from "./schema/introspector.js";
12
12
  import { type FlashbackInput } from "./taurus/flashback.js";
13
13
  import { type RestoreRecycleBinTableInput } from "./taurus/recycle-bin.js";
14
- import { type DbHotspotResult, type DiagnoseDbHotspotInput, type DiagnoseServiceLatencyInput, type FindTopSlowSqlInput, type FindTopSlowSqlResult, type DiagnoseConnectionSpikeInput, type DiagnoseLockContentionInput, type DiagnoseSlowQueryInput, type DiagnoseStoragePressureInput, type DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.js";
14
+ import { type DbHotspotResult, type DiagnoseDbHotspotInput, type DiagnoseServiceLatencyInput, type FindTopSlowSqlInput, type FindTopSlowSqlResult, type DiagnoseConnectionSpikeInput, type DiagnoseLockContentionInput, type DiagnoseSlowQueryInput, type DiagnoseStoragePressureInput, type DiagnoseReplicationLagInput, type DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.js";
15
15
  import { type SlowSqlSource } from "./diagnostics/slow-sql-source.js";
16
16
  import { type MetricsSource } from "./diagnostics/metrics-source.js";
17
17
  import type { ConfirmationOutcome, DataSourceInfo, EnhancedExplainResult, IssueConfirmationInput, ShowLockWaitsInput, ShowProcesslistInput, TaurusDBEngineCreateOptions, TaurusDBEngineDeps } from "./engine/types.js";
@@ -44,6 +44,7 @@ export declare class TaurusDBEngine {
44
44
  listFeatures(ctx: SessionContext): Promise<FeatureMatrix>;
45
45
  showProcesslist(input: ShowProcesslistInput, ctx: SessionContext): Promise<QueryResult>;
46
46
  showLockWaits(input: ShowLockWaitsInput, ctx: SessionContext): Promise<QueryResult>;
47
+ diagnoseReplicationLag(input: DiagnoseReplicationLagInput, ctx: SessionContext): Promise<DiagnosticResult>;
47
48
  findMetadataLockWaits(input: DiagnoseLockContentionInput, ctx: SessionContext): Promise<MetadataLockRow[]>;
48
49
  findLatestDeadlock(ctx: SessionContext): Promise<DeadlockSummary | undefined>;
49
50
  findStatementDigestSample(digestText: string, ctx: SessionContext): Promise<StatementDigestRow | undefined>;
@@ -70,7 +71,7 @@ export declare class TaurusDBEngine {
70
71
  restoreRecycleBinTable(input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
71
72
  getQueryStatus(queryId: string): Promise<QueryStatus>;
72
73
  cancelQuery(queryId: string): Promise<CancelResult>;
73
- issueConfirmation(input: IssueConfirmationInput): Promise<ConfirmationToken>;
74
+ issueConfirmation(input: IssueConfirmationInput): Promise<ConfirmationRequest>;
74
75
  validateConfirmation(token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
75
76
  handleConfirmation(decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
76
77
  close(): Promise<void>;
package/dist/engine.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { lstat, readFile } from "node:fs/promises";
1
2
  import { createSqlProfileLoader, } from "./auth/sql-profile-loader.js";
2
3
  import { createSecretResolver, } from "./auth/secret-resolver.js";
4
+ import { createHuaweiKmsSecretResolver } from "./cloud/kms.js";
5
+ import { createHuaweiCsmsSecretResolver } from "./cloud/csms.js";
3
6
  import { createCapabilityProbe, } from "./capability/probe.js";
4
7
  import { getConfig } from "./config/index.js";
5
8
  import { createDatasourceResolver } from "./context/datasource-resolver.js";
@@ -10,11 +13,26 @@ import { createConfirmationStore, } from "./safety/confirmation-store.js";
10
13
  import { createGuardrail, } from "./safety/guardrail.js";
11
14
  import { createSchemaIntrospector, } from "./schema/introspector.js";
12
15
  import { createMySqlSchemaAdapter } from "./schema/adapters/mysql.js";
16
+ import { diagnoseReplicationLag } from "./diagnostics/replication-lag.js";
13
17
  import { createSlowSqlSource, } from "./diagnostics/slow-sql-source.js";
14
18
  import { createMetricsSource, } from "./diagnostics/metrics-source.js";
15
19
  import { findLatestDeadlock, findMetadataLockWaits, findStatementDigestCandidatesForSqlHints, findStatementDigestSample, findStatementDigestSampleForSql, findStatementWaitEvents, findStorageStatementDigests, findTableStorageStats, findTopStatementDigests, isPerformanceSchemaEnabled, showLockWaits, showProcesslist, } from "./engine/data-access.js";
16
20
  import { diagnoseConnectionSpike, diagnoseDbHotspot, diagnoseLockContention, diagnoseServiceLatency, diagnoseSlowQuery, diagnoseStoragePressure, findTopSlowSql, } from "./engine/diagnostics.js";
17
21
  import { cancelQuery, close, executeMutation, executeReadonly, explain, explainEnhanced, flashbackQuery, getQueryStatus, handleConfirmation, issueConfirmation, listRecycleBin, restoreRecycleBinTable, validateConfirmation, } from "./engine/runtime.js";
22
+ async function readMutationApprovalSecret(filePath) {
23
+ const info = await lstat(filePath);
24
+ if (!info.isFile() || info.isSymbolicLink()) {
25
+ throw new Error("Mutation approval secret must be a regular, non-symlink file.");
26
+ }
27
+ if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
28
+ throw new Error("Mutation approval secret file must not be accessible by group or other users.");
29
+ }
30
+ const secret = (await readFile(filePath, "utf8")).trim();
31
+ if (Buffer.byteLength(secret, "utf8") < 32) {
32
+ throw new Error("Mutation approval secret must contain at least 32 bytes.");
33
+ }
34
+ return secret;
35
+ }
18
36
  function toDataSourceInfo(profile, defaultDatasource) {
19
37
  return {
20
38
  name: profile.name,
@@ -56,7 +74,14 @@ export class TaurusDBEngine {
56
74
  static async create(options = {}) {
57
75
  const config = options.config ?? getConfig();
58
76
  const profileLoader = options.profileLoader ?? createSqlProfileLoader({ config });
59
- const secretResolver = options.secretResolver ?? createSecretResolver();
77
+ const secretResolver = options.secretResolver ??
78
+ createSecretResolver({
79
+ uriResolvers: {
80
+ "hw-csms": createHuaweiCsmsSecretResolver({ config }),
81
+ "hw-kms": createHuaweiKmsSecretResolver({ config }),
82
+ "hw-kms-file": createHuaweiKmsSecretResolver({ config }),
83
+ },
84
+ });
60
85
  const datasourceResolver = options.datasourceResolver ??
61
86
  createDatasourceResolver({
62
87
  config,
@@ -80,9 +105,23 @@ export class TaurusDBEngine {
80
105
  const executor = options.executor ??
81
106
  createSqlExecutor({
82
107
  connectionPool,
108
+ maxConcurrentQueries: config.limits.maxConcurrentQueries,
109
+ maxQueuedQueries: config.limits.maxQueuedQueries,
110
+ queueTimeoutMs: config.limits.queueTimeoutMs,
83
111
  });
84
112
  const guardrail = options.guardrail ?? createGuardrail();
85
- const confirmationStore = options.confirmationStore ?? createConfirmationStore();
113
+ let approvalSecret;
114
+ if (config.security.approvalSecretPath) {
115
+ approvalSecret = await readMutationApprovalSecret(config.security.approvalSecretPath);
116
+ }
117
+ if (config.security.mutationsEnabled && !approvalSecret && !options.confirmationStore) {
118
+ throw new Error("Mutations require TAURUSDB_MUTATION_APPROVAL_SECRET_FILE with at least 32 bytes.");
119
+ }
120
+ const confirmationStore = options.confirmationStore ??
121
+ createConfirmationStore({
122
+ approvalSecret,
123
+ ttlSeconds: config.security.approvalTtlSeconds,
124
+ });
86
125
  const capabilityProbe = options.capabilityProbe ??
87
126
  createCapabilityProbe({
88
127
  connectionPool,
@@ -146,6 +185,9 @@ export class TaurusDBEngine {
146
185
  async showLockWaits(input, ctx) {
147
186
  return showLockWaits(this, input, ctx);
148
187
  }
188
+ async diagnoseReplicationLag(input, ctx) {
189
+ return diagnoseReplicationLag(this.executor, input, ctx);
190
+ }
149
191
  async findMetadataLockWaits(input, ctx) {
150
192
  return findMetadataLockWaits(this, input, ctx);
151
193
  }
@@ -0,0 +1 @@
1
+ export declare function buildServerBoundedReadonlySql(sql: string, maxRows: number): string;
@@ -0,0 +1,10 @@
1
+ export function buildServerBoundedReadonlySql(sql, maxRows) {
2
+ const normalized = sql.trim().replace(/;\s*$/, "");
3
+ if (!/^(?:select|with)\b/i.test(normalized)) {
4
+ return normalized;
5
+ }
6
+ if (!Number.isInteger(maxRows) || maxRows <= 0) {
7
+ throw new Error("maxRows must be a positive integer.");
8
+ }
9
+ return `SELECT * FROM (${normalized}) AS __taurus_mcp_bounded LIMIT ${maxRows + 1}`;
10
+ }
@@ -0,0 +1,15 @@
1
+ export declare class QueryConcurrencyError extends Error {
2
+ readonly code = "SERVER_BUSY";
3
+ constructor(message: string);
4
+ }
5
+ export declare class QueryConcurrencyLimiter {
6
+ private readonly maxConcurrent;
7
+ private readonly maxQueued;
8
+ private readonly queueTimeoutMs;
9
+ private active;
10
+ private readonly queue;
11
+ constructor(maxConcurrent: number, maxQueued: number, queueTimeoutMs: number);
12
+ run<T>(operation: () => Promise<T>): Promise<T>;
13
+ private acquire;
14
+ private releaseFactory;
15
+ }