@zktx.io/sui-mcp 0.7.0 → 0.8.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.
- package/README.md +89 -42
- package/dist/core/activity/localDataService.js +8 -21
- package/dist/core/activity/localDataValidation.js +22 -46
- package/dist/core/activity/sqliteAccountStore.js +46 -0
- package/dist/core/activity/sqliteActivityStore.js +65 -147
- package/dist/core/activity/sqliteActivityStoreSchema.js +44 -28
- package/dist/core/activity/sqliteActivityStoreTypes.js +9 -0
- package/dist/core/activity/transactionActivityService.js +2 -2
- package/dist/core/preferences/preferencesStore.js +2 -2
- package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
- package/dist/core/session/cardResult.js +41 -0
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +51 -19
- package/dist/core/session/chainAccounts.js +120 -0
- package/dist/core/session/sessionStore.js +4 -8
- package/dist/core/session/settingsFileStore.js +3 -30
- package/dist/core/session/settingsOperation.js +27 -19
- package/dist/core/session/settingsWorkflow.js +161 -87
- package/dist/core/session/sqliteCardStore.js +34 -14
- package/dist/core/session/sqliteSessionStore.js +5 -5
- package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
- package/dist/core/session/sqliteTransactionRequest.js +18 -0
- package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/wait.js +6 -11
- package/dist/core/session/walletConnection.js +71 -46
- package/dist/core/session/walletConnectionOperations.js +24 -0
- package/dist/core/session/walletRuntime.js +7 -13
- package/dist/core/session/walletWorkflow.js +248 -99
- package/dist/core/session/workflowState.js +104 -38
- package/dist/core/session/workflowView.js +92 -101
- package/dist/mcp/activeAccountResponse.js +3 -0
- package/dist/mcp/responseGuidance.js +7 -9
- package/dist/mcp/serverInfo.js +11 -10
- package/dist/mcp/toolErrors.js +5 -3
- package/dist/mcp/toolNames.js +1 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +9 -16
- package/dist/mcp/tools/read/readToolHelpers.js +9 -7
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +2 -2
- package/dist/mcp/tools/session/statusTools.js +12 -10
- package/dist/mcp/tools/session/walletConnectionTools.js +26 -14
- package/dist/mcp/tools/settings/index.js +7 -4
- package/dist/mcp-app/account.html +7 -7
- package/dist/mcp-app/chart.html +3 -3
- package/dist/mcp-app/connect.html +13 -13
- package/dist/mcp-app/disconnect.html +3637 -0
- package/dist/mcp-app/disconnect.notices.txt +1480 -0
- package/dist/mcp-app/receipt.html +393 -393
- package/dist/mcp-app/review.html +459 -459
- package/dist/mcp-app/settings.html +7 -7
- package/dist/mcp-ui/contracts.js +3 -1
- package/dist/mcp-ui/tools.js +4 -4
- package/dist/runtime/application.js +1 -1
- package/dist/runtime/localSettingsService.js +7 -4
- package/dist/runtime/runtimeTermination.js +3 -0
- package/dist/runtime/smokeMainnetRead.js +5 -4
- package/dist/runtime/walletConnectTransport.js +16 -16
- package/dist/runtime/walletSdkChildRuntime.js +29 -12
- package/dist/runtime/walletSdkIpc.js +5 -4
- package/dist/runtime/walletSdkProcess.js +24 -8
- package/docs/AGENT_BEHAVIOR.md +82 -54
- package/docs/LOCAL_DB_ARCHITECTURE.md +100 -70
- package/docs/MCP_SETUP.md +384 -347
- package/docs/MCP_TOOLS.md +292 -177
- package/docs/SDK_API.md +15 -6
- package/docs/TRANSACTION_ACTIVITY_LOG.md +4 -0
- package/docs/UTILITY_INDEX.md +9 -7
- package/docs/WALLET_CONNECTION.md +162 -167
- package/package.json +1 -1
|
@@ -17,11 +17,29 @@ export class SqliteSettingsWorkflowStore {
|
|
|
17
17
|
}
|
|
18
18
|
transaction(work) { return this.db.transaction(work).immediate(); }
|
|
19
19
|
control() { return this.db.prepare("SELECT * FROM settings_control WHERE id=1").get(); }
|
|
20
|
+
clearReadAddress(connectionId, account) {
|
|
21
|
+
const changed = this.db.prepare(`UPDATE live_wallet_connections SET read_address_cleared=1
|
|
22
|
+
WHERE id=? AND current_slot=1 AND read_address_cleared=0 AND data_epoch=(SELECT data_epoch FROM settings_control WHERE id=1)
|
|
23
|
+
AND EXISTS(SELECT 1 FROM json_each(connection_json,'$.chainResults') r WHERE json_extract(r.value,'$.chainId')='sui:mainnet'
|
|
24
|
+
AND json_extract(r.value,'$.status')='bound' AND json_extract(r.value,'$.address')=?)`).run(connectionId, account);
|
|
25
|
+
if (!changed.changes)
|
|
26
|
+
throw new SettingsConflict("The saved read address changed. Check the current address before clearing it.");
|
|
27
|
+
}
|
|
20
28
|
create(record) {
|
|
21
29
|
this.transaction(() => {
|
|
22
30
|
if (record.scope !== "settings" || record.state.kind !== "settings" || !record.operationId || record.ownerId !== this.ownerId) {
|
|
23
31
|
throw new SettingsConflict("This Settings card could not be identified.");
|
|
24
32
|
}
|
|
33
|
+
if (record.state.input.backupDelivery) {
|
|
34
|
+
const ref = record.state.input.backupDelivery;
|
|
35
|
+
const op = this.get(ref.operationId), file = this.file(ref.fileId), download = op && settingsDownload(op);
|
|
36
|
+
if (!op || op.ownerId !== this.ownerId || op.dataEpoch !== this.control().data_epoch || !file || op.fileId !== file.fileId ||
|
|
37
|
+
!download?.readyAt || download.endReason || !download.expiresAt || Date.parse(download.expiresAt) <= this.now().getTime() || file.invalidated ||
|
|
38
|
+
download.localSave?.status === "saving")
|
|
39
|
+
throw new SettingsConflict("This backup is no longer available for another delivery. Request a new backup.");
|
|
40
|
+
this.cards.create(record);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
25
43
|
this.cards.create(record);
|
|
26
44
|
this.db.prepare(`INSERT INTO settings_operations (operation_id,card_id,owner_id,data_epoch,phase)
|
|
27
45
|
VALUES (?,?,?,?,'draft')`).run(record.operationId, record.state.cardId, this.ownerId, this.control().data_epoch);
|
|
@@ -44,12 +62,13 @@ export class SqliteSettingsWorkflowStore {
|
|
|
44
62
|
...(row.prior_run_id === null ? {} : { priorRunId: row.prior_run_id }),
|
|
45
63
|
...(row.next_run_id === null ? {} : { nextRunId: row.next_run_id }),
|
|
46
64
|
...(row.supersedes_operation_id === null ? {} : { supersedesOperationId: row.supersedes_operation_id }),
|
|
47
|
-
...(row.superseded_by_operation_id === null ? {} : { supersededByOperationId: row.superseded_by_operation_id })
|
|
48
|
-
...(row.checkpoint === null ? {} : { checkpoint: row.checkpoint }) };
|
|
65
|
+
...(row.superseded_by_operation_id === null ? {} : { supersededByOperationId: row.superseded_by_operation_id }) };
|
|
49
66
|
}
|
|
50
67
|
forCard(record) {
|
|
51
68
|
const operation = record.operationId && this.get(record.operationId);
|
|
52
|
-
if (!operation || operation.cardId !== record.state.cardId
|
|
69
|
+
if (!operation || record.scope !== "settings" || operation.cardId !== record.state.cardId &&
|
|
70
|
+
!(record.state.input.backupDelivery && record.state.input.backupDelivery.operationId === operation.operationId &&
|
|
71
|
+
record.state.input.backupDelivery.fileId === operation.fileId)) {
|
|
53
72
|
throw new SettingsConflict("This Settings request does not match the open card.");
|
|
54
73
|
}
|
|
55
74
|
return operation;
|
|
@@ -57,7 +76,7 @@ export class SqliteSettingsWorkflowStore {
|
|
|
57
76
|
requireInput(expected, operationId, now = this.now()) {
|
|
58
77
|
const card = this.cards.get(expected.state.cardId);
|
|
59
78
|
if (!card || card.ownerId !== this.ownerId || card.tokenHash !== expected.tokenHash || card.scope !== "settings" ||
|
|
60
|
-
card.state.kind !== "settings" || card.state.revision !== expected.state.revision ||
|
|
79
|
+
card.state.kind !== "settings" || card.state.input.backupDelivery !== undefined || card.state.input.operationStatus !== undefined || card.state.revision !== expected.state.revision ||
|
|
61
80
|
card.state.state !== "ready" || card.acceptedInput !== undefined || Date.parse(card.state.expiresAt) <= now.getTime()) {
|
|
62
81
|
throw new SettingsConflict("This Settings selection changed or its time limit ended.");
|
|
63
82
|
}
|
|
@@ -68,27 +87,41 @@ export class SqliteSettingsWorkflowStore {
|
|
|
68
87
|
}
|
|
69
88
|
return { card, operation };
|
|
70
89
|
}
|
|
71
|
-
write(expected, next, card, nextCard) {
|
|
90
|
+
write(expected, next, card, nextCard, display) {
|
|
72
91
|
this.transaction(() => {
|
|
73
92
|
if (!isDeepStrictEqual(this.get(expected.operationId), expected) || next.operationId !== expected.operationId ||
|
|
74
93
|
next.cardId !== expected.cardId || next.ownerId !== expected.ownerId)
|
|
75
94
|
throw new SettingsConflict("Settings state changed before it could be saved.");
|
|
95
|
+
if (next.fileId !== expected.fileId || next.draft?.kind !== "import_local_data" || next.phase === "terminal") {
|
|
96
|
+
this.invalidateUpload(expected);
|
|
97
|
+
}
|
|
76
98
|
this.db.prepare(`UPDATE settings_operations SET data_epoch=@epoch,kind=@kind,phase=@phase,draft_json=@draft,
|
|
77
99
|
preparation_id=@preparationId,fingerprint=@fingerprint,prepared_user_data_revision=@userRevision,preparation_json=@preparation,
|
|
78
100
|
file_id=@fileId,accepted_digest=@accepted,admitted_at=@admitted,result_json=@result,
|
|
79
|
-
prior_run_id=@prior,next_run_id=@next,supersedes_operation_id=@supersedes,superseded_by_operation_id=@supersededBy
|
|
101
|
+
prior_run_id=@prior,next_run_id=@next,supersedes_operation_id=@supersedes,superseded_by_operation_id=@supersededBy
|
|
80
102
|
WHERE operation_id=@id`).run({ id: next.operationId, epoch: next.dataEpoch, kind: next.draft?.kind ?? null,
|
|
81
103
|
phase: next.phase, draft: next.draft ? JSON.stringify(next.draft) : null, preparationId: next.preparationId ?? null,
|
|
82
104
|
fingerprint: next.fingerprint ?? null, userRevision: next.preparedUserDataRevision ?? null,
|
|
83
105
|
preparation: next.preparation ? JSON.stringify(next.preparation) : null, fileId: next.fileId ?? null,
|
|
84
106
|
accepted: next.acceptedDigest ?? null, admitted: next.admittedAt ?? null, result: next.result ? JSON.stringify(next.result) : null,
|
|
85
107
|
prior: next.priorRunId ?? null, next: next.nextRunId ?? null,
|
|
86
|
-
supersedes: next.supersedesOperationId ?? null, supersededBy: next.supersededByOperationId ?? null
|
|
87
|
-
|
|
108
|
+
supersedes: next.supersedesOperationId ?? null, supersededBy: next.supersededByOperationId ?? null });
|
|
109
|
+
let projected = nextCard ?? { ...card, state: { ...card.state, revision: card.state.revision + 1 } };
|
|
110
|
+
if (projected.state.state === "closed" && projected.state.data === undefined && next.result) {
|
|
111
|
+
projected = { ...projected, state: { ...projected.state, data: this.resultView(next, next.result, display) } };
|
|
112
|
+
}
|
|
113
|
+
if (card.state.state !== "closed" && !this.cards.replace(card, projected, this.now())) {
|
|
88
114
|
throw new SettingsConflict("The Settings card changed before it could be saved.");
|
|
89
115
|
}
|
|
90
116
|
});
|
|
91
117
|
}
|
|
118
|
+
invalidateUpload(operation) {
|
|
119
|
+
if (!operation.fileId)
|
|
120
|
+
return;
|
|
121
|
+
this.db.prepare(`UPDATE settings_files SET invalidated=1,cleanup='pending'
|
|
122
|
+
WHERE file_id=? AND operation_id=? AND role='upload' AND invalidated=0`)
|
|
123
|
+
.run(operation.fileId, operation.operationId);
|
|
124
|
+
}
|
|
92
125
|
hasReusableUpload(operationId) {
|
|
93
126
|
return !!this.db.prepare(`SELECT 1 FROM settings_operations WHERE operation_id=? AND (${SETTINGS_REUSABLE_UPLOAD_SQL})`).get(operationId);
|
|
94
127
|
}
|
|
@@ -160,16 +193,11 @@ export class SqliteSettingsWorkflowStore {
|
|
|
160
193
|
const file = snapshot(operation);
|
|
161
194
|
if (!isSettingsDownloadFile(file) || file.operationId !== operation.operationId)
|
|
162
195
|
throw new SettingsConflict("The download snapshot does not match this request.");
|
|
163
|
-
if (operation.fileId) {
|
|
164
|
-
const previous = this.file(operation.fileId);
|
|
165
|
-
if (previous?.role === "upload")
|
|
166
|
-
this.writeFile({ ...previous, invalidated: true, cleanup: "pending" });
|
|
167
|
-
}
|
|
168
196
|
this.createFile(file);
|
|
169
197
|
this.claimFinal(operation.operationId);
|
|
170
198
|
this.reserveFileIO(file.fileId, operation.operationId);
|
|
171
199
|
const admittedAt = this.now().toISOString();
|
|
172
|
-
const download = { currentAttemptId: input.attemptId, attempts: [{ attemptId: input.attemptId, admittedAt }] };
|
|
200
|
+
const download = { currentAttemptId: input.attemptId, attempts: [{ attemptId: input.attemptId, admittedAt, cardId: card.state.cardId }] };
|
|
173
201
|
const next = { operationId: operation.operationId, cardId: operation.cardId, ownerId: operation.ownerId,
|
|
174
202
|
dataEpoch: operation.dataEpoch, phase: "executing", draft: { kind: "download_backup" }, fileId: file.fileId,
|
|
175
203
|
preparation: { download }, acceptedDigest: settingsIdentity(input), admittedAt };
|
|
@@ -184,7 +212,9 @@ export class SqliteSettingsWorkflowStore {
|
|
|
184
212
|
throw new SettingsConflict("This download no longer belongs to the current Settings request.");
|
|
185
213
|
}
|
|
186
214
|
const operation = this.get(operationId), file = this.file(fileId);
|
|
187
|
-
if (!operation || operation.cardId !== card.state.cardId
|
|
215
|
+
if (!operation || operation.cardId !== card.state.cardId && !(card.state.input.backupDelivery &&
|
|
216
|
+
card.state.input.backupDelivery.operationId === operationId &&
|
|
217
|
+
card.state.input.backupDelivery.fileId === fileId) || operation.ownerId !== this.ownerId ||
|
|
188
218
|
operation.dataEpoch !== this.control().data_epoch || operation.fileId !== fileId || !file ||
|
|
189
219
|
file.operationId !== operationId || !isSettingsDownloadFile(file))
|
|
190
220
|
throw new SettingsConflict("This download no longer belongs to the current Settings request.");
|
|
@@ -194,14 +224,14 @@ export class SqliteSettingsWorkflowStore {
|
|
|
194
224
|
return { card, operation, file, download };
|
|
195
225
|
}
|
|
196
226
|
requireDownloadRead(expected, fileId, attemptId) {
|
|
197
|
-
const context = this.downloadContext(expected, expected.operationId, fileId), {
|
|
198
|
-
if (!attemptId || attemptId !== download.currentAttemptId || download.endReason || download.localSave || !download.expiresAt ||
|
|
227
|
+
const context = this.downloadContext(expected, expected.operationId, fileId), { download, file } = context;
|
|
228
|
+
if (context.card.state.state === "closed" || !attemptId || download.attempts.find((a) => a.attemptId === attemptId)?.cardId !== context.card.state.cardId || attemptId !== download.currentAttemptId || download.endReason || download.localSave || !download.expiresAt ||
|
|
199
229
|
this.now().getTime() >= Date.parse(download.expiresAt) || !file.sealed || file.invalidated ||
|
|
200
|
-
|
|
230
|
+
!download.readyAt)
|
|
201
231
|
throw new SettingsConflict("This backup's download period has ended or its attempt changed.");
|
|
202
232
|
return context;
|
|
203
233
|
}
|
|
204
|
-
writeDownloadState(expected, download, card) {
|
|
234
|
+
writeDownloadState(expected, download, card, nextCard) {
|
|
205
235
|
storedSettingsDownloadSchema.parse(download);
|
|
206
236
|
this.transaction(() => {
|
|
207
237
|
if (!isDeepStrictEqual(this.get(expected.operationId), expected) || expected.draft?.kind !== "download_backup") {
|
|
@@ -209,12 +239,13 @@ export class SqliteSettingsWorkflowStore {
|
|
|
209
239
|
}
|
|
210
240
|
this.db.prepare("UPDATE settings_operations SET preparation_json=? WHERE operation_id=?")
|
|
211
241
|
.run(JSON.stringify({ download }), expected.operationId);
|
|
212
|
-
|
|
242
|
+
const current = this.cards.get(card.state.cardId);
|
|
243
|
+
if (current && current.state.state !== "closed" && !this.cards.replace(current, nextCard ?? { ...current, state: { ...current.state, revision: current.state.revision + 1 } })) {
|
|
213
244
|
throw new SettingsConflict("The download card changed before its state could be saved.");
|
|
214
245
|
}
|
|
215
246
|
});
|
|
216
247
|
}
|
|
217
|
-
endDownload(id, reason) {
|
|
248
|
+
endDownload(id, reason, finalResult) {
|
|
218
249
|
const operation = this.get(id);
|
|
219
250
|
if (!operation)
|
|
220
251
|
return;
|
|
@@ -227,6 +258,7 @@ export class SqliteSettingsWorkflowStore {
|
|
|
227
258
|
if (file && isSettingsDownloadFile(file))
|
|
228
259
|
this.writeFile({ ...file, invalidated: true, cleanup: "pending" });
|
|
229
260
|
this.writeDownloadState(operation, { ...download, endReason: reason }, card);
|
|
261
|
+
this.finishDownloadCard(id, undefined, finalResult);
|
|
230
262
|
}
|
|
231
263
|
expireDownloads(operationId) {
|
|
232
264
|
const now = this.now().getTime();
|
|
@@ -259,38 +291,40 @@ export class SqliteSettingsWorkflowStore {
|
|
|
259
291
|
const save = download.localSave;
|
|
260
292
|
const duplicate = save?.attempts.find((a) => a.saveAttemptId === input.saveAttemptId);
|
|
261
293
|
if (duplicate) {
|
|
262
|
-
if (save.attemptId !== input.attemptId || duplicate.previousSaveAttemptId !== input.previousSaveAttemptId ||
|
|
294
|
+
if (duplicate.cardId !== card.state.cardId || save.attemptId !== input.attemptId || duplicate.previousSaveAttemptId !== input.previousSaveAttemptId ||
|
|
263
295
|
duplicate.allowPossibleCopy !== input.allowPossibleCopy)
|
|
264
296
|
throw new SettingsConflict("This save does not match the recorded request.");
|
|
265
297
|
return;
|
|
266
298
|
}
|
|
267
|
-
if (!save || save.attemptId !== input.attemptId || download.endReason || !download.expiresAt ||
|
|
299
|
+
if (card.state.state !== "ready" || !card.state.input.backupDelivery || !save || save.attemptId !== input.attemptId || download.endReason || !download.expiresAt ||
|
|
268
300
|
Date.parse(download.expiresAt) <= this.now().getTime() || file.invalidated || !file.sealed ||
|
|
269
301
|
(save.status === "needs_consent" ? input.previousSaveAttemptId !== undefined :
|
|
270
302
|
save.status !== "failed" || save.failure !== "not_published" || input.previousSaveAttemptId !== save.currentSaveAttemptId) ||
|
|
271
303
|
input.allowPossibleCopy !== this.previousDeliveryPossible(download))
|
|
272
304
|
throw new SettingsConflict("This backup cannot be saved by that request. Check its current status.");
|
|
273
|
-
const next = this.admitLocalSave(operation, file, save, input.saveAttemptId, input.allowPossibleCopy, input.previousSaveAttemptId);
|
|
274
|
-
this.writeDownloadState(operation, { ...download, localSave: next }, card);
|
|
305
|
+
const next = this.admitLocalSave(operation, file, save, input.saveAttemptId, input.allowPossibleCopy, card.state.cardId, input.previousSaveAttemptId);
|
|
306
|
+
this.writeDownloadState(operation, { ...download, localSave: next }, card, { ...card, acceptedInput: input, state: { ...card.state, state: "running", revision: card.state.revision + 1 } });
|
|
275
307
|
return file;
|
|
276
308
|
}
|
|
277
309
|
if (input.action === "settings_retry_download") {
|
|
278
310
|
const duplicate = download.attempts.find((attempt) => attempt.attemptId === input.attemptId);
|
|
279
311
|
if (duplicate) {
|
|
280
|
-
if (duplicate.previousAttemptId !== input.previousAttemptId)
|
|
312
|
+
if (duplicate.cardId !== card.state.cardId || duplicate.previousAttemptId !== input.previousAttemptId)
|
|
281
313
|
throw new SettingsConflict("This retry does not match the recorded download attempt.");
|
|
282
314
|
return;
|
|
283
315
|
}
|
|
284
|
-
if (download.currentAttemptId !== input.previousAttemptId || download.endReason || download.localSave || !download.expiresAt || !file.sealed ||
|
|
285
|
-
file.invalidated ||
|
|
316
|
+
if (card.state.state !== "ready" || !card.state.input.backupDelivery || download.currentAttemptId !== input.previousAttemptId || download.endReason || download.localSave || !download.expiresAt || !file.sealed ||
|
|
317
|
+
file.invalidated || !download.readyAt)
|
|
286
318
|
throw new SettingsConflict("This backup is no longer available for that retry.");
|
|
319
|
+
const previousCardId = download.attempts.find((a) => a.attemptId === download.currentAttemptId).cardId;
|
|
320
|
+
this.finishDownloadCard(operation.operationId, previousCardId);
|
|
287
321
|
const next = { ...download, currentAttemptId: input.attemptId,
|
|
288
|
-
attempts: [...download.attempts, { attemptId: input.attemptId, previousAttemptId: input.previousAttemptId, admittedAt: this.now().toISOString() }] };
|
|
289
|
-
this.writeDownloadState(operation, next, card);
|
|
322
|
+
attempts: [...download.attempts, { attemptId: input.attemptId, cardId: card.state.cardId, previousAttemptId: input.previousAttemptId, admittedAt: this.now().toISOString() }] };
|
|
323
|
+
this.writeDownloadState(this.get(operation.operationId), next, card, { ...card, acceptedInput: input, state: { ...card.state, state: "running", revision: card.state.revision + 1 } });
|
|
290
324
|
return;
|
|
291
325
|
}
|
|
292
326
|
const entry = download.attempts.find((attempt) => attempt.attemptId === input.attemptId);
|
|
293
|
-
if (!entry)
|
|
327
|
+
if (!entry || entry.cardId !== card.state.cardId)
|
|
294
328
|
throw new SettingsConflict("This download attempt was not admitted.");
|
|
295
329
|
if (input.action === "settings_report_download") {
|
|
296
330
|
if (!download.readyAt)
|
|
@@ -303,24 +337,26 @@ export class SqliteSettingsWorkflowStore {
|
|
|
303
337
|
const next = { ...download, attempts: download.attempts.map((attempt) => attempt === entry
|
|
304
338
|
? { ...attempt, hostReport: input.status, ...(input.failure ? { failure: input.failure } : {}) } : attempt) };
|
|
305
339
|
let admitted = false;
|
|
306
|
-
if (input.status === "unsupported" && input.attemptId === download.currentAttemptId && !download.endReason &&
|
|
340
|
+
if (input.status === "unsupported" && input.attemptId === download.currentAttemptId && card.state.state === "running" && !download.endReason &&
|
|
307
341
|
download.expiresAt && this.now().getTime() < Date.parse(download.expiresAt) && file.sealed && !file.invalidated) {
|
|
308
342
|
const save = { status: "needs_consent", attemptId: input.attemptId, fileId: file.fileId,
|
|
309
343
|
sha256: file.sha256, totalSize: file.totalSize, destination: settingsBackupPath(operation.operationId), ownerId: this.ownerId, attempts: [] };
|
|
310
344
|
if (this.previousDeliveryPossible(download))
|
|
311
345
|
next.localSave = save;
|
|
312
346
|
else {
|
|
313
|
-
next.localSave = this.admitLocalSave(operation, file, save, randomUUID(), false);
|
|
347
|
+
next.localSave = this.admitLocalSave(operation, file, save, randomUUID(), false, card.state.cardId);
|
|
314
348
|
admitted = true;
|
|
315
349
|
}
|
|
316
350
|
}
|
|
317
351
|
this.writeDownloadState(operation, next, card);
|
|
318
352
|
if (input.attemptId === download.currentAttemptId && input.status === "acknowledged" && !download.endReason)
|
|
319
353
|
this.endDownload(operation.operationId, "acknowledged");
|
|
354
|
+
else if (!admitted)
|
|
355
|
+
this.finishDownloadCard(operation.operationId, card.state.cardId);
|
|
320
356
|
return admitted ? file : undefined;
|
|
321
357
|
}
|
|
322
358
|
else {
|
|
323
|
-
if (input.attemptId !== download.currentAttemptId)
|
|
359
|
+
if (card.state.state === "closed" || input.attemptId !== download.currentAttemptId)
|
|
324
360
|
throw new SettingsConflict("A different download attempt is current.");
|
|
325
361
|
if (download.localSave?.status === "saving")
|
|
326
362
|
throw new SettingsConflict("Saving has already started. Check its result before continuing.");
|
|
@@ -329,19 +365,20 @@ export class SqliteSettingsWorkflowStore {
|
|
|
329
365
|
});
|
|
330
366
|
}
|
|
331
367
|
previousDeliveryPossible(download) {
|
|
332
|
-
return download.attempts.some((a) => a.attemptId !== download.currentAttemptId && a.hostReport !== "unsupported");
|
|
368
|
+
return download.attempts.some((a) => a.attemptId !== download.currentAttemptId && a.hostReport !== "unsupported" && a.hostReport !== "not_sent");
|
|
333
369
|
}
|
|
334
|
-
admitLocalSave(operation, file, saved, saveAttemptId, allowPossibleCopy, previousSaveAttemptId) {
|
|
370
|
+
admitLocalSave(operation, file, saved, saveAttemptId, allowPossibleCopy, cardId, previousSaveAttemptId) {
|
|
335
371
|
this.claimFinal(operation.operationId);
|
|
336
372
|
this.reserveFileIO(file.fileId, operation.operationId);
|
|
337
373
|
const { failure: _failure, sourceIdentity: _identity, savedAt: _savedAt, ...base } = saved;
|
|
338
374
|
return { ...base, status: "saving", currentSaveAttemptId: saveAttemptId, attempts: [...saved.attempts,
|
|
339
|
-
{ saveAttemptId, allowPossibleCopy, admittedAt: this.now().toISOString(), ...(previousSaveAttemptId ? { previousSaveAttemptId } : {}) }] };
|
|
375
|
+
{ saveAttemptId, cardId, allowPossibleCopy, admittedAt: this.now().toISOString(), ...(previousSaveAttemptId ? { previousSaveAttemptId } : {}) }] };
|
|
340
376
|
}
|
|
341
377
|
localSaveContext(id, saveAttemptId) {
|
|
342
378
|
const operation = this.get(id), download = operation && settingsDownload(operation), save = download?.localSave;
|
|
343
379
|
const file = operation?.fileId ? this.file(operation.fileId) : undefined;
|
|
344
|
-
const
|
|
380
|
+
const ownerCardId = save?.attempts.find((a) => a.saveAttemptId === save.currentSaveAttemptId)?.cardId;
|
|
381
|
+
const card = ownerCardId && this.cards.get(ownerCardId);
|
|
345
382
|
if (!operation || operation.ownerId !== this.ownerId || operation.dataEpoch !== this.control().data_epoch ||
|
|
346
383
|
!download || !save || !file || !card || save.status !== "saving" || save.currentSaveAttemptId !== saveAttemptId ||
|
|
347
384
|
save.fileId !== file.fileId || save.sha256 !== file.sha256 || save.totalSize !== file.totalSize ||
|
|
@@ -358,7 +395,7 @@ export class SqliteSettingsWorkflowStore {
|
|
|
358
395
|
this.writeDownloadState(operation, { ...download, localSave: { ...save, sourceIdentity } }, card);
|
|
359
396
|
});
|
|
360
397
|
}
|
|
361
|
-
settleLocalSave(id, saveAttemptId, outcome) {
|
|
398
|
+
settleLocalSave(id, saveAttemptId, outcome, backupLocation) {
|
|
362
399
|
this.transaction(() => {
|
|
363
400
|
const { operation, download, save, file, card } = this.localSaveContext(id, saveAttemptId);
|
|
364
401
|
const expired = !download.expiresAt || this.now().getTime() >= Date.parse(download.expiresAt);
|
|
@@ -370,6 +407,7 @@ export class SqliteSettingsWorkflowStore {
|
|
|
370
407
|
this.writeDownloadState(operation, { ...download, localSave, ...(ended ? { endReason: ended } : {}) }, card);
|
|
371
408
|
if (ended)
|
|
372
409
|
this.writeFile({ ...file, invalidated: true, cleanup: "pending" });
|
|
410
|
+
this.finishDownloadCard(id, card.state.cardId, undefined, backupLocation);
|
|
373
411
|
this.releaseFileIO(file.fileId);
|
|
374
412
|
this.db.prepare("UPDATE settings_control SET active_operation_id=NULL WHERE id=1 AND active_operation_id=?").run(id);
|
|
375
413
|
});
|
|
@@ -385,12 +423,68 @@ export class SqliteSettingsWorkflowStore {
|
|
|
385
423
|
this.writeDownloadState(operation, { ...download, readyAt,
|
|
386
424
|
expiresAt: new Date(Date.parse(readyAt) + retryWindowMs).toISOString() }, card);
|
|
387
425
|
}
|
|
426
|
+
releaseFinal(id) {
|
|
427
|
+
this.db.prepare("UPDATE settings_control SET active_operation_id=NULL WHERE id=1 AND active_operation_id=?").run(id);
|
|
428
|
+
}
|
|
429
|
+
resultView(operation, result, display) {
|
|
430
|
+
const file = operation.fileId ? this.file(operation.fileId) : undefined;
|
|
431
|
+
const { file: _file, ...extra } = display ?? {};
|
|
432
|
+
return { kind: "settings", operationId: operation.operationId, phase: "terminal", result,
|
|
433
|
+
...(operation.draft ? { draft: operation.draft } : {}), temporaryFileCleanup: this.temporaryFileCleanup(operation.operationId),
|
|
434
|
+
allowedActions: [], observe: false,
|
|
435
|
+
...(operation.priorRunId ? { priorRunId: operation.priorRunId } : {}), ...(operation.nextRunId ? { nextRunId: operation.nextRunId } : {}),
|
|
436
|
+
...(operation.supersedesOperationId ? { supersedesOperationId: operation.supersedesOperationId } : {}),
|
|
437
|
+
...(operation.supersededByOperationId ? { supersededByOperationId: operation.supersededByOperationId } : {}),
|
|
438
|
+
...(file ? { file: { fileId: file.fileId, displayName: file.displayName, totalSize: file.totalSize, receivedSize: file.receivedSize,
|
|
439
|
+
sha256: file.sha256, sealed: file.sealed, cleanup: file.cleanup } } : {}), ...extra };
|
|
440
|
+
}
|
|
441
|
+
downloadResult(operation) {
|
|
442
|
+
const download = settingsDownload(operation);
|
|
443
|
+
if (!download)
|
|
444
|
+
throw new SettingsConflict("This Settings request does not provide a download.");
|
|
445
|
+
const local = download.localSave, report = download.attempts.find((a) => a.attemptId === download.currentAttemptId)?.hostReport;
|
|
446
|
+
const succeeded = download.endReason === "stored_locally" || report === "acknowledged";
|
|
447
|
+
return { completionScope: "backup_delivery", outcome: succeeded ? "completed" : download.endReason === "cancelled" || download.endReason === "server_restarted" ? "interrupted" : "failed", recordedAt: this.now().toISOString(),
|
|
448
|
+
message: download.endReason === "stored_locally" ? "Backup saved." : report === "acknowledged" ? "The app accepted the download request. This does not confirm that the file was saved or where it was saved."
|
|
449
|
+
: download.endReason === "cancelled" ? "Backup delivery was cancelled."
|
|
450
|
+
: download.endReason ? "This backup is no longer available for delivery. Ask in chat for a new Settings card to create a backup."
|
|
451
|
+
: report === "not_sent" ? "The backup could not be read or checked. Nothing was sent to the app for download." : local?.status === "needs_consent" ? "A local copy needs a new explicit request because another copy may exist."
|
|
452
|
+
: local?.status === "failed" && local.failure === "not_published" ? "The backup was not saved. Ask in chat for a new delivery card to retry saving the same backup."
|
|
453
|
+
: "Backup delivery was not confirmed. Ask in chat for a new delivery card." };
|
|
454
|
+
}
|
|
455
|
+
finishDownloadCard(id, cardId, finalResult, backupLocation) {
|
|
456
|
+
const operation = this.get(id), download = operation && settingsDownload(operation);
|
|
457
|
+
if (!operation || !download)
|
|
458
|
+
return;
|
|
459
|
+
const local = download.localSave;
|
|
460
|
+
const target = cardId ?? local?.attempts.find((a) => a.saveAttemptId === local.currentSaveAttemptId)?.cardId ?? download.attempts.find((a) => a.attemptId === download.currentAttemptId)?.cardId;
|
|
461
|
+
const card = target && this.cards.get(target);
|
|
462
|
+
if (!card)
|
|
463
|
+
return;
|
|
464
|
+
const report = download.attempts.find((a) => a.attemptId === download.currentAttemptId)?.hostReport;
|
|
465
|
+
const succeeded = download.endReason === "stored_locally" || report === "acknowledged";
|
|
466
|
+
const result = finalResult ?? this.downloadResult(operation);
|
|
467
|
+
const file = operation.fileId ? this.file(operation.fileId) : undefined;
|
|
468
|
+
const display = { ...(file ? { file: { fileId: file.fileId, displayName: file.displayName, totalSize: file.totalSize, receivedSize: file.receivedSize,
|
|
469
|
+
sha256: file.sha256, sealed: file.sealed, cleanup: file.cleanup } } : {}),
|
|
470
|
+
download: { availability: download.endReason ? "ended" : download.readyAt ? "available" : "preparing", currentAttemptId: download.currentAttemptId,
|
|
471
|
+
...(report ? { hostReport: report } : {}), previousUnconfirmed: this.previousDeliveryPossible(download),
|
|
472
|
+
...(download.endReason ? { endReason: download.endReason } : {}), ...(download.expiresAt ? { expiresAt: download.expiresAt, remainingMs: Math.max(0, Date.parse(download.expiresAt) - this.now().getTime()) } : {}),
|
|
473
|
+
...(local ? { localSave: { status: local.status, currentSaveAttemptId: local.currentSaveAttemptId, failure: local.failure, savedAt: local.savedAt } } : {}), allowedFileActions: [] },
|
|
474
|
+
...(backupLocation ? { backupLocation } : {}) };
|
|
475
|
+
const reason = download.endReason === "cancelled" ? "cancelled" : download.endReason === "expired" ? "expired" : download.endReason === "server_restarted" ? "server_restarted" : succeeded ? "completed" : "failed";
|
|
476
|
+
if (!operation.result && operation.cardId === card.state.cardId)
|
|
477
|
+
this.finish(id, result, display, reason);
|
|
478
|
+
else if (card.state.state !== "closed" && !this.cards.replace(card, { ...card, state: { ...card.state, state: "closed", reason,
|
|
479
|
+
revision: card.state.revision + 1, data: this.resultView(operation, result, display) } }, this.now()))
|
|
480
|
+
throw new SettingsConflict("The delivery result could not be saved.");
|
|
481
|
+
}
|
|
388
482
|
filesForCleanup(operationId) {
|
|
389
483
|
const rows = this.db.prepare(`SELECT file_id FROM settings_files WHERE cleanup!='complete' AND ${SETTINGS_FILE_CLEANUP_SQL}
|
|
390
484
|
${operationId === undefined ? "" : "AND cleanup_operation_id=?"}`).all(...(operationId === undefined ? [] : [operationId]));
|
|
391
485
|
return rows.map((row) => this.file(row.file_id));
|
|
392
486
|
}
|
|
393
|
-
finish(id, result) {
|
|
487
|
+
finish(id, result, display, reason) {
|
|
394
488
|
return this.transaction(() => {
|
|
395
489
|
const old = this.get(id);
|
|
396
490
|
if (!old || old.ownerId !== this.ownerId)
|
|
@@ -407,7 +501,7 @@ export class SqliteSettingsWorkflowStore {
|
|
|
407
501
|
return false;
|
|
408
502
|
const next = { ...old, phase: "terminal", result };
|
|
409
503
|
this.write(old, next, card, { ...card, state: { ...card.state, revision: card.state.revision + 1,
|
|
410
|
-
state: "closed", reason: card.state.reason ?? (result.outcome === "completed" ? "completed" : result.outcome === "superseded" || !old.admittedAt ? "cancelled" : "failed") } });
|
|
504
|
+
state: "closed", data: undefined, reason: card.state.reason ?? reason ?? (result.outcome === "completed" ? "completed" : result.outcome === "superseded" || !old.admittedAt ? "cancelled" : "failed") } }, display);
|
|
411
505
|
this.db.prepare(`UPDATE settings_control SET active_operation_id=NULL WHERE id=1 AND active_operation_id=?`).run(id);
|
|
412
506
|
this.db.prepare(`UPDATE settings_control SET wallet_input_blocked=0,wallet_block_operation_id=NULL
|
|
413
507
|
WHERE id=1 AND wallet_block_operation_id=?`).run(id);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
|
|
2
|
+
import { transactionRequestSchema } from "./transactionRequest.js";
|
|
3
|
+
export function readStoredTransactionRequest(db, id) {
|
|
4
|
+
const row = db.prepare(`SELECT r.*, a.address AS account, e.result_json FROM review_requests r
|
|
5
|
+
JOIN accounts a ON a.id=r.account_id AND a.chain_id='sui:mainnet' LEFT JOIN review_executions e ON e.attempt_id=r.attempt_id
|
|
6
|
+
WHERE r.attempt_id=?`).get(id);
|
|
7
|
+
if (!row)
|
|
8
|
+
return undefined;
|
|
9
|
+
const result = transactionRequestSchema.parse({ attemptId: row.attempt_id, reviewSessionId: row.review_session_id,
|
|
10
|
+
planId: row.plan_id, reviewRevision: row.review_revision, account: row.account, transactionDigest: row.transaction_digest,
|
|
11
|
+
requestStatus: row.request_status, revision: row.revision, createdAt: row.created_at, updatedAt: row.updated_at,
|
|
12
|
+
...(row.reason === null ? {} : { reason: row.reason }),
|
|
13
|
+
...(row.signature_verified_at === null ? {} : { signatureVerifiedAt: row.signature_verified_at }),
|
|
14
|
+
...(row.submitted_at === null ? {} : { submittedAt: row.submitted_at }),
|
|
15
|
+
...(row.result_json === null ? {} : { execution: JSON.parse(row.result_json) }) });
|
|
16
|
+
assertNoForbiddenMcpFields(result);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { walletConnectionActionSchema, walletConnectionOperationSchema } from "./walletConnectionOperations.js";
|
|
2
|
+
export function readStoredWalletConnectionOperation(db, id) {
|
|
3
|
+
const row = db.prepare("SELECT * FROM wallet_connection_operations WHERE operation_id=?").get(id);
|
|
4
|
+
if (!row)
|
|
5
|
+
return undefined;
|
|
6
|
+
const input = walletConnectionActionSchema.parse(JSON.parse(row.input_json));
|
|
7
|
+
const result = row.result_json ? JSON.parse(row.result_json) : {};
|
|
8
|
+
return { ...walletConnectionOperationSchema.parse({ operationId: row.operation_id, cardId: row.card_id, action: row.action,
|
|
9
|
+
status: row.status, settlementPending: row.sdk_pending === 1, createdAt: row.created_at, updatedAt: row.updated_at, expiresAt: row.expires_at,
|
|
10
|
+
...(row.connection_id ? { connectionId: row.connection_id } : {}), ...({}),
|
|
11
|
+
...result }),
|
|
12
|
+
ownerId: row.owner_id, runId: row.run_id, dataEpoch: row.data_epoch, input };
|
|
13
|
+
}
|