@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.
- package/README.md +6 -6
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +3 -3
- package/dist/core/activity/localDataService.js +71 -52
- package/dist/core/activity/localDataValidation.js +1 -3
- package/dist/core/activity/sqliteActivityStore.js +24 -16
- package/dist/core/activity/sqliteActivityStoreSchema.js +83 -12
- package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +53 -3
- package/dist/core/session/sessionStore.js +0 -17
- package/dist/core/session/settingsFileIdentity.js +11 -0
- package/dist/core/session/settingsFileProtocol.js +25 -0
- package/dist/core/session/settingsFileStore.js +346 -0
- package/dist/core/session/settingsInputError.js +34 -0
- package/dist/core/session/settingsOperation.js +224 -0
- package/dist/core/session/settingsPreparationSql.js +15 -0
- package/dist/core/session/settingsWorkflow.js +1219 -0
- package/dist/core/session/sqliteSessionStore.js +0 -40
- package/dist/core/session/sqliteSettingsWorkflowStore.js +463 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +164 -41
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/walletConnection.js +27 -8
- package/dist/core/session/walletWorkflow.js +73 -15
- package/dist/core/session/workflowState.js +16 -8
- package/dist/core/session/workflowView.js +18 -2
- package/dist/mcp/responseGuidance.js +10 -5
- package/dist/mcp/serverInfo.js +3 -4
- package/dist/mcp/toolNames.js +2 -2
- package/dist/mcp/tools/account/index.js +1 -19
- package/dist/mcp/tools/read/readToolHelpers.js +5 -4
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +1 -0
- package/dist/mcp/tools/session/statusTools.js +4 -3
- package/dist/mcp/tools/settings/index.js +8 -28
- package/dist/mcp-app/account.html +14 -14
- package/dist/mcp-app/account.notices.txt +1 -1
- package/dist/mcp-app/chart.html +9 -9
- package/dist/mcp-app/chart.notices.txt +1 -1
- package/dist/mcp-app/connect.html +14 -14
- package/dist/mcp-app/connect.notices.txt +1 -1
- package/dist/mcp-app/receipt.html +1298 -1463
- package/dist/mcp-app/receipt.notices.txt +226 -1204
- package/dist/mcp-app/review.html +1298 -1463
- package/dist/mcp-app/review.notices.txt +226 -1204
- package/dist/mcp-app/settings.html +3133 -0
- package/dist/mcp-app/settings.notices.txt +973 -0
- package/dist/mcp-ui/contracts.js +8 -2
- package/dist/mcp-ui/readCards.js +1 -1
- package/dist/mcp-ui/tools.js +33 -2
- package/dist/review-server/server.js +5 -51
- package/dist/runtime/application.js +41 -14
- package/dist/runtime/localSettingsService.js +26 -2
- package/dist/runtime/shared/authenticatedFetch.js +6 -2
- package/dist/runtime/shared/control.js +5 -6
- package/dist/runtime/shared/http.js +14 -0
- package/dist/runtime/shared/mcpHttp.js +9 -4
- package/dist/runtime/shared/server.js +1 -1
- package/dist/runtime/smokeMainnetRead.js +0 -1
- package/dist/runtime/walletConnectStorage.js +2 -4
- package/dist/runtime/walletSdkChildRuntime.js +2 -1
- package/dist/runtime/walletSdkIpc.js +2 -1
- package/dist/runtime/walletSdkProcess.js +3 -2
- package/docs/AGENT_BEHAVIOR.md +44 -10
- package/docs/AGENT_DEVELOPMENT_POLICY.md +16 -12
- package/docs/FRONTEND_POLICY.md +105 -28
- package/docs/LOCAL_DB_ARCHITECTURE.md +143 -20
- package/docs/MCP_SETUP.md +99 -39
- package/docs/MCP_TOOLS.md +273 -24
- package/docs/SDK_API.md +51 -10
- package/docs/UTILITY_INDEX.md +9 -6
- package/docs/WALLET_CONNECTION.md +30 -9
- package/package.json +13 -7
- package/dist/core/activity/schemaVersion.js +0 -2
- package/dist/core/session/keyedRecordStore.js +0 -18
- package/dist/core/session/settingsSession.js +0 -1
- package/dist/core/session/settingsSessions.js +0 -45
- package/dist/review-app/brand-dark.svg +0 -1
- package/dist/review-app/brand-light.svg +0 -1
- package/dist/review-app/favicon.svg +0 -1
- package/dist/review-app/settings.css +0 -1
- package/dist/review-app/settings.js +0 -1
- package/dist/review-app/ui.css +0 -809
- package/dist/review-server/assets.js +0 -48
- package/dist/review-server/html.js +0 -45
- package/dist/review-server/http.js +0 -47
- package/dist/review-server/middleware/reviewToken.js +0 -7
- 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
|
|
20
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
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.
|
|
18
|
+
return this.currentDataCountsSync();
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
currentDataCountsSync(now = this.now()) {
|
|
21
21
|
return this.db.transaction(() => {
|
|
22
|
-
this.options.advanceRequestDeadlines(
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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 (
|
|
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
|
|
730
|
-
const facts = {
|
|
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,
|
|
734
|
-
runtimeRecovery: record && records.walletRecovery(record),
|
|
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 (
|
|
377
|
+
if (signature === "[]" && !db.prepare("SELECT 1 FROM sqlite_schema LIMIT 1").get())
|
|
328
378
|
return "empty";
|
|
329
|
-
if (
|
|
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 =
|
|
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"]),
|