@zktx.io/sui-mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +6 -6
  2. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +3 -3
  3. package/dist/core/activity/localDataService.js +71 -52
  4. package/dist/core/activity/localDataValidation.js +1 -3
  5. package/dist/core/activity/sqliteActivityStore.js +24 -16
  6. package/dist/core/activity/sqliteActivityStoreSchema.js +83 -12
  7. package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
  8. package/dist/core/session/cardSession.js +1 -1
  9. package/dist/core/session/cardSessionStore.js +53 -3
  10. package/dist/core/session/sessionStore.js +0 -17
  11. package/dist/core/session/settingsFileIdentity.js +11 -0
  12. package/dist/core/session/settingsFileProtocol.js +25 -0
  13. package/dist/core/session/settingsFileStore.js +346 -0
  14. package/dist/core/session/settingsInputError.js +34 -0
  15. package/dist/core/session/settingsOperation.js +224 -0
  16. package/dist/core/session/settingsPreparationSql.js +15 -0
  17. package/dist/core/session/settingsWorkflow.js +1219 -0
  18. package/dist/core/session/sqliteSessionStore.js +0 -40
  19. package/dist/core/session/sqliteSettingsWorkflowStore.js +463 -0
  20. package/dist/core/session/sqliteWalletWorkflowStore.js +164 -41
  21. package/dist/core/session/status.js +2 -2
  22. package/dist/core/session/walletConnection.js +27 -8
  23. package/dist/core/session/walletWorkflow.js +73 -15
  24. package/dist/core/session/workflowState.js +16 -8
  25. package/dist/core/session/workflowView.js +18 -2
  26. package/dist/mcp/responseGuidance.js +10 -5
  27. package/dist/mcp/serverInfo.js +3 -4
  28. package/dist/mcp/toolNames.js +2 -2
  29. package/dist/mcp/tools/account/index.js +1 -19
  30. package/dist/mcp/tools/read/readToolHelpers.js +5 -4
  31. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  32. package/dist/mcp/tools/session/shared.js +1 -0
  33. package/dist/mcp/tools/session/statusTools.js +4 -3
  34. package/dist/mcp/tools/settings/index.js +8 -28
  35. package/dist/mcp-app/account.html +14 -14
  36. package/dist/mcp-app/account.notices.txt +1 -1
  37. package/dist/mcp-app/chart.html +9 -9
  38. package/dist/mcp-app/chart.notices.txt +1 -1
  39. package/dist/mcp-app/connect.html +14 -14
  40. package/dist/mcp-app/connect.notices.txt +1 -1
  41. package/dist/mcp-app/receipt.html +1298 -1463
  42. package/dist/mcp-app/receipt.notices.txt +226 -1204
  43. package/dist/mcp-app/review.html +1298 -1463
  44. package/dist/mcp-app/review.notices.txt +226 -1204
  45. package/dist/mcp-app/settings.html +3133 -0
  46. package/dist/mcp-app/settings.notices.txt +973 -0
  47. package/dist/mcp-ui/contracts.js +8 -2
  48. package/dist/mcp-ui/readCards.js +1 -1
  49. package/dist/mcp-ui/tools.js +33 -2
  50. package/dist/review-server/server.js +5 -51
  51. package/dist/runtime/application.js +41 -14
  52. package/dist/runtime/localSettingsService.js +26 -2
  53. package/dist/runtime/shared/authenticatedFetch.js +6 -2
  54. package/dist/runtime/shared/control.js +5 -6
  55. package/dist/runtime/shared/http.js +14 -0
  56. package/dist/runtime/shared/mcpHttp.js +9 -4
  57. package/dist/runtime/shared/server.js +1 -1
  58. package/dist/runtime/smokeMainnetRead.js +0 -1
  59. package/dist/runtime/walletConnectStorage.js +2 -4
  60. package/dist/runtime/walletSdkChildRuntime.js +2 -1
  61. package/dist/runtime/walletSdkIpc.js +2 -1
  62. package/dist/runtime/walletSdkProcess.js +3 -2
  63. package/docs/AGENT_BEHAVIOR.md +44 -10
  64. package/docs/AGENT_DEVELOPMENT_POLICY.md +16 -12
  65. package/docs/FRONTEND_POLICY.md +105 -28
  66. package/docs/LOCAL_DB_ARCHITECTURE.md +143 -20
  67. package/docs/MCP_SETUP.md +99 -39
  68. package/docs/MCP_TOOLS.md +273 -24
  69. package/docs/SDK_API.md +51 -10
  70. package/docs/UTILITY_INDEX.md +9 -6
  71. package/docs/WALLET_CONNECTION.md +30 -9
  72. package/package.json +13 -7
  73. package/dist/core/activity/schemaVersion.js +0 -2
  74. package/dist/core/session/keyedRecordStore.js +0 -18
  75. package/dist/core/session/settingsSession.js +0 -1
  76. package/dist/core/session/settingsSessions.js +0 -45
  77. package/dist/review-app/brand-dark.svg +0 -1
  78. package/dist/review-app/brand-light.svg +0 -1
  79. package/dist/review-app/favicon.svg +0 -1
  80. package/dist/review-app/settings.css +0 -1
  81. package/dist/review-app/settings.js +0 -1
  82. package/dist/review-app/ui.css +0 -809
  83. package/dist/review-server/assets.js +0 -48
  84. package/dist/review-server/html.js +0 -45
  85. package/dist/review-server/http.js +0 -47
  86. package/dist/review-server/middleware/reviewToken.js +0 -7
  87. package/dist/review-server/settingsApi.js +0 -158
package/README.md CHANGED
@@ -16,8 +16,8 @@ cause another signing or submission request. Request failures and unknown
16
16
  outcomes are separate from independently observed chain success/failure.
17
17
 
18
18
  Account, Receipt and Chart cards provide read-only evidence independently of
19
- signing. Connect and Review also run inside the MCP host. Settings currently
20
- uses a local token page. Human-readable review is not a guarantee of safety or
19
+ signing. Connect, Review and Settings also run inside the MCP host. Settings
20
+ provides local data controls and explicit connection-service recovery. Human-readable review is not a guarantee of safety or
21
21
  of complete interpretation of every transaction detail; the raw PTB structure
22
22
  and addresses remain inspectable.
23
23
 
@@ -27,13 +27,13 @@ Users can ask ordinary questions:
27
27
  - "How much are my USD-denominated assets together?"
28
28
  - "What is the shortfall?"
29
29
 
30
- This document describes `@zktx.io/sui-mcp@0.5.0`. Confirm that version is
30
+ This document describes `@zktx.io/sui-mcp@0.6.0`. Confirm that version is
31
31
  published before using npm installation commands. For a local build, follow
32
32
  [Developer Checkout Setup](docs/MCP_SETUP.md#developer-checkout-setup).
33
33
 
34
34
  ## Product Scope in Two Layers
35
35
 
36
- - **Implemented:** Sui mainnet read evidence, non-signable external proposal review, and account-bound DeepBook swap review using protocol-agnostic adapters. Five internal cards use the same SQLite state and permission model. The backend verifies stored material, ownership, quote/policy provenance, human-readable facts, simulation and PTB evidence before an explicit user request. WalletConnect carries bytes only between the backend and wallet; ordinary MCP responses expose facts, request status and observed execution results. Settings remains a local token page; an internal Settings card is not implemented.
36
+ - **Implemented:** Sui mainnet read evidence, non-signable external proposal review, and account-bound DeepBook swap review using protocol-agnostic adapters. Six internal cards use the same SQLite state and permission model. The backend verifies stored material, ownership, quote/policy provenance, human-readable facts, simulation and PTB evidence before an explicit user request. WalletConnect carries bytes only between the backend and wallet; ordinary MCP responses expose facts, request status and observed execution results. Settings owns endpoint and read-address controls, local backup/import/reset, and explicit connection-service recovery. Settings operations require scoped user confirmation; ordinary MCP calls only create the card or read settings.
37
37
  - **Never (permanently unsupported at every layer):** no private-key custody, no MCP or AI autonomous execution, no forwarding of opaque external transaction bytes to a wallet, no silent settlement-token or route choice, no fiat cash-out, no P&L, no peg guarantee.
38
38
 
39
39
  DeepBook provides the current scoped Sui conversion, price, and orderbook
@@ -193,12 +193,12 @@ running from a local checkout, see [docs/MCP_SETUP.md](docs/MCP_SETUP.md).
193
193
 
194
194
  ### Package and repository transition
195
195
 
196
- The package for this release is `@zktx.io/sui-mcp@0.5.0`, and its repository is
196
+ The package for this release is `@zktx.io/sui-mcp@0.6.0`, and its repository is
197
197
  `zktx-mcp/sui-mcp`. The previous package is `@zktx.io/say-ur-intent` (through
198
198
  0.4.4). The older `@stelis/say-ur-intent@0.4.0` is also a separate package.
199
199
  Existing installations and MCP client registrations do not change names
200
200
  automatically. Before switching, confirm the new package with
201
- `npm view @zktx.io/sui-mcp@0.5.0 version`; renaming the repository does not publish
201
+ `npm view @zktx.io/sui-mcp@0.6.0 version`; renaming the repository does not publish
202
202
  an npm package or MCP Registry entry.
203
203
 
204
204
  Use the full scoped package name: the unscoped `sui-mcp` package is a different
@@ -112,9 +112,9 @@ export function createDeepbookSwapTransactionMaterialProducer(options) {
112
112
  });
113
113
  const transaction = new Transaction();
114
114
  transaction.setSender(input.account);
115
- // The DeepBook SDK reserves a 0.25 SUI gas budget by default, which
116
- // blocks small wallets whose balance is close to the swap amount. A
117
- // mainnet DeepBook swap costs well under this explicit 0.05 SUI budget.
115
+ // Set the product's explicit 0.05 SUI spending cap before the SDK's
116
+ // setGasBudgetIfNotSet fallback (0.25 SUI). This is not a cost estimate;
117
+ // funding and checks-enabled simulation must validate each transaction.
118
118
  transaction.setGasBudget(DEEPBOOK_SWAP_GAS_BUDGET_MIST);
119
119
  // input_coin fee mode swaps with an explicit zero DEEP coin; the Move
120
120
  // entry point then charges the taker fee in the source coin at the
@@ -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,48 @@ 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
+ // The caller commits this primitive with the admitted Settings card/result.
87
+ // It contains no async gap or post-commit reconstruction of its own authority.
88
+ applyValidatedReplacement(envelope, now, preserveCardId) {
89
+ this.options.advanceRequestDeadlines(now);
90
+ this.assertReplacementAllowed(now);
91
+ this.resetLocalDataTables(preserveCardId);
92
+ if (envelope) {
77
93
  this.insertAccounts(envelope.data.accounts);
78
94
  this.insertActiveAccountContext(envelope.data.activeAccountContext);
79
95
  this.insertReviewSessions(envelope.data.reviewSessions);
@@ -85,39 +101,37 @@ export class SqliteLocalDataService {
85
101
  this.insertExternalActivityTransactions(envelope.data.externalActivityTransactions);
86
102
  this.insertLocalSettings(envelope.data.localSettings);
87
103
  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();
104
+ }
105
+ else
102
106
  this.insertDefaultSuiGrpcUrl(now);
103
- })();
104
- this.options.onDataReplaced?.();
105
- return {
106
- status: "reset",
107
- dataCounts: this.getDataCountsSync(),
108
- sessionsInvalidated: true
109
- };
107
+ this.db.prepare("UPDATE settings_control SET data_epoch=data_epoch+1 WHERE id=1").run();
108
+ if (preserveCardId)
109
+ this.db.prepare(`UPDATE settings_operations SET data_epoch=(SELECT data_epoch FROM settings_control WHERE id=1)
110
+ WHERE card_id=?`).run(preserveCardId);
111
+ return { status: envelope ? "imported" : "reset", dataCounts: this.getDataCountsSync(), sessionsInvalidated: true };
110
112
  }
111
- resetLocalDataTables() {
113
+ notifyDataReplaced() { this.options.onDataReplaced?.(); }
114
+ resetLocalDataTables(preserveCardId) {
115
+ this.db.prepare(`UPDATE settings_files SET invalidated=1 WHERE role='upload' OR ${SETTINGS_DOWNLOAD_FILE_SQL}`).run();
116
+ if (preserveCardId) {
117
+ // Deleted cards cannot report or retry their cleanup. Transfer cleanup
118
+ // responsibility while preserving file identities and public backup paths.
119
+ // Uploads remain invalidated and never replace the selected input.
120
+ const operation = this.db.prepare("SELECT operation_id FROM settings_operations WHERE card_id=?").get(preserveCardId);
121
+ if (!operation)
122
+ throw new LocalDataError("internal_error", "The replacement result identity is unavailable.");
123
+ this.db.prepare(`UPDATE settings_files SET cleanup_operation_id=? WHERE cleanup!='complete'
124
+ AND ${SETTINGS_FILE_CLEANUP_SQL}`).run(operation.operation_id);
125
+ this.db.prepare("DELETE FROM live_read_cards WHERE id!=?").run(preserveCardId);
126
+ }
127
+ else
128
+ this.db.prepare("DELETE FROM live_read_cards").run();
112
129
  for (const table of [
113
- "live_read_cards",
114
130
  "live_execution_details",
115
131
  "live_request_authority",
116
- "live_wallet_connections",
117
132
  "live_transaction_materials",
118
133
  "live_private_review_artifacts",
119
134
  "live_review_sessions",
120
- "live_settings_sessions",
121
135
  "external_activity_transactions",
122
136
  "external_activity_scans",
123
137
  "review_state_snapshots",
@@ -259,6 +273,11 @@ export class SqliteLocalDataService {
259
273
  }
260
274
  }
261
275
  }
276
+ // Preparation and final replacement use the same rule without starting wallet I/O.
277
+ assertReplacementAllowed(now = this.now()) {
278
+ if (hasUnsettledWalletWork(this.db, now))
279
+ throw new LocalDataError("input_invalid", "A wallet request is still unsettled.", { reason: "wallet_request_unsettled" });
280
+ }
262
281
  async parseAndVerifyImportEnvelope(input) {
263
282
  const envelope = parseLocalDataEnvelope(input, {
264
283
  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";
@@ -117,7 +118,8 @@ export class SqliteActivityStore {
117
118
  async getActiveAccount() {
118
119
  return this.getActiveAccountSync();
119
120
  }
120
- async clearActiveAccount(now = new Date()) {
121
+ async clearActiveAccount(now = new Date()) { this.clearActiveAccountSync(now); }
122
+ clearActiveAccountSync(now = new Date()) {
121
123
  this.db
122
124
  .prepare(`INSERT INTO active_account_context (id, account_id, source, set_at)
123
125
  VALUES (?, NULL, 'cleared', ?)
@@ -657,11 +659,14 @@ export class SqliteActivityStore {
657
659
  createCardRecordStore() {
658
660
  return new SqliteCardRecordStore(this.db);
659
661
  }
662
+ createSettingsWorkflowStore(ownerId, now = () => new Date()) {
663
+ return new SqliteSettingsWorkflowStore(this.db, ownerId, now);
664
+ }
660
665
  createPrivateReviewArtifactStore() {
661
666
  return new SqlitePrivateReviewArtifactStore(this.db);
662
667
  }
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());
668
+ createWalletWorkflowStore(ownerId, clock = () => new Date(), readSdkSnapshot = () => undefined) {
669
+ 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
670
  return records;
666
671
  }
667
672
  finalizeReviewEvaluation(candidate, clock) {
@@ -694,19 +699,26 @@ export class SqliteActivityStore {
694
699
  return { session: next, events: next.status === "expired" ? [] : [{ type: "state.computed", sessionId: next.id,
695
700
  status: next.status, reason: "private_review_artifacts_refresh_required", at: now.toISOString() }] };
696
701
  }
697
- evaluateWorkflowState(input, records, ownerId, clock) {
702
+ reconcileWorkflowState(input, records, ownerId, clock) {
698
703
  return this.db.transaction(() => {
699
704
  const now = clock();
705
+ if (input.purpose !== "current_wallet" && input.purpose !== "review" ||
706
+ input.purpose === "review" && (!input.readTarget || !input.candidate)) {
707
+ throw new SessionStoreError("input_invalid", "An explicit workflow evaluation target is required.");
708
+ }
700
709
  const cards = this.createCardRecordStore();
701
710
  let record = input.expectedCard && records.currentCard(input.expectedCard);
702
- const reviewId = record?.state.kind === "review" ? String(record.state.input.reviewSessionId) : input.reviewSessionId;
711
+ const reviewId = input.purpose === "review" ? input.reviewSessionId : undefined;
712
+ if (record && (input.purpose === "review" ? record.state.kind !== "review" || record.state.input.reviewSessionId !== reviewId : record.state.kind !== "connect")) {
713
+ throw new SessionStoreError("session_mismatch", "The workflow target does not match this card.");
714
+ }
703
715
  const target = reviewId ? records.reviewReadTarget(reviewId, record) : undefined;
704
- if (input.readTarget && (!target || input.readTarget.reviewSessionId !== target.reviewSessionId ||
716
+ if (input.purpose === "review" && (!target || input.readTarget.reviewSessionId !== target.reviewSessionId ||
705
717
  input.readTarget.attemptId !== target.attemptId || !input.readTarget.walletDependent && target.walletDependent)) {
706
718
  throw new SessionStoreError("session_mismatch", "The review target changed while its state was being read. Check this review again.");
707
719
  }
708
720
  let evaluated;
709
- if (reviewId) {
721
+ if (input.purpose === "review") {
710
722
  if (!input.candidate || input.candidate.session.id !== reviewId)
711
723
  throw new SessionStoreError("session_not_found", "The saved review is unavailable.");
712
724
  evaluated = this.finalizeReviewEvaluationAt(input.candidate, now);
@@ -726,14 +738,13 @@ export class SqliteActivityStore {
726
738
  const authority = request && records.authority(request.attemptId);
727
739
  const details = request?.execution && records.executionDetails(request.attemptId);
728
740
  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,
741
+ const connectionFacts = records.readConnectionFacts(now), targetAccount = session?.account ?? connectionFacts.activeAccount;
742
+ const facts = { ...connectionFacts, ownerId, record, session, request, authority,
731
743
  hasReviewInput: !!session && session.status !== "expired" && Date.parse(session.expiresAt) > now.getTime() &&
732
744
  cards.hasReviewInput(session.id, ownerId, now),
733
- walletAvailability: input.walletAvailability, walletObservation: input.walletObservation,
734
- runtimeRecovery: record && records.walletRecovery(record), activeAccount, activeAccountConnectionId: storedAccount?.walletId,
745
+ walletAvailability: input.walletAvailability,
746
+ runtimeRecovery: record && records.walletRecovery(record),
735
747
  walletDependent: !target || target.walletDependent,
736
- connections: records.connectionViews(),
737
748
  connection: connection ? records.connectionView(connection.connection) : undefined,
738
749
  boundReview: request && (record?.operationId || record?.scope === "review_manage") ? records.requestReview(request.attemptId) : undefined,
739
750
  busyForAccount: !!targetAccount && records.busyForAccount(targetAccount, now),
@@ -741,9 +752,6 @@ export class SqliteActivityStore {
741
752
  return { ...facts, ...workflowEligibility(facts), events: evaluated?.events ?? [] };
742
753
  }).immediate();
743
754
  }
744
- createSettingsRecordStore() {
745
- return createSqliteSettingsRecordStore(this.db);
746
- }
747
755
  runLiveReviewSessionMutation(live, writeActivity) {
748
756
  const stale = new Error("live review session changed before commit");
749
757
  try {
@@ -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;
@@ -322,11 +373,10 @@ function expectedSchemaSignature() {
322
373
  }
323
374
  }
324
375
  export function assertCurrentDatabaseFormat(db) {
325
- const version = db.pragma("user_version", { simple: true });
326
376
  const signature = schemaSignature(db);
327
- if (version === 0 && signature === "[]")
377
+ if (signature === "[]" && !db.prepare("SELECT 1 FROM sqlite_schema LIMIT 1").get())
328
378
  return "empty";
329
- if (version === DB_USER_VERSION && signature === expectedSchemaSignature())
379
+ if (signature === expectedSchemaSignature())
330
380
  return "current";
331
381
  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
382
  }
@@ -335,7 +385,6 @@ export function initializeDatabase(db) {
335
385
  return;
336
386
  db.transaction(() => {
337
387
  db.exec(CURRENT_SCHEMA_SQL);
338
- db.pragma(`user_version = ${DB_USER_VERSION}`);
339
388
  })();
340
389
  }
341
390
  function externalActivityScansTableSql() {
@@ -365,3 +414,25 @@ function externalActivityScansTableSql() {
365
414
  )
366
415
  )`;
367
416
  }
417
+ // Exported user data has one durable revision, including writers outside Settings.
418
+ // A prepared confirmation is invalidated once, in the writer's own transaction.
419
+ function settingsRevisionTriggers() {
420
+ const userTables = ["accounts", "active_account_context", "review_sessions", "review_state_snapshots",
421
+ "review_status_transitions", "review_executions", "review_requests", "external_activity_scans",
422
+ "external_activity_transactions", "local_settings"];
423
+ const walletTables = ["live_wallet_connections", "live_review_sessions", "live_request_authority"];
424
+ const invalidate = (condition) => `
425
+ UPDATE live_read_cards SET revision=revision+1
426
+ WHERE id IN (SELECT card_id FROM settings_operations WHERE phase='prepared' AND (${condition}));
427
+ ${SETTINGS_RESET_PREPARATION_SQL}
428
+ WHERE phase='prepared' AND (${condition});`;
429
+ return [...new Set([...userTables, ...walletTables])].flatMap((table) => ["INSERT", "UPDATE", "DELETE"].map((event) => `
430
+ CREATE TRIGGER settings_revision_${table}_${event.toLowerCase()} AFTER ${event} ON ${table}
431
+ BEGIN
432
+ ${userTables.includes(table) ? "UPDATE settings_control SET user_data_revision=user_data_revision+1 WHERE id=1;" : ""}
433
+ ${invalidate([...(userTables.includes(table) ? ["kind IN ('import_local_data','reset_local_data')"] : []),
434
+ ...(walletTables.includes(table) || table === "review_requests" ? ["kind='restart_wallet_service'"] : []),
435
+ ...(table === "local_settings" ? ["kind IN ('set_endpoint','restore_endpoint')"] : []),
436
+ ...(table === "active_account_context" ? ["kind='clear_read_address'"] : [])].join(" OR ") || "0")}
437
+ END;`)).join("\n");
438
+ }
@@ -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
  }
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  export const walletDisplaySchema = z.object({
3
3
  connectionId: z.string().min(1), pairingUri: z.string().startsWith("wc:"), expiresAt: z.string().datetime()
4
4
  }).strict();
5
- export const cardKindSchema = z.enum(["account", "receipt", "chart", "connect", "review"]);
5
+ export const cardKindSchema = z.enum(["account", "receipt", "chart", "connect", "review", "settings"]);
6
6
  export const cardStateSchema = z.object({
7
7
  cardId: z.string().min(1), kind: cardKindSchema,
8
8
  state: z.enum(["ready", "running", "closed"]),