@zktx.io/sui-mcp 0.5.0 → 0.6.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 (138) hide show
  1. package/README.md +21 -21
  2. package/dist/adapters/adapterPromptSurfaces.js +0 -20
  3. package/dist/adapters/deepbook/deepbookQuotePolicy.js +0 -4
  4. package/dist/adapters/deepbook/deepbookReviewEvidence.js +0 -9
  5. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +0 -9
  6. package/dist/adapters/intentPlanFactories.js +0 -6
  7. package/dist/core/action/contractNameRegistry.js +0 -53
  8. package/dist/core/action/forbiddenFields.js +0 -4
  9. package/dist/core/action/ptbVisualizationProducer.js +0 -3
  10. package/dist/core/action/receiptPtbGraph.js +0 -17
  11. package/dist/core/action/reviewTimeSimulationEvidence.js +0 -4
  12. package/dist/core/action/signableAdapterContract.js +0 -5
  13. package/dist/core/action/suiChainReceiptMapping.js +0 -18
  14. package/dist/core/action/suiChainReceiptReader.js +1 -21
  15. package/dist/core/action/suiChainReceiptVerifier.js +0 -10
  16. package/dist/core/action/transactionFunding.js +0 -7
  17. package/dist/core/action/types.js +0 -1
  18. package/dist/core/action/walletReviewContractAssembler.js +0 -2
  19. package/dist/core/activity/localDataService.js +65 -52
  20. package/dist/core/activity/localDataValidation.js +1 -3
  21. package/dist/core/activity/sqliteActivityStore.js +24 -24
  22. package/dist/core/activity/sqliteActivityStoreRows.js +0 -2
  23. package/dist/core/activity/sqliteActivityStoreSchema.js +81 -14
  24. package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
  25. package/dist/core/read/deepbookReadHelpers.js +0 -2
  26. package/dist/core/read/deepbookRegistry.js +0 -19
  27. package/dist/core/read/deepbookUsdcChartService.js +0 -2
  28. package/dist/core/read/readCardResult.js +0 -2
  29. package/dist/core/read/readService.js +0 -19
  30. package/dist/core/read/readServiceTypes.js +0 -2
  31. package/dist/core/session/cardSession.js +1 -5
  32. package/dist/core/session/cardSessionStore.js +54 -7
  33. package/dist/core/session/reviewValidity.js +0 -3
  34. package/dist/core/session/sessionRecordStore.js +0 -1
  35. package/dist/core/session/sessionStore.js +0 -32
  36. package/dist/core/session/settingsFileIdentity.js +9 -0
  37. package/dist/core/session/settingsFileProtocol.js +20 -0
  38. package/dist/core/session/settingsFileStore.js +337 -0
  39. package/dist/core/session/settingsInputError.js +32 -0
  40. package/dist/core/session/settingsOperation.js +220 -0
  41. package/dist/core/session/settingsPreparationSql.js +13 -0
  42. package/dist/core/session/settingsWorkflow.js +1183 -0
  43. package/dist/core/session/sqliteCardStore.js +0 -2
  44. package/dist/core/session/sqliteSessionStore.js +0 -51
  45. package/dist/core/session/sqliteSettingsWorkflowStore.js +453 -0
  46. package/dist/core/session/sqliteTransactionMaterialStore.js +0 -11
  47. package/dist/core/session/sqliteWalletWorkflowStore.js +160 -66
  48. package/dist/core/session/status.js +2 -4
  49. package/dist/core/session/transactionMaterialStore.js +0 -4
  50. package/dist/core/session/transactionRequest.js +0 -4
  51. package/dist/core/session/walletConnection.js +26 -10
  52. package/dist/core/session/walletWorkflow.js +70 -30
  53. package/dist/core/session/workflowState.js +14 -14
  54. package/dist/core/session/workflowView.js +18 -8
  55. package/dist/core/suiAddress.js +0 -5
  56. package/dist/core/suiEndpoint.js +0 -3
  57. package/dist/mcp/prompts.js +0 -12
  58. package/dist/mcp/responseGuidance.js +10 -5
  59. package/dist/mcp/schemas.js +0 -3
  60. package/dist/mcp/serverInfo.js +3 -4
  61. package/dist/mcp/toolNames.js +2 -2
  62. package/dist/mcp/tools/account/index.js +1 -19
  63. package/dist/mcp/tools/action/prepareSuiActionReview.js +0 -6
  64. package/dist/mcp/tools/read/readToolHelpers.js +5 -4
  65. package/dist/mcp/tools/read/serverStatusTools.js +0 -1
  66. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  67. package/dist/mcp/tools/session/shared.js +1 -2
  68. package/dist/mcp/tools/session/statusTools.js +4 -5
  69. package/dist/mcp/tools/settings/index.js +8 -28
  70. package/dist/mcp-app/account.html +14 -14
  71. package/dist/mcp-app/account.notices.txt +1 -1
  72. package/dist/mcp-app/chart.html +9 -9
  73. package/dist/mcp-app/chart.notices.txt +1 -1
  74. package/dist/mcp-app/connect.html +14 -14
  75. package/dist/mcp-app/connect.notices.txt +1 -1
  76. package/dist/mcp-app/receipt.html +1298 -1463
  77. package/dist/mcp-app/receipt.notices.txt +226 -1204
  78. package/dist/mcp-app/review.html +1298 -1463
  79. package/dist/mcp-app/review.notices.txt +226 -1204
  80. package/dist/mcp-app/settings.html +3133 -0
  81. package/dist/mcp-app/settings.notices.txt +973 -0
  82. package/dist/mcp-ui/contracts.js +8 -2
  83. package/dist/mcp-ui/readCards.js +1 -1
  84. package/dist/mcp-ui/tools.js +34 -4
  85. package/dist/review-server/server.js +3 -51
  86. package/dist/runtime/application.js +41 -14
  87. package/dist/runtime/config.js +0 -2
  88. package/dist/runtime/localSettingsService.js +23 -2
  89. package/dist/runtime/logger.js +0 -2
  90. package/dist/runtime/reviewServerAcquire.js +1 -18
  91. package/dist/runtime/runtimeTermination.js +1 -4
  92. package/dist/runtime/shared/authenticatedFetch.js +2 -4
  93. package/dist/runtime/shared/control.js +4 -7
  94. package/dist/runtime/shared/dataAccess.js +0 -2
  95. package/dist/runtime/shared/http.js +14 -0
  96. package/dist/runtime/shared/mcpHttp.js +6 -6
  97. package/dist/runtime/shared/ownerLease.js +0 -4
  98. package/dist/runtime/shared/server.js +1 -2
  99. package/dist/runtime/shared/stdio.js +2 -5
  100. package/dist/runtime/smokeMainnetRead.js +0 -3
  101. package/dist/runtime/smokeReview.js +0 -4
  102. package/dist/runtime/start.js +0 -2
  103. package/dist/runtime/suiTransactionGraphqlMapping.js +0 -2
  104. package/dist/runtime/suiTransactionGraphqlQueries.js +0 -3
  105. package/dist/runtime/walletConnectConfig.js +0 -2
  106. package/dist/runtime/walletConnectStorage.js +2 -10
  107. package/dist/runtime/walletConnectTransport.js +0 -11
  108. package/dist/runtime/walletSdkChildRuntime.js +2 -6
  109. package/dist/runtime/walletSdkIpc.js +2 -1
  110. package/dist/runtime/walletSdkProcess.js +3 -8
  111. package/docs/AGENT_BEHAVIOR.md +45 -15
  112. package/docs/LOCAL_DB_ARCHITECTURE.md +145 -22
  113. package/docs/MCP_SETUP.md +99 -39
  114. package/docs/MCP_TOOLS.md +288 -24
  115. package/docs/SDK_API.md +63 -10
  116. package/docs/UTILITY_INDEX.md +9 -6
  117. package/docs/WALLET_CONNECTION.md +32 -10
  118. package/package.json +22 -8
  119. package/dist/core/activity/schemaVersion.js +0 -2
  120. package/dist/core/session/keyedRecordStore.js +0 -18
  121. package/dist/core/session/settingsSession.js +0 -1
  122. package/dist/core/session/settingsSessions.js +0 -45
  123. package/dist/review-app/brand-dark.svg +0 -1
  124. package/dist/review-app/brand-light.svg +0 -1
  125. package/dist/review-app/favicon.svg +0 -1
  126. package/dist/review-app/settings.css +0 -1
  127. package/dist/review-app/settings.js +0 -1
  128. package/dist/review-app/ui.css +0 -809
  129. package/dist/review-server/assets.js +0 -48
  130. package/dist/review-server/html.js +0 -45
  131. package/dist/review-server/http.js +0 -47
  132. package/dist/review-server/middleware/reviewToken.js +0 -7
  133. package/dist/review-server/settingsApi.js +0 -158
  134. package/docs/AGENT_DEVELOPMENT_POLICY.md +0 -815
  135. package/docs/FRONTEND_POLICY.md +0 -534
  136. package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +0 -166
  137. package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +0 -161
  138. package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +0 -259
@@ -1,8 +1,8 @@
1
+ import { SETTINGS_DOWNLOAD_FILE_SQL, SETTINGS_FILE_CLEANUP_SQL } from "../session/settingsFileIdentity.js";
1
2
  import { hasUnsettledWalletWork } from "../session/sqliteWalletWorkflowStore.js";
2
- import { DB_USER_VERSION } from "./schemaVersion.js";
3
3
  import { SuiEndpointError, parseGraphqlUrl, parseGrpcUrl } from "../suiEndpoint.js";
4
4
  import { LOCAL_DATA_EXPORT_FORMAT, LOCAL_DATA_NETWORK, LocalDataError } from "./localDataTypes.js";
5
- import { activeAccountChange, countsForPayload, invalidBackup, maxNumber, parseLocalDataEnvelope, suiGraphqlUrlFromPayload, suiGrpcUrlFromPayload } from "./localDataValidation.js";
5
+ import { activeAccountChange, activeAccountAddress, countsForPayload, invalidBackup, maxNumber, parseLocalDataEnvelope, suiGraphqlUrlFromPayload, suiGrpcUrlFromPayload } from "./localDataValidation.js";
6
6
  export * from "./localDataTypes.js";
7
7
  export class SqliteLocalDataService {
8
8
  db;
@@ -15,18 +15,18 @@ export class SqliteLocalDataService {
15
15
  }
16
16
  now() { return this.options.now?.() ?? new Date(); }
17
17
  async getDataCounts() {
18
- return this.currentDataCounts();
18
+ return this.currentDataCountsSync();
19
19
  }
20
- currentDataCounts() {
20
+ currentDataCountsSync(now = this.now()) {
21
21
  return this.db.transaction(() => {
22
- this.options.advanceRequestDeadlines(this.now());
22
+ this.options.advanceRequestDeadlines(now);
23
23
  return this.getDataCountsSync();
24
24
  }).immediate();
25
25
  }
26
- async exportLocalData(now = new Date()) {
26
+ async exportLocalData(now = new Date()) { return this.exportLocalDataSync(now); }
27
+ exportLocalDataSync(now = this.now()) {
27
28
  return this.db.transaction(() => {
28
29
  const envelope = {
29
- schemaVersion: DB_USER_VERSION,
30
30
  format: LOCAL_DATA_EXPORT_FORMAT,
31
31
  network: LOCAL_DATA_NETWORK,
32
32
  exportedAt: now.toISOString(),
@@ -48,32 +48,46 @@ export class SqliteLocalDataService {
48
48
  });
49
49
  })();
50
50
  }
51
- async previewImportLocalData(input) {
51
+ async previewImportLocalData(input) { return this.previewImportLocalDataSync(input); }
52
+ previewImportLocalDataSync(input, now = this.now()) {
52
53
  const envelope = parseLocalDataEnvelope(input, {
53
54
  validateAdapterLifecycle: this.validateAdapterLifecycle
54
55
  });
55
- return {
56
- status: "valid",
57
- format: envelope.format,
58
- network: envelope.network,
59
- exportedAt: envelope.exportedAt,
60
- currentCounts: this.currentDataCounts(),
61
- incomingCounts: countsForPayload(envelope.data),
62
- willReplace: true,
63
- activeAccountChange: activeAccountChange(this.selectAll("accounts"), this.selectAll("active_account_context"), envelope.data.accounts, envelope.data.activeAccountContext),
64
- restartRequiredAfterImport: true,
65
- };
56
+ return this.db.transaction(() => {
57
+ this.options.advanceRequestDeadlines(now);
58
+ const currentAccounts = this.selectAll("accounts");
59
+ const currentContext = this.selectAll("active_account_context");
60
+ return {
61
+ status: "valid",
62
+ format: envelope.format,
63
+ network: envelope.network,
64
+ exportedAt: envelope.exportedAt,
65
+ currentCounts: this.getDataCountsSync(),
66
+ incomingCounts: countsForPayload(envelope.data),
67
+ willReplace: true,
68
+ activeAccountChange: activeAccountChange(currentAccounts, currentContext, envelope.data.accounts, envelope.data.activeAccountContext),
69
+ currentReadAddress: activeAccountAddress(currentAccounts, currentContext),
70
+ incomingReadAddress: activeAccountAddress(envelope.data.accounts, envelope.data.activeAccountContext),
71
+ restartRequiredAfterImport: true,
72
+ };
73
+ }).immediate();
66
74
  }
67
75
  async importLocalDataReplace(input, _now = new Date()) {
68
76
  const envelope = await this.parseAndVerifyImportEnvelope(input);
69
- this.db.transaction(() => {
70
- // Endpoint verification may have outlived the original request window.
71
- // Evaluate the current owner state inside the actual replacement transaction.
72
- const now = this.now();
73
- this.options.advanceRequestDeadlines(now);
74
- if (hasUnsettledWalletWork(this.db, now))
75
- throw new LocalDataError("input_invalid", "A wallet request is still unsettled.", { reason: "wallet_request_unsettled" });
76
- this.resetLocalDataTables();
77
+ const result = this.db.transaction(() => this.applyValidatedReplacement(envelope, this.now()))();
78
+ this.notifyDataReplaced();
79
+ return result;
80
+ }
81
+ async resetLocalData(now = this.now()) {
82
+ const result = this.db.transaction(() => this.applyValidatedReplacement(undefined, now))();
83
+ this.notifyDataReplaced();
84
+ return result;
85
+ }
86
+ applyValidatedReplacement(envelope, now, preserveCardId) {
87
+ this.options.advanceRequestDeadlines(now);
88
+ this.assertReplacementAllowed(now);
89
+ this.resetLocalDataTables(preserveCardId);
90
+ if (envelope) {
77
91
  this.insertAccounts(envelope.data.accounts);
78
92
  this.insertActiveAccountContext(envelope.data.activeAccountContext);
79
93
  this.insertReviewSessions(envelope.data.reviewSessions);
@@ -85,39 +99,34 @@ export class SqliteLocalDataService {
85
99
  this.insertExternalActivityTransactions(envelope.data.externalActivityTransactions);
86
100
  this.insertLocalSettings(envelope.data.localSettings);
87
101
  this.syncSqliteSequences(envelope.data);
88
- })();
89
- this.options.onDataReplaced?.();
90
- return {
91
- status: "imported",
92
- dataCounts: this.getDataCountsSync(),
93
- sessionsInvalidated: true
94
- };
95
- }
96
- async resetLocalData(now = this.now()) {
97
- this.db.transaction(() => {
98
- this.options.advanceRequestDeadlines(now);
99
- if (hasUnsettledWalletWork(this.db, now))
100
- throw new LocalDataError("input_invalid", "A wallet request is still unsettled.", { reason: "wallet_request_unsettled" });
101
- this.resetLocalDataTables();
102
+ }
103
+ else
102
104
  this.insertDefaultSuiGrpcUrl(now);
103
- })();
104
- this.options.onDataReplaced?.();
105
- return {
106
- status: "reset",
107
- dataCounts: this.getDataCountsSync(),
108
- sessionsInvalidated: true
109
- };
105
+ this.db.prepare("UPDATE settings_control SET data_epoch=data_epoch+1 WHERE id=1").run();
106
+ if (preserveCardId)
107
+ this.db.prepare(`UPDATE settings_operations SET data_epoch=(SELECT data_epoch FROM settings_control WHERE id=1)
108
+ WHERE card_id=?`).run(preserveCardId);
109
+ return { status: envelope ? "imported" : "reset", dataCounts: this.getDataCountsSync(), sessionsInvalidated: true };
110
110
  }
111
- resetLocalDataTables() {
111
+ notifyDataReplaced() { this.options.onDataReplaced?.(); }
112
+ resetLocalDataTables(preserveCardId) {
113
+ this.db.prepare(`UPDATE settings_files SET invalidated=1 WHERE role='upload' OR ${SETTINGS_DOWNLOAD_FILE_SQL}`).run();
114
+ if (preserveCardId) {
115
+ const operation = this.db.prepare("SELECT operation_id FROM settings_operations WHERE card_id=?").get(preserveCardId);
116
+ if (!operation)
117
+ throw new LocalDataError("internal_error", "The replacement result identity is unavailable.");
118
+ this.db.prepare(`UPDATE settings_files SET cleanup_operation_id=? WHERE cleanup!='complete'
119
+ AND ${SETTINGS_FILE_CLEANUP_SQL}`).run(operation.operation_id);
120
+ this.db.prepare("DELETE FROM live_read_cards WHERE id!=?").run(preserveCardId);
121
+ }
122
+ else
123
+ this.db.prepare("DELETE FROM live_read_cards").run();
112
124
  for (const table of [
113
- "live_read_cards",
114
125
  "live_execution_details",
115
126
  "live_request_authority",
116
- "live_wallet_connections",
117
127
  "live_transaction_materials",
118
128
  "live_private_review_artifacts",
119
129
  "live_review_sessions",
120
- "live_settings_sessions",
121
130
  "external_activity_transactions",
122
131
  "external_activity_scans",
123
132
  "review_state_snapshots",
@@ -259,6 +268,10 @@ export class SqliteLocalDataService {
259
268
  }
260
269
  }
261
270
  }
271
+ assertReplacementAllowed(now = this.now()) {
272
+ if (hasUnsettledWalletWork(this.db, now))
273
+ throw new LocalDataError("input_invalid", "A wallet request is still unsettled.", { reason: "wallet_request_unsettled" });
274
+ }
262
275
  async parseAndVerifyImportEnvelope(input) {
263
276
  const envelope = parseLocalDataEnvelope(input, {
264
277
  validateAdapterLifecycle: this.validateAdapterLifecycle
@@ -1,6 +1,5 @@
1
1
  import { transactionRequestStatusSchema, transactionRequestSchema } from "../session/transactionRequest.js";
2
2
  import { REVIEW_TRANSITION_EVENTS } from "./sqliteActivityStoreRows.js";
3
- import { DB_USER_VERSION } from "./schemaVersion.js";
4
3
  import { z } from "zod";
5
4
  import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
6
5
  import { parseLifecycleValidatedReviewState } from "../action/reviewStateValidation.js";
@@ -144,7 +143,6 @@ const payloadSchema = z.object({
144
143
  localSettings: z.array(localSettingRowSchema).length(2)
145
144
  }).strict();
146
145
  const envelopeSchema = z.object({
147
- schemaVersion: z.literal(DB_USER_VERSION),
148
146
  format: z.literal(LOCAL_DATA_EXPORT_FORMAT),
149
147
  network: z.literal(LOCAL_DATA_NETWORK),
150
148
  exportedAt: isoTimestamp,
@@ -427,7 +425,7 @@ export function activeAccountChange(currentAccounts, current, incomingAccounts,
427
425
  }
428
426
  return incomingAccount === null ? "cleared" : "set";
429
427
  }
430
- function activeAccountAddress(accounts, context) {
428
+ export function activeAccountAddress(accounts, context) {
431
429
  const accountId = context[0]?.account_id ?? null;
432
430
  if (accountId === null) {
433
431
  return null;
@@ -17,7 +17,8 @@ import { SqlitePreferencesRepository } from "../preferences/sqlitePreferencesRep
17
17
  import { SqliteTransactionMaterialStore } from "../session/sqliteTransactionMaterialStore.js";
18
18
  import { SqliteCardRecordStore } from "../session/sqliteCardStore.js";
19
19
  import { sameHandle } from "../session/transactionMaterialStore.js";
20
- import { SqliteSessionRecordStore, SqlitePrivateReviewArtifactStore, createSqliteSettingsRecordStore, sessionFromLiveReviewSessionRow, insertLiveReviewSessionRow, updateLiveReviewSessionRow } from "../session/sqliteSessionStore.js";
20
+ import { SqliteSessionRecordStore, SqlitePrivateReviewArtifactStore, sessionFromLiveReviewSessionRow, insertLiveReviewSessionRow, updateLiveReviewSessionRow } from "../session/sqliteSessionStore.js";
21
+ import { SqliteSettingsWorkflowStore } from "../session/sqliteSettingsWorkflowStore.js";
21
22
  import { SqliteLocalDataService } from "./localDataService.js";
22
23
  import { buildExternalActivityCoverageResult } from "./externalActivityCoverage.js";
23
24
  import { buildExternalActivityTransactionStreamResult, canonicalExternalActivityTransactions } from "./externalActivityTransactionStream.js";
@@ -29,14 +30,11 @@ export { ActivityStoreError };
29
30
  const ACTIVE_ACCOUNT_SINGLETON_ID = 1;
30
31
  export const DATA_DIR_ENV = "SUI_MCP_DATA_DIR";
31
32
  export const ACTIVITY_DATABASE_FILENAME = "sui-mcp.sqlite";
32
- // Best-effort permission hardening. The owner-only data directory is the primary
33
- // protection; failures (e.g. on Windows, which ignores POSIX modes) are non-fatal.
34
33
  function restrictPathPermissions(path, mode) {
35
34
  try {
36
35
  chmodSync(path, mode);
37
36
  }
38
37
  catch {
39
- // Non-fatal: the 0700 directory remains the primary protection.
40
38
  }
41
39
  }
42
40
  export class SqliteActivityStore {
@@ -70,10 +68,6 @@ export class SqliteActivityStore {
70
68
  this.db.close();
71
69
  throw error;
72
70
  }
73
- // This database persists unsigned transaction material (Option B), so restrict it to
74
- // the owner. The 0700 directory is the primary protection (it also covers the WAL/SHM
75
- // sidecars and blocks other OS users); the 0600 file is belt-and-suspenders. Best-effort
76
- // because some platforms (e.g. Windows) ignore POSIX modes.
77
71
  restrictPathPermissions(dataDirectory, 0o700);
78
72
  restrictPathPermissions(options.databasePath, 0o600);
79
73
  }
@@ -117,7 +111,8 @@ export class SqliteActivityStore {
117
111
  async getActiveAccount() {
118
112
  return this.getActiveAccountSync();
119
113
  }
120
- async clearActiveAccount(now = new Date()) {
114
+ async clearActiveAccount(now = new Date()) { this.clearActiveAccountSync(now); }
115
+ clearActiveAccountSync(now = new Date()) {
121
116
  this.db
122
117
  .prepare(`INSERT INTO active_account_context (id, account_id, source, set_at)
123
118
  VALUES (?, NULL, 'cleared', ?)
@@ -360,7 +355,6 @@ export class SqliteActivityStore {
360
355
  (r.request_status!='completed' OR e.tx_digest!=r.transaction_digest OR json_valid(e.result_json)=0))) LIMIT 1`).get(...params);
361
356
  if (inconsistent)
362
357
  throw new ActivityStoreReadError("internal_error", "Stored review request evidence is inconsistent.", { reason: "invalid_stored_evidence" });
363
- // Aggregation must not turn a malformed current result into a valid count.
364
358
  for (const row of this.db.prepare(`SELECT rs.id,rs.current_attempt_id,a.sui_address AS account
365
359
  FROM review_sessions rs JOIN accounts a ON a.id=rs.account_id ${where} AND rs.current_attempt_id IS NOT NULL`)
366
360
  .all(...params)) {
@@ -657,11 +651,14 @@ export class SqliteActivityStore {
657
651
  createCardRecordStore() {
658
652
  return new SqliteCardRecordStore(this.db);
659
653
  }
654
+ createSettingsWorkflowStore(ownerId, now = () => new Date()) {
655
+ return new SqliteSettingsWorkflowStore(this.db, ownerId, now);
656
+ }
660
657
  createPrivateReviewArtifactStore() {
661
658
  return new SqlitePrivateReviewArtifactStore(this.db);
662
659
  }
663
- createWalletWorkflowStore(ownerId, clock = () => new Date()) {
664
- const records = new SqliteWalletWorkflowStore(this.db, ownerId, (address, id, name, now) => this.setActiveAccountSync(address, "wallet_connection", now, { id, name }), clock, this.createTransactionMaterialStore(), (input, at) => this.evaluateWorkflowState(input, records, ownerId, at === undefined ? clock : () => at), () => this.getActiveAccountSync());
660
+ createWalletWorkflowStore(ownerId, clock = () => new Date(), readSdkSnapshot = () => undefined) {
661
+ const records = new SqliteWalletWorkflowStore(this.db, ownerId, (address, id, name, now) => this.setActiveAccountSync(address, "wallet_connection", now, { id, name }), clock, this.createTransactionMaterialStore(), (input, at) => this.reconcileWorkflowState(input, records, ownerId, at === undefined ? clock : () => at), () => this.getActiveAccountSync(), readSdkSnapshot);
665
662
  return records;
666
663
  }
667
664
  finalizeReviewEvaluation(candidate, clock) {
@@ -694,19 +691,26 @@ export class SqliteActivityStore {
694
691
  return { session: next, events: next.status === "expired" ? [] : [{ type: "state.computed", sessionId: next.id,
695
692
  status: next.status, reason: "private_review_artifacts_refresh_required", at: now.toISOString() }] };
696
693
  }
697
- evaluateWorkflowState(input, records, ownerId, clock) {
694
+ reconcileWorkflowState(input, records, ownerId, clock) {
698
695
  return this.db.transaction(() => {
699
696
  const now = clock();
697
+ if (input.purpose !== "current_wallet" && input.purpose !== "review" ||
698
+ input.purpose === "review" && (!input.readTarget || !input.candidate)) {
699
+ throw new SessionStoreError("input_invalid", "An explicit workflow evaluation target is required.");
700
+ }
700
701
  const cards = this.createCardRecordStore();
701
702
  let record = input.expectedCard && records.currentCard(input.expectedCard);
702
- const reviewId = record?.state.kind === "review" ? String(record.state.input.reviewSessionId) : input.reviewSessionId;
703
+ const reviewId = input.purpose === "review" ? input.reviewSessionId : undefined;
704
+ if (record && (input.purpose === "review" ? record.state.kind !== "review" || record.state.input.reviewSessionId !== reviewId : record.state.kind !== "connect")) {
705
+ throw new SessionStoreError("session_mismatch", "The workflow target does not match this card.");
706
+ }
703
707
  const target = reviewId ? records.reviewReadTarget(reviewId, record) : undefined;
704
- if (input.readTarget && (!target || input.readTarget.reviewSessionId !== target.reviewSessionId ||
708
+ if (input.purpose === "review" && (!target || input.readTarget.reviewSessionId !== target.reviewSessionId ||
705
709
  input.readTarget.attemptId !== target.attemptId || !input.readTarget.walletDependent && target.walletDependent)) {
706
710
  throw new SessionStoreError("session_mismatch", "The review target changed while its state was being read. Check this review again.");
707
711
  }
708
712
  let evaluated;
709
- if (reviewId) {
713
+ if (input.purpose === "review") {
710
714
  if (!input.candidate || input.candidate.session.id !== reviewId)
711
715
  throw new SessionStoreError("session_not_found", "The saved review is unavailable.");
712
716
  evaluated = this.finalizeReviewEvaluationAt(input.candidate, now);
@@ -726,14 +730,13 @@ export class SqliteActivityStore {
726
730
  const authority = request && records.authority(request.attemptId);
727
731
  const details = request?.execution && records.executionDetails(request.attemptId);
728
732
  const connection = record?.state.kind === "connect" && record.operationId ? records.connection(record.operationId) : undefined;
729
- const storedAccount = this.getActiveAccountSync(), activeAccount = storedAccount?.address, targetAccount = session?.account ?? activeAccount;
730
- const facts = { evaluatedAt: now.toISOString(), ownerId, record, session, request, authority,
733
+ const connectionFacts = records.readConnectionFacts(now), targetAccount = session?.account ?? connectionFacts.activeAccount;
734
+ const facts = { ...connectionFacts, ownerId, record, session, request, authority,
731
735
  hasReviewInput: !!session && session.status !== "expired" && Date.parse(session.expiresAt) > now.getTime() &&
732
736
  cards.hasReviewInput(session.id, ownerId, now),
733
- walletAvailability: input.walletAvailability, walletObservation: input.walletObservation,
734
- runtimeRecovery: record && records.walletRecovery(record), activeAccount, activeAccountConnectionId: storedAccount?.walletId,
737
+ walletAvailability: input.walletAvailability,
738
+ runtimeRecovery: record && records.walletRecovery(record),
735
739
  walletDependent: !target || target.walletDependent,
736
- connections: records.connectionViews(),
737
740
  connection: connection ? records.connectionView(connection.connection) : undefined,
738
741
  boundReview: request && (record?.operationId || record?.scope === "review_manage") ? records.requestReview(request.attemptId) : undefined,
739
742
  busyForAccount: !!targetAccount && records.busyForAccount(targetAccount, now),
@@ -741,9 +744,6 @@ export class SqliteActivityStore {
741
744
  return { ...facts, ...workflowEligibility(facts), events: evaluated?.events ?? [] };
742
745
  }).immediate();
743
746
  }
744
- createSettingsRecordStore() {
745
- return createSqliteSettingsRecordStore(this.db);
746
- }
747
747
  runLiveReviewSessionMutation(live, writeActivity) {
748
748
  const stale = new Error("live review session changed before commit");
749
749
  try {
@@ -315,8 +315,6 @@ export function parseActionPlanEvidence(plan) {
315
315
  return parsed.data;
316
316
  }
317
317
  export function extractRequestedIntent(plan) {
318
- // Adapter convention: full plan_json stays canonical, while requestedIntent
319
- // is materialized only to make activity queries avoid nested JSON paths.
320
318
  return plan.adapterData.requestedIntent;
321
319
  }
322
320
  export function reasonForReviewState(state) {
@@ -1,7 +1,7 @@
1
1
  import Database from "better-sqlite3";
2
2
  import { EXTERNAL_ACTIVITY_SCAN_MAX_LIMIT } from "./activityStore.js";
3
- import { DB_USER_VERSION } from "./schemaVersion.js";
4
3
  import { ActivityStoreError } from "./sqliteActivityStoreTypes.js";
4
+ import { SETTINGS_RESET_PREPARATION_SQL } from "../session/settingsPreparationSql.js";
5
5
  import { LIVE_REVIEW_SESSION_INSERT_TRIGGER, LIVE_REVIEW_SESSION_UPDATE_TRIGGER, LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION } from "../session/liveReviewSessionContract.js";
6
6
  const EXTERNAL_ACTIVITY_SCAN_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_external_activity_scans_account_fetched
7
7
  ON external_activity_scans(account_id, fetched_at)`;
@@ -132,6 +132,7 @@ const CURRENT_SCHEMA_SQL = `
132
132
  CREATE TABLE IF NOT EXISTS live_wallet_connections (
133
133
  id TEXT PRIMARY KEY,
134
134
  owner_id TEXT NOT NULL,
135
+ run_id TEXT,
135
136
  status TEXT NOT NULL,
136
137
  revision INTEGER NOT NULL CHECK (revision >= 0),
137
138
  topic TEXT UNIQUE,
@@ -141,6 +142,7 @@ const CURRENT_SCHEMA_SQL = `
141
142
  CREATE TABLE IF NOT EXISTS live_request_authority (
142
143
  attempt_id TEXT PRIMARY KEY REFERENCES review_requests(attempt_id) ON DELETE RESTRICT,
143
144
  owner_id TEXT NOT NULL,
145
+ run_id TEXT,
144
146
  connection_id TEXT NOT NULL REFERENCES live_wallet_connections(id) ON DELETE RESTRICT,
145
147
  connection_revision INTEGER NOT NULL,
146
148
  can_submit INTEGER NOT NULL CHECK (can_submit IN (0,1)),
@@ -220,6 +222,7 @@ const CURRENT_SCHEMA_SQL = `
220
222
  owner_id TEXT NOT NULL,
221
223
  review_revision INTEGER NOT NULL DEFAULT 0,
222
224
  preparation_id TEXT,
225
+ preparation_run_id TEXT,
223
226
  preparation_error TEXT,
224
227
  wallet_connection_id TEXT,
225
228
  wallet_connection_revision INTEGER,
@@ -239,16 +242,11 @@ const CURRENT_SCHEMA_SQL = `
239
242
  artifacts_json TEXT NOT NULL
240
243
  );
241
244
 
242
- CREATE TABLE IF NOT EXISTS live_settings_sessions (
243
- id TEXT PRIMARY KEY,
244
- session_json TEXT NOT NULL
245
- );
246
-
247
245
  CREATE TABLE IF NOT EXISTS live_read_cards (
248
246
  id TEXT PRIMARY KEY,
249
247
  owner_id TEXT NOT NULL,
250
248
  token_hash TEXT NOT NULL,
251
- kind TEXT NOT NULL CHECK (kind IN ('account', 'receipt', 'chart', 'connect', 'review')),
249
+ kind TEXT NOT NULL CHECK (kind IN ('account', 'receipt', 'chart', 'connect', 'review', 'settings')),
252
250
  state TEXT NOT NULL CHECK (state IN ('ready', 'running', 'closed')),
253
251
  reason TEXT CHECK (reason IN ('completed', 'expired', 'failed', 'server_restarted', 'cancelled')),
254
252
  error TEXT,
@@ -256,7 +254,7 @@ const CURRENT_SCHEMA_SQL = `
256
254
  created_at TEXT NOT NULL,
257
255
  expires_at TEXT NOT NULL,
258
256
  input_json TEXT NOT NULL,
259
- scope TEXT NOT NULL DEFAULT 'read' CHECK (scope IN ('read', 'connect', 'review', 'review_manage')),
257
+ scope TEXT NOT NULL DEFAULT 'read' CHECK (scope IN ('read', 'connect', 'review', 'review_manage', 'settings')),
260
258
  operation_id TEXT,
261
259
  accepted_input_json TEXT,
262
260
  result_json TEXT,
@@ -266,6 +264,59 @@ const CURRENT_SCHEMA_SQL = `
266
264
  CHECK (receipt_display_json IS NULL OR (kind = 'receipt' AND reason = 'completed'))
267
265
  );
268
266
  CREATE INDEX IF NOT EXISTS idx_live_read_cards_owner_state ON live_read_cards(owner_id, state);
267
+ CREATE TABLE IF NOT EXISTS settings_control (
268
+ id INTEGER PRIMARY KEY CHECK (id=1),
269
+ data_epoch INTEGER NOT NULL DEFAULT 0 CHECK (data_epoch BETWEEN 0 AND 9007199254740991),
270
+ user_data_revision INTEGER NOT NULL DEFAULT 0 CHECK (user_data_revision BETWEEN 0 AND 9007199254740991),
271
+ active_operation_id TEXT,
272
+ file_io_id TEXT,
273
+ wallet_input_blocked INTEGER NOT NULL DEFAULT 0 CHECK (wallet_input_blocked IN (0,1)),
274
+ wallet_block_operation_id TEXT
275
+ );
276
+ INSERT INTO settings_control (id) VALUES (1);
277
+ CREATE TABLE IF NOT EXISTS settings_operations (
278
+ operation_id TEXT PRIMARY KEY,
279
+ card_id TEXT NOT NULL UNIQUE REFERENCES live_read_cards(id) ON DELETE CASCADE,
280
+ owner_id TEXT NOT NULL,
281
+ data_epoch INTEGER NOT NULL CHECK (data_epoch BETWEEN 0 AND 9007199254740991),
282
+ kind TEXT,
283
+ phase TEXT NOT NULL CHECK (phase IN ('draft','uploading','sealed','prepared','executing','stopping','starting','terminal')),
284
+ draft_json TEXT,
285
+ preparation_id TEXT,
286
+ fingerprint TEXT,
287
+ prepared_user_data_revision INTEGER,
288
+ preparation_json TEXT,
289
+ file_id TEXT,
290
+ accepted_digest TEXT,
291
+ admitted_at TEXT,
292
+ result_json TEXT,
293
+ prior_run_id TEXT,
294
+ next_run_id TEXT,
295
+ supersedes_operation_id TEXT,
296
+ superseded_by_operation_id TEXT,
297
+ checkpoint TEXT,
298
+ CHECK ((phase='terminal' AND result_json IS NOT NULL) OR (phase!='terminal' AND result_json IS NULL))
299
+ );
300
+ CREATE TRIGGER IF NOT EXISTS settings_immutable_result
301
+ BEFORE UPDATE ON settings_operations WHEN OLD.result_json IS NOT NULL AND NEW.result_json IS NOT OLD.result_json
302
+ BEGIN SELECT RAISE(ABORT, 'settings terminal result is immutable'); END;
303
+ CREATE TABLE IF NOT EXISTS settings_files (
304
+ file_id TEXT PRIMARY KEY,
305
+ operation_id TEXT NOT NULL,
306
+ cleanup_operation_id TEXT NOT NULL,
307
+ role TEXT NOT NULL CHECK (role IN ('upload','export')),
308
+ relative_path TEXT NOT NULL UNIQUE,
309
+ display_name TEXT NOT NULL,
310
+ total_size INTEGER NOT NULL CHECK (total_size BETWEEN 0 AND 9007199254740991),
311
+ received_size INTEGER NOT NULL CHECK (received_size BETWEEN 0 AND total_size),
312
+ sha256 TEXT NOT NULL,
313
+ sealed INTEGER NOT NULL DEFAULT 0 CHECK (sealed IN (0,1)),
314
+ invalidated INTEGER NOT NULL DEFAULT 0 CHECK (invalidated IN (0,1)),
315
+ cleanup TEXT NOT NULL DEFAULT 'pending' CHECK (cleanup IN ('pending','complete','failed')),
316
+ reservation_json TEXT
317
+ );
318
+ ${settingsRevisionTriggers()}
319
+
269
320
  CREATE TRIGGER IF NOT EXISTS live_read_cards_insert_revision
270
321
  BEFORE INSERT ON live_read_cards WHEN NEW.revision != 0
271
322
  BEGIN SELECT RAISE(ABORT, 'card insert requires revision zero'); END;
@@ -302,8 +353,6 @@ export function configureDatabase(db) {
302
353
  db.exec("PRAGMA foreign_keys=ON");
303
354
  db.exec("PRAGMA busy_timeout=5000");
304
355
  }
305
- // SQLite normalizes its own DDL. Compare only schema metadata, never user rows.
306
- // The reference is built once from the single current schema, not from the DB being checked.
307
356
  let currentSchemaSignature;
308
357
  function schemaSignature(db) {
309
358
  return JSON.stringify(db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name").all());
@@ -322,11 +371,10 @@ function expectedSchemaSignature() {
322
371
  }
323
372
  }
324
373
  export function assertCurrentDatabaseFormat(db) {
325
- const version = db.pragma("user_version", { simple: true });
326
374
  const signature = schemaSignature(db);
327
- if (version === 0 && signature === "[]")
375
+ if (signature === "[]" && !db.prepare("SELECT 1 FROM sqlite_schema LIMIT 1").get())
328
376
  return "empty";
329
- if (version === DB_USER_VERSION && signature === expectedSchemaSignature())
377
+ if (signature === expectedSchemaSignature())
330
378
  return "current";
331
379
  throw new ActivityStoreError("Local database format does not match this runtime. Use a new empty SUI_MCP_DATA_DIR; existing data is not converted or deleted.");
332
380
  }
@@ -335,7 +383,6 @@ export function initializeDatabase(db) {
335
383
  return;
336
384
  db.transaction(() => {
337
385
  db.exec(CURRENT_SCHEMA_SQL);
338
- db.pragma(`user_version = ${DB_USER_VERSION}`);
339
386
  })();
340
387
  }
341
388
  function externalActivityScansTableSql() {
@@ -365,3 +412,23 @@ function externalActivityScansTableSql() {
365
412
  )
366
413
  )`;
367
414
  }
415
+ function settingsRevisionTriggers() {
416
+ const userTables = ["accounts", "active_account_context", "review_sessions", "review_state_snapshots",
417
+ "review_status_transitions", "review_executions", "review_requests", "external_activity_scans",
418
+ "external_activity_transactions", "local_settings"];
419
+ const walletTables = ["live_wallet_connections", "live_review_sessions", "live_request_authority"];
420
+ const invalidate = (condition) => `
421
+ UPDATE live_read_cards SET revision=revision+1
422
+ WHERE id IN (SELECT card_id FROM settings_operations WHERE phase='prepared' AND (${condition}));
423
+ ${SETTINGS_RESET_PREPARATION_SQL}
424
+ WHERE phase='prepared' AND (${condition});`;
425
+ return [...new Set([...userTables, ...walletTables])].flatMap((table) => ["INSERT", "UPDATE", "DELETE"].map((event) => `
426
+ CREATE TRIGGER settings_revision_${table}_${event.toLowerCase()} AFTER ${event} ON ${table}
427
+ BEGIN
428
+ ${userTables.includes(table) ? "UPDATE settings_control SET user_data_revision=user_data_revision+1 WHERE id=1;" : ""}
429
+ ${invalidate([...(userTables.includes(table) ? ["kind IN ('import_local_data','reset_local_data')"] : []),
430
+ ...(walletTables.includes(table) || table === "review_requests" ? ["kind='restart_wallet_service'"] : []),
431
+ ...(table === "local_settings" ? ["kind IN ('set_endpoint','restore_endpoint')"] : []),
432
+ ...(table === "active_account_context" ? ["kind='clear_read_address'"] : [])].join(" OR ") || "0")}
433
+ END;`)).join("\n");
434
+ }
@@ -56,7 +56,8 @@ export class SqlitePreferencesRepository {
56
56
  async resetSuiGraphqlUrl(defaultUrl, now = new Date()) {
57
57
  return this.setSuiGraphqlUrl(defaultUrl, now);
58
58
  }
59
- async getLocalSetting(key) {
59
+ async getLocalSetting(key) { return this.getLocalSettingSync(key); }
60
+ getLocalSettingSync(key) {
60
61
  const settingKey = assertLocalSettingKey(key);
61
62
  const row = this.db
62
63
  .prepare(`SELECT key, value_json, updated_at
@@ -65,7 +66,8 @@ export class SqlitePreferencesRepository {
65
66
  .get(settingKey);
66
67
  return row ? rowToLocalSetting(row) : undefined;
67
68
  }
68
- async setLocalSetting(key, value, now = new Date()) {
69
+ async setLocalSetting(key, value, now = new Date()) { return this.setLocalSettingSync(key, value, now); }
70
+ setLocalSettingSync(key, value, now = new Date()) {
69
71
  const settingKey = assertLocalSettingKey(key);
70
72
  this.db
71
73
  .prepare(`INSERT INTO local_settings (key, value_json, updated_at)
@@ -74,7 +76,7 @@ export class SqlitePreferencesRepository {
74
76
  value_json = excluded.value_json,
75
77
  updated_at = excluded.updated_at`)
76
78
  .run(settingKey, encodeSettingValue(settingKey, value), now.toISOString());
77
- const record = await this.getLocalSetting(settingKey);
79
+ const record = this.getLocalSettingSync(settingKey);
78
80
  if (!record) {
79
81
  throw new PreferencesStoreError("not_recorded", "Local setting was not recorded", { key: settingKey });
80
82
  }
@@ -192,8 +192,6 @@ export function normalizeManagerAddresses(values) {
192
192
  return normalized;
193
193
  });
194
194
  }
195
- // Account inventory v1 exposes only ledger and rebate balances; governance,
196
- // stake, volume, and AccountInfo.open_orders need separate review before export.
197
195
  export function toDeepbookAccountSummary(accountInfo) {
198
196
  return {
199
197
  epoch: accountInfo.epoch,
@@ -2,24 +2,7 @@ import { mainnetCoins, mainnetPools } from "@mysten/deepbook-v3";
2
2
  import { DEEPBOOK_SCALAR_UNIT_SOURCE, decimalsFromScalar, normalizeCoinType } from "./coinMetadata.js";
3
3
  import { ReadServiceInputError } from "./readServiceTypes.js";
4
4
  export const PINNED_DEEPBOOK_COINS = mainnetCoins;
5
- // Curated DeepBook USDC chart pairs: the meaningful Sui-ecosystem base tokens worth
6
- // featuring against USDC, in display order. This is a deliberate editorial allowlist
7
- // — NOT a dynamic volume cut — so the chart shows a stable, hand-picked set and never
8
- // surfaces the dead/dust pools the indexer also lists (stable-vs-stable, wrapped
9
- // duplicates, single-trade listings). Each entry is a token judged worth registering
10
- // on its own merits:
11
- // SUI — Sui L1 gas / native token
12
- // DEEP — DeepBook protocol token (this venue's own token)
13
- // WAL — Walrus decentralized storage (Mysten)
14
- // NS — SuiNS name service
15
- // IKA — Ika MPC / dWallet network
16
- // XBTC — Bitcoin on Sui
17
- // USDT — Tether USD stablecoin
18
- // Edit this list to change which pairs the chart offers; order here is display order.
19
5
  const CURATED_USDC_CHART_BASE_ORDER = new Map(["SUI", "DEEP", "WAL", "NS", "IKA", "XBTC", "USDT"].map((symbol, index) => [symbol, index]));
20
- // Filter the chart pool list to the curated base tokens (by symbol) in curated
21
- // display order. Matching is by base SYMBOL because the indexer's pool name can
22
- // differ from the SDK pool key (e.g. BWETH_USDC vs BETH_USDC).
23
6
  export function curateUsdcChartPools(pools) {
24
7
  return pools
25
8
  .filter((pool) => CURATED_USDC_CHART_BASE_ORDER.has(pool.baseAsset.symbol))
@@ -52,8 +35,6 @@ export function deepbookUnitForCoinType(coinType, coins) {
52
35
  return { decimals, symbol: entry.symbol, name: entry.symbol };
53
36
  }
54
37
  export function findDeepbookCoinEntry(normalizedCoinType, coins) {
55
- // Validate the whole pinned token unit registry before using any DeepBook
56
- // coin match so one bad scalar cannot make the registry look partially trustworthy.
57
38
  assertDeepbookScalars(coins);
58
39
  for (const [symbol, coin] of Object.entries(coins)) {
59
40
  if (normalizeCoinType(coin.type) === normalizedCoinType) {
@@ -35,8 +35,6 @@ export function createDeepbookUsdcChartService(options = {}) {
35
35
  ...chartCommonFields()
36
36
  };
37
37
  }
38
- // Curate to the hand-picked allowlist of meaningful pairs (SUI/USDC, DEEP/USDC,
39
- // …), in display order; the indexer's dead/dust pools never appear.
40
38
  const pools = curateUsdcChartPools(selectDeepbookOfficialIndexerCanonicalUsdcPools(poolResult.pools).map(deepbookUsdcPriceHistoryPairFromOfficialPool));
41
39
  return {
42
40
  status: "ok",
@@ -12,8 +12,6 @@ const foundReceipt = z.object({ status: z.literal("found"), receipt: z.object({
12
12
  inputs: z.array(receiptInput).refine((items) => new Set(items.map((item) => item.index)).size === items.length),
13
13
  ptbGraph: receiptDisplaySchema.shape.ptbGraph
14
14
  }).passthrough() }).strict();
15
- // Partition the same source result before persistence. Never weaken the public
16
- // forbidden-field check or turn arbitrary fields into private metadata.
17
15
  export function projectReadCardResult(kind, input, source) {
18
16
  if (kind !== "receipt" || !source || typeof source !== "object" || !("status" in source) || source.status !== "found") {
19
17
  assertNoForbiddenMcpFields(source);