@zktx.io/sui-mcp 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -18
- package/dist/adapters/adapterPromptSurfaces.js +0 -20
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +0 -4
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +0 -9
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +0 -9
- package/dist/adapters/intentPlanFactories.js +0 -6
- package/dist/core/action/contractNameRegistry.js +0 -53
- package/dist/core/action/forbiddenFields.js +0 -4
- package/dist/core/action/ptbVisualizationProducer.js +0 -3
- package/dist/core/action/receiptPtbGraph.js +0 -17
- package/dist/core/action/reviewTimeSimulationEvidence.js +0 -4
- package/dist/core/action/signableAdapterContract.js +0 -5
- package/dist/core/action/suiChainReceiptMapping.js +0 -18
- package/dist/core/action/suiChainReceiptReader.js +1 -21
- package/dist/core/action/suiChainReceiptVerifier.js +0 -10
- package/dist/core/action/transactionFunding.js +0 -7
- package/dist/core/action/types.js +0 -1
- package/dist/core/action/walletReviewContractAssembler.js +0 -2
- package/dist/core/activity/localDataService.js +0 -6
- package/dist/core/activity/sqliteActivityStore.js +0 -8
- package/dist/core/activity/sqliteActivityStoreRows.js +0 -2
- package/dist/core/activity/sqliteActivityStoreSchema.js +0 -4
- package/dist/core/read/deepbookReadHelpers.js +0 -2
- package/dist/core/read/deepbookRegistry.js +0 -19
- package/dist/core/read/deepbookUsdcChartService.js +0 -2
- package/dist/core/read/readCardResult.js +0 -2
- package/dist/core/read/readService.js +0 -19
- package/dist/core/read/readServiceTypes.js +0 -2
- package/dist/core/session/cardSession.js +0 -4
- package/dist/core/session/cardSessionStore.js +1 -4
- package/dist/core/session/reviewValidity.js +0 -3
- package/dist/core/session/sessionRecordStore.js +0 -1
- package/dist/core/session/sessionStore.js +0 -15
- package/dist/core/session/settingsFileIdentity.js +0 -2
- package/dist/core/session/settingsFileProtocol.js +0 -5
- package/dist/core/session/settingsFileStore.js +1 -10
- package/dist/core/session/settingsInputError.js +0 -2
- package/dist/core/session/settingsOperation.js +0 -4
- package/dist/core/session/settingsPreparationSql.js +0 -2
- package/dist/core/session/settingsWorkflow.js +1 -37
- package/dist/core/session/sqliteCardStore.js +0 -2
- package/dist/core/session/sqliteSessionStore.js +0 -11
- package/dist/core/session/sqliteSettingsWorkflowStore.js +0 -10
- package/dist/core/session/sqliteTransactionMaterialStore.js +0 -11
- package/dist/core/session/sqliteWalletWorkflowStore.js +0 -29
- package/dist/core/session/status.js +0 -2
- package/dist/core/session/transactionMaterialStore.js +0 -4
- package/dist/core/session/transactionRequest.js +0 -4
- package/dist/core/session/walletConnection.js +0 -3
- package/dist/core/session/walletWorkflow.js +3 -21
- package/dist/core/session/workflowState.js +0 -8
- package/dist/core/session/workflowView.js +0 -6
- package/dist/core/suiAddress.js +0 -5
- package/dist/core/suiEndpoint.js +0 -3
- package/dist/mcp/prompts.js +0 -12
- package/dist/mcp/schemas.js +0 -3
- package/dist/mcp/tools/action/prepareSuiActionReview.js +0 -6
- package/dist/mcp/tools/read/serverStatusTools.js +0 -1
- package/dist/mcp/tools/session/shared.js +0 -2
- package/dist/mcp/tools/session/statusTools.js +0 -2
- package/dist/mcp-app/account.html +1 -1
- package/dist/mcp-app/chart.html +6 -6
- package/dist/mcp-app/connect.html +6 -6
- package/dist/mcp-app/receipt.html +298 -298
- package/dist/mcp-app/review.html +237 -237
- package/dist/mcp-app/settings.html +1 -1
- package/dist/mcp-ui/tools.js +3 -4
- package/dist/review-server/server.js +0 -2
- package/dist/runtime/application.js +1 -1
- package/dist/runtime/config.js +0 -2
- package/dist/runtime/localSettingsService.js +0 -3
- package/dist/runtime/logger.js +0 -2
- package/dist/runtime/reviewServerAcquire.js +1 -18
- package/dist/runtime/runtimeTermination.js +1 -4
- package/dist/runtime/shared/authenticatedFetch.js +0 -6
- package/dist/runtime/shared/control.js +0 -2
- package/dist/runtime/shared/dataAccess.js +0 -2
- package/dist/runtime/shared/mcpHttp.js +0 -5
- package/dist/runtime/shared/ownerLease.js +0 -4
- package/dist/runtime/shared/server.js +0 -1
- package/dist/runtime/shared/stdio.js +2 -5
- package/dist/runtime/smokeMainnetRead.js +0 -2
- package/dist/runtime/smokeReview.js +0 -4
- package/dist/runtime/start.js +0 -2
- package/dist/runtime/suiTransactionGraphqlMapping.js +0 -2
- package/dist/runtime/suiTransactionGraphqlQueries.js +0 -3
- package/dist/runtime/walletConnectConfig.js +0 -2
- package/dist/runtime/walletConnectStorage.js +0 -6
- package/dist/runtime/walletConnectTransport.js +0 -11
- package/dist/runtime/walletSdkChildRuntime.js +0 -5
- package/dist/runtime/walletSdkProcess.js +0 -6
- package/docs/AGENT_BEHAVIOR.md +1 -5
- package/docs/LOCAL_DB_ARCHITECTURE.md +3 -3
- package/docs/MCP_SETUP.md +14 -14
- package/docs/MCP_TOOLS.md +15 -0
- package/docs/SDK_API.md +12 -0
- package/docs/WALLET_CONNECTION.md +2 -1
- package/package.json +10 -2
- package/docs/AGENT_DEVELOPMENT_POLICY.md +0 -819
- package/docs/FRONTEND_POLICY.md +0 -611
- package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +0 -166
- package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +0 -161
- package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +0 -259
|
@@ -353,8 +353,6 @@ export function configureDatabase(db) {
|
|
|
353
353
|
db.exec("PRAGMA foreign_keys=ON");
|
|
354
354
|
db.exec("PRAGMA busy_timeout=5000");
|
|
355
355
|
}
|
|
356
|
-
// SQLite normalizes its own DDL. Compare only schema metadata, never user rows.
|
|
357
|
-
// The reference is built once from the single current schema, not from the DB being checked.
|
|
358
356
|
let currentSchemaSignature;
|
|
359
357
|
function schemaSignature(db) {
|
|
360
358
|
return JSON.stringify(db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name").all());
|
|
@@ -414,8 +412,6 @@ function externalActivityScansTableSql() {
|
|
|
414
412
|
)
|
|
415
413
|
)`;
|
|
416
414
|
}
|
|
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
415
|
function settingsRevisionTriggers() {
|
|
420
416
|
const userTables = ["accounts", "active_account_context", "review_sessions", "review_state_snapshots",
|
|
421
417
|
"review_status_transitions", "review_executions", "review_requests", "external_activity_scans",
|
|
@@ -192,8 +192,6 @@ export function normalizeManagerAddresses(values) {
|
|
|
192
192
|
return normalized;
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
|
-
// Account inventory v1 exposes only ledger and rebate balances; governance,
|
|
196
|
-
// stake, volume, and AccountInfo.open_orders need separate review before export.
|
|
197
195
|
export function toDeepbookAccountSummary(accountInfo) {
|
|
198
196
|
return {
|
|
199
197
|
epoch: accountInfo.epoch,
|
|
@@ -2,24 +2,7 @@ import { mainnetCoins, mainnetPools } from "@mysten/deepbook-v3";
|
|
|
2
2
|
import { DEEPBOOK_SCALAR_UNIT_SOURCE, decimalsFromScalar, normalizeCoinType } from "./coinMetadata.js";
|
|
3
3
|
import { ReadServiceInputError } from "./readServiceTypes.js";
|
|
4
4
|
export const PINNED_DEEPBOOK_COINS = mainnetCoins;
|
|
5
|
-
// Curated DeepBook USDC chart pairs: the meaningful Sui-ecosystem base tokens worth
|
|
6
|
-
// featuring against USDC, in display order. This is a deliberate editorial allowlist
|
|
7
|
-
// — NOT a dynamic volume cut — so the chart shows a stable, hand-picked set and never
|
|
8
|
-
// surfaces the dead/dust pools the indexer also lists (stable-vs-stable, wrapped
|
|
9
|
-
// duplicates, single-trade listings). Each entry is a token judged worth registering
|
|
10
|
-
// on its own merits:
|
|
11
|
-
// SUI — Sui L1 gas / native token
|
|
12
|
-
// DEEP — DeepBook protocol token (this venue's own token)
|
|
13
|
-
// WAL — Walrus decentralized storage (Mysten)
|
|
14
|
-
// NS — SuiNS name service
|
|
15
|
-
// IKA — Ika MPC / dWallet network
|
|
16
|
-
// XBTC — Bitcoin on Sui
|
|
17
|
-
// USDT — Tether USD stablecoin
|
|
18
|
-
// Edit this list to change which pairs the chart offers; order here is display order.
|
|
19
5
|
const CURATED_USDC_CHART_BASE_ORDER = new Map(["SUI", "DEEP", "WAL", "NS", "IKA", "XBTC", "USDT"].map((symbol, index) => [symbol, index]));
|
|
20
|
-
// Filter the chart pool list to the curated base tokens (by symbol) in curated
|
|
21
|
-
// display order. Matching is by base SYMBOL because the indexer's pool name can
|
|
22
|
-
// differ from the SDK pool key (e.g. BWETH_USDC vs BETH_USDC).
|
|
23
6
|
export function curateUsdcChartPools(pools) {
|
|
24
7
|
return pools
|
|
25
8
|
.filter((pool) => CURATED_USDC_CHART_BASE_ORDER.has(pool.baseAsset.symbol))
|
|
@@ -52,8 +35,6 @@ export function deepbookUnitForCoinType(coinType, coins) {
|
|
|
52
35
|
return { decimals, symbol: entry.symbol, name: entry.symbol };
|
|
53
36
|
}
|
|
54
37
|
export function findDeepbookCoinEntry(normalizedCoinType, coins) {
|
|
55
|
-
// Validate the whole pinned token unit registry before using any DeepBook
|
|
56
|
-
// coin match so one bad scalar cannot make the registry look partially trustworthy.
|
|
57
38
|
assertDeepbookScalars(coins);
|
|
58
39
|
for (const [symbol, coin] of Object.entries(coins)) {
|
|
59
40
|
if (normalizeCoinType(coin.type) === normalizedCoinType) {
|
|
@@ -35,8 +35,6 @@ export function createDeepbookUsdcChartService(options = {}) {
|
|
|
35
35
|
...chartCommonFields()
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
// Curate to the hand-picked allowlist of meaningful pairs (SUI/USDC, DEEP/USDC,
|
|
39
|
-
// …), in display order; the indexer's dead/dust pools never appear.
|
|
40
38
|
const pools = curateUsdcChartPools(selectDeepbookOfficialIndexerCanonicalUsdcPools(poolResult.pools).map(deepbookUsdcPriceHistoryPairFromOfficialPool));
|
|
41
39
|
return {
|
|
42
40
|
status: "ok",
|
|
@@ -12,8 +12,6 @@ const foundReceipt = z.object({ status: z.literal("found"), receipt: z.object({
|
|
|
12
12
|
inputs: z.array(receiptInput).refine((items) => new Set(items.map((item) => item.index)).size === items.length),
|
|
13
13
|
ptbGraph: receiptDisplaySchema.shape.ptbGraph
|
|
14
14
|
}).passthrough() }).strict();
|
|
15
|
-
// Partition the same source result before persistence. Never weaken the public
|
|
16
|
-
// forbidden-field check or turn arbitrary fields into private metadata.
|
|
17
15
|
export function projectReadCardResult(kind, input, source) {
|
|
18
16
|
if (kind !== "receipt" || !source || typeof source !== "object" || !("status" in source) || source.status !== "found") {
|
|
19
17
|
assertNoForbiddenMcpFields(source);
|
|
@@ -14,9 +14,6 @@ import { DEEPBOOK_MID_PRICE_DIRECTION, DEEPBOOK_MID_PRICE_PRECISION, DEEPBOOK_MI
|
|
|
14
14
|
export * from "./readServiceTypes.js";
|
|
15
15
|
export { listDeepbookTokenRegistry } from "./deepbookRegistry.js";
|
|
16
16
|
const DEFAULT_DEEPBOOK_USDC_PRICE_AT_TIME_MAX_DISTANCE_MINUTES = 360;
|
|
17
|
-
// Account inventory object scan: pages of listOwnedObjects to walk (50 objects per
|
|
18
|
-
// page) and the max NFTs to surface. A coin-heavy account may truncate before all
|
|
19
|
-
// objects are seen; the snapshot flags that rather than paging unbounded.
|
|
20
17
|
const ACCOUNT_OBJECT_SCAN_PAGES = 6;
|
|
21
18
|
const ACCOUNT_NFT_LIMIT = 60;
|
|
22
19
|
const DEEPBOOK_USDC_PRICE_HISTORY_RESPONSE_SUMMARY = {
|
|
@@ -332,10 +329,6 @@ export class SuiReadService {
|
|
|
332
329
|
cursor: summary.cursor
|
|
333
330
|
};
|
|
334
331
|
}
|
|
335
|
-
// The public account snapshot for the account page: the SuiNS name, coin
|
|
336
|
-
// balances (with the held-as split), owned NFTs, and other owned objects grouped
|
|
337
|
-
// by type. Each part is read independently so a missing name or object scan never
|
|
338
|
-
// blocks the balances.
|
|
339
332
|
async summarizeAccountInventory(input) {
|
|
340
333
|
const [walletSummary, name, owned] = await Promise.all([
|
|
341
334
|
this.summarizeWalletAssets(input),
|
|
@@ -347,8 +340,6 @@ export class SuiReadService {
|
|
|
347
340
|
account: input.account,
|
|
348
341
|
fetchedAt: this.#fetchedAt(),
|
|
349
342
|
name,
|
|
350
|
-
// Drop coins whose total balance is zero — an emptied coin object or
|
|
351
|
-
// accumulator residue is not a holding worth listing on the account page.
|
|
352
343
|
balances: walletSummary.balances.filter((balance) => {
|
|
353
344
|
try {
|
|
354
345
|
return BigInt(balance.balance) !== 0n;
|
|
@@ -363,8 +354,6 @@ export class SuiReadService {
|
|
|
363
354
|
objectsTruncated: owned.truncated
|
|
364
355
|
};
|
|
365
356
|
}
|
|
366
|
-
// The account's primary SuiNS name, or null when it has none (the lookup throws
|
|
367
|
-
// NOT_FOUND) or the call fails — a name is convenience, never gating.
|
|
368
357
|
async #resolveAccountName(account) {
|
|
369
358
|
const resolve = this.#client.core.defaultNameServiceName;
|
|
370
359
|
if (!resolve) {
|
|
@@ -378,10 +367,6 @@ export class SuiReadService {
|
|
|
378
367
|
return null;
|
|
379
368
|
}
|
|
380
369
|
}
|
|
381
|
-
// Enumerate owned objects (capped) and split them: Coin objects belong to the
|
|
382
|
-
// balance view, objects carrying a Display name/image are NFTs, and the rest are
|
|
383
|
-
// grouped by Move type. listOwnedObjects returns all three intermixed, so a
|
|
384
|
-
// coin-heavy account may truncate before reaching every NFT.
|
|
385
370
|
async #scanOwnedObjects(account) {
|
|
386
371
|
const nfts = [];
|
|
387
372
|
const groups = new Map();
|
|
@@ -1493,10 +1478,6 @@ export class SuiReadService {
|
|
|
1493
1478
|
#fetchedAt() {
|
|
1494
1479
|
return this.#now().toISOString();
|
|
1495
1480
|
}
|
|
1496
|
-
// Resolve one coin type's verified unit (decimals/symbol) through the shared
|
|
1497
|
-
// coin-metadata cache and DeepBook fallback. Public so other readers (e.g. the
|
|
1498
|
-
// public chain-receipt reader) can render decimal amounts without duplicating
|
|
1499
|
-
// the cache logic. Fails soft to an unavailable unit on an unresolvable type.
|
|
1500
1481
|
async resolveCoinUnit(coinType) {
|
|
1501
1482
|
let normalizedCoinType;
|
|
1502
1483
|
try {
|
|
@@ -42,8 +42,6 @@ export const DEEPBOOK_USDC_PRICE_HISTORY_SOURCE_UNAVAILABLE_REASONS = [
|
|
|
42
42
|
"candle_fetch_failed",
|
|
43
43
|
"official_indexer_invalid_payload"
|
|
44
44
|
];
|
|
45
|
-
// Internal placeholder for sender-independent DeepBook market reads. The pinned Sui gRPC
|
|
46
|
-
// resolver uses the same dummy sender when no transaction sender is provided.
|
|
47
45
|
export const DEFAULT_DEEPBOOK_SIMULATION_SENDER = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
48
46
|
export const NOT_INSPECTED_ASSET_CLASSES = [
|
|
49
47
|
{
|
|
@@ -17,11 +17,7 @@ export const cardSnapshotSchema = cardStateSchema.extend({
|
|
|
17
17
|
pollAfterMs: z.number().int().positive(),
|
|
18
18
|
inputRemainingMs: z.number().int().nonnegative()
|
|
19
19
|
});
|
|
20
|
-
// Private display values from a receipt already read by the backend. These are
|
|
21
|
-
// individual on-chain inputs, never a serialized transaction or a signature.
|
|
22
20
|
export const receiptDisplaySchema = z.object({
|
|
23
|
-
// Sui syntax is validated by the backend producer/projection. The View checks
|
|
24
|
-
// equality with its selected digest and does not load an SDK to recompute it.
|
|
25
21
|
transactionDigest: z.string().min(1),
|
|
26
22
|
pureInputs: z.array(z.object({
|
|
27
23
|
index: z.number().int().nonnegative(),
|
|
@@ -47,8 +47,6 @@ export class CardStore {
|
|
|
47
47
|
catch {
|
|
48
48
|
preparation = { status: "failed", error: "The choices for this card could not be loaded." };
|
|
49
49
|
}
|
|
50
|
-
// Preparation may finish after reset/import or shutdown. Never create a
|
|
51
|
-
// card in replacement data from that stale continuation.
|
|
52
50
|
this.assertCurrent();
|
|
53
51
|
}
|
|
54
52
|
assertNoForbiddenMcpFields({ input, preparation });
|
|
@@ -199,7 +197,6 @@ export class CardStore {
|
|
|
199
197
|
}
|
|
200
198
|
const complete = { ...admitted, ...("receiptDisplay" in projected ? { receiptDisplay: projected.receiptDisplay } : {}),
|
|
201
199
|
state: { ...admitted.state, data: projected.data, state: "closed", reason: "completed", revision: admitted.state.revision + 1 } };
|
|
202
|
-
// Do not report success or replay the source call if this commit fails.
|
|
203
200
|
try {
|
|
204
201
|
this.options.records.replace(admitted, complete);
|
|
205
202
|
}
|
|
@@ -220,7 +217,7 @@ export class CardStore {
|
|
|
220
217
|
try {
|
|
221
218
|
this.options.logger?.error("Card read failed", { cardId: record.state.cardId, kind: record.state.kind, stage });
|
|
222
219
|
}
|
|
223
|
-
catch {
|
|
220
|
+
catch { }
|
|
224
221
|
}
|
|
225
222
|
conflict(record, message) {
|
|
226
223
|
return { ...this.response(record), error: { code: "card_conflict", message } };
|
|
@@ -11,8 +11,6 @@ export function invalidateReviewEvidence(session, now) {
|
|
|
11
11
|
const state = { ...session.reviewState, evidenceValidity: "invalidated", updatedAt: now.toISOString() };
|
|
12
12
|
for (const field of REVIEW_MATERIAL_DERIVED_FIELDS)
|
|
13
13
|
delete state[field];
|
|
14
|
-
// Keep the computation's failure reason, checks and stage provenance. Their
|
|
15
|
-
// presence describes that failed attempt, not currently usable material.
|
|
16
14
|
return { ...session, lastActivityAt: now.toISOString(), reviewState: state };
|
|
17
15
|
}
|
|
18
16
|
return { ...session, status: "refresh_required", lastActivityAt: now.toISOString(),
|
|
@@ -33,7 +31,6 @@ export function materialStillMatches(verified, session, rowRevision, artifacts,
|
|
|
33
31
|
!!material && sameHandle(material, verified.transactionMaterial) && Date.parse(material.expiresAt) > now.getTime() &&
|
|
34
32
|
Buffer.from(material.transactionBytes).toString("base64") === verified.transactionBytesBase64;
|
|
35
33
|
}
|
|
36
|
-
// The caller owns a synchronous transaction and supplies its one decision time.
|
|
37
34
|
export function decideReviewEvaluation(candidate, current, rowRevision, artifacts, material, admitted, now) {
|
|
38
35
|
if (current.status !== "expired" && Date.parse(current.expiresAt) <= now.getTime()) {
|
|
39
36
|
return { ...current, status: "expired" };
|
|
@@ -104,8 +104,6 @@ export class LocalSessionStore {
|
|
|
104
104
|
});
|
|
105
105
|
return { session: cloneLocalSession(session), token };
|
|
106
106
|
}
|
|
107
|
-
// Snapshot access has no expiry, artifact reconciliation or external work.
|
|
108
|
-
// Public consumers project this private record before exposing any fields.
|
|
109
107
|
readReviewSession(id) {
|
|
110
108
|
const session = this.sessions.get(id);
|
|
111
109
|
return session ? cloneLocalSession(session) : undefined;
|
|
@@ -130,8 +128,6 @@ export class LocalSessionStore {
|
|
|
130
128
|
transactionBytesBase64: Buffer.from(checked.transactionBytes).toString("base64") };
|
|
131
129
|
}
|
|
132
130
|
catch (error) {
|
|
133
|
-
// Invalid evidence is a candidate for refresh, never permission to mutate
|
|
134
|
-
// a potentially newer revision. Storage failures must still propagate.
|
|
135
131
|
if (!(error instanceof LocalTransactionMaterialStoreError) && !(error instanceof ReviewEvidenceMismatch) && !(error instanceof ZodError))
|
|
136
132
|
throw error;
|
|
137
133
|
}
|
|
@@ -150,8 +146,6 @@ export class LocalSessionStore {
|
|
|
150
146
|
this.recordEvaluationEvents(result.events);
|
|
151
147
|
return cloneLocalSession(result.session);
|
|
152
148
|
}
|
|
153
|
-
// Preparation-only in-memory fixtures share the decision function. They do
|
|
154
|
-
// not establish SQLite atomicity or wallet admission guarantees.
|
|
155
149
|
const current = this.sessions.get(id);
|
|
156
150
|
if (!current)
|
|
157
151
|
return undefined;
|
|
@@ -186,8 +180,6 @@ export class LocalSessionStore {
|
|
|
186
180
|
if (!session) {
|
|
187
181
|
return false;
|
|
188
182
|
}
|
|
189
|
-
// Review sessions expose expired/final lifecycle states through read APIs after token validation.
|
|
190
|
-
// Mutable methods own expiry transitions and return lifecycle-specific errors.
|
|
191
183
|
return tokenMatchesHash(session.tokenHash, token);
|
|
192
184
|
}
|
|
193
185
|
async recordWalletConnected(id, account, now = new Date()) {
|
|
@@ -208,9 +200,6 @@ export class LocalSessionStore {
|
|
|
208
200
|
throw new SessionStoreError("invalid_session_transition", `Review session already bound to a different account: ${id}`);
|
|
209
201
|
}
|
|
210
202
|
if (nextSession.status === "proposed") {
|
|
211
|
-
// Explicit backend account binding follows the preparation lifecycle.
|
|
212
|
-
// Active read context alone is not wallet approval; the workflow checks
|
|
213
|
-
// the live wallet connection before invoking this operation.
|
|
214
203
|
transition(nextSession, "awaiting_wallet");
|
|
215
204
|
transition(nextSession, "wallet_connected");
|
|
216
205
|
}
|
|
@@ -356,7 +345,6 @@ export class LocalSessionStore {
|
|
|
356
345
|
await this.eventLog.append(record);
|
|
357
346
|
}
|
|
358
347
|
catch (error) {
|
|
359
|
-
// Event logs are optional audit/debug sinks. SQLite and in-memory session state remain authoritative.
|
|
360
348
|
this.logger.error("event log append failed", {
|
|
361
349
|
eventType: record.type,
|
|
362
350
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -572,9 +560,6 @@ export class LocalSessionStore {
|
|
|
572
560
|
}
|
|
573
561
|
}
|
|
574
562
|
}
|
|
575
|
-
// In-memory session store: the orchestration above with Map-backed record and
|
|
576
|
-
// artifact stores. Public name + constructor are unchanged so existing callers and
|
|
577
|
-
// the session-store contract test keep working as the regression wall.
|
|
578
563
|
export class InMemorySessionStore extends LocalSessionStore {
|
|
579
564
|
constructor(options) {
|
|
580
565
|
super({
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// The existing internal card/HTTP limit applies to the complete JSON-RPC message.
|
|
2
1
|
import { SETTINGS_IMPORT_MAX_SIZE } from "./settingsOperation.js";
|
|
3
2
|
export const MAX_JSON_BODY_BYTES = 64 * 1024;
|
|
4
3
|
export function settingsJsonSize(value) { return new TextEncoder().encode(JSON.stringify(value)).length; }
|
|
@@ -6,8 +5,6 @@ export function settingsRawChunkBudget(envelopeWithEmptyBase64) {
|
|
|
6
5
|
const room = MAX_JSON_BODY_BYTES - settingsJsonSize(envelopeWithEmptyBase64);
|
|
7
6
|
return room > 0 ? 3 * Math.floor(room / 4) : 0;
|
|
8
7
|
}
|
|
9
|
-
// The actual request may carry additional Host metadata. Rejection happens
|
|
10
|
-
// before dispatch; its independently measured room lets the same offset retry.
|
|
11
8
|
export function settingsUploadMessageBudget(message) {
|
|
12
9
|
if (!message || typeof message !== "object")
|
|
13
10
|
return undefined;
|
|
@@ -15,8 +12,6 @@ export function settingsUploadMessageBudget(message) {
|
|
|
15
12
|
if (envelope.method !== "tools/call" || envelope.params?.name !== "ui.act_card" ||
|
|
16
13
|
envelope.params.arguments?.input?.action !== "settings_upload_chunk")
|
|
17
14
|
return undefined;
|
|
18
|
-
// Safe-integer decimal maxima cover ids/revisions; upload offsets use the
|
|
19
|
-
// accepted raw-file limit.
|
|
20
15
|
envelope.id = Number.MAX_SAFE_INTEGER;
|
|
21
16
|
envelope.params.arguments.revision = Number.MAX_SAFE_INTEGER;
|
|
22
17
|
envelope.params.arguments.input.offset = SETTINGS_IMPORT_MAX_SIZE;
|
|
@@ -19,8 +19,6 @@ export function decodeSettingsChunk(value) {
|
|
|
19
19
|
throw new SettingsConflict("Part of the file was not encoded correctly.");
|
|
20
20
|
return data;
|
|
21
21
|
}
|
|
22
|
-
/** Private files are named from backend-generated identities only. SQLite owns
|
|
23
|
-
* reservations and progress; this class verifies the corresponding physical fact. */
|
|
24
22
|
export class SettingsFileStore {
|
|
25
23
|
dataDirectory;
|
|
26
24
|
constructor(dataDirectory) {
|
|
@@ -94,8 +92,6 @@ export class SettingsFileStore {
|
|
|
94
92
|
throw new SettingsConflict("The snapshot is not ready to save.");
|
|
95
93
|
await this.prepareDirectory("backups");
|
|
96
94
|
const source = this.stagingPath(file), destination = this.localBackupPath(file);
|
|
97
|
-
// Persist the verified source identity before the only effect. Recovery
|
|
98
|
-
// verifies this same inode; it never creates a missing destination.
|
|
99
95
|
const identity = await this.downloadSourceIdentity(file);
|
|
100
96
|
rememberSource(identity);
|
|
101
97
|
try {
|
|
@@ -133,7 +129,7 @@ export class SettingsFileStore {
|
|
|
133
129
|
}
|
|
134
130
|
if (error instanceof SettingsConflict || fileCode(error, "ELOOP"))
|
|
135
131
|
return { status: "failed", reason: "target_conflict" };
|
|
136
|
-
throw error;
|
|
132
|
+
throw error;
|
|
137
133
|
}
|
|
138
134
|
try {
|
|
139
135
|
if (!sourceIdentity)
|
|
@@ -190,8 +186,6 @@ export class SettingsFileStore {
|
|
|
190
186
|
const length = (await handle.stat()).size;
|
|
191
187
|
if (length < reservation.offset || length > reservation.offset + data.length)
|
|
192
188
|
throw new SettingsConflict("The file size changed during transfer.");
|
|
193
|
-
// A crash may leave the beginning of this exact reserved chunk. Verify it
|
|
194
|
-
// before completing the remainder; never overwrite a different prefix.
|
|
195
189
|
const written = length - reservation.offset;
|
|
196
190
|
const prefix = Buffer.alloc(written);
|
|
197
191
|
const read = await handle.read(prefix, 0, written, reservation.offset);
|
|
@@ -309,8 +303,6 @@ export class SettingsFileStore {
|
|
|
309
303
|
if (!file.sealed || file.invalidated || !Number.isSafeInteger(offset) || !Number.isSafeInteger(length) ||
|
|
310
304
|
offset < 0 || length < 1 || offset + length > file.totalSize)
|
|
311
305
|
throw new SettingsConflict("This part of the backup is unavailable.");
|
|
312
|
-
// Verify the artifact on the same open descriptor used for this chunk, so
|
|
313
|
-
// path replacement cannot substitute unchecked content between two opens.
|
|
314
306
|
const info = await lstat(directory);
|
|
315
307
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
316
308
|
throw new SettingsConflict("The local backup directory changed.");
|
|
@@ -341,6 +333,5 @@ export class SettingsFileStore {
|
|
|
341
333
|
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
342
334
|
throw error;
|
|
343
335
|
}
|
|
344
|
-
// The public backup is the user's artifact and is never removed here.
|
|
345
336
|
}
|
|
346
337
|
}
|
|
@@ -2,8 +2,6 @@ import { SuiEndpointError } from "../suiEndpoint.js";
|
|
|
2
2
|
import { LocalSettingsError } from "../preferences/preferencesStore.js";
|
|
3
3
|
import { LocalDataError } from "../activity/localDataTypes.js";
|
|
4
4
|
export const SETTINGS_REPLACEMENT_BLOCKED_MESSAGE = "A wallet or transaction response is still pending, or the initial transaction-result waiting period has not ended. Local records cannot be replaced yet.";
|
|
5
|
-
// Only authenticated Settings validation reaches this mapping. Return fixed
|
|
6
|
-
// explanations and recovery categories, never imported values or exceptions.
|
|
7
5
|
export function settingsInputError(error, kind) {
|
|
8
6
|
if (!(error instanceof SuiEndpointError) &&
|
|
9
7
|
!(error instanceof LocalSettingsError && error.kind === "input_invalid") &&
|
|
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|
|
2
2
|
import { assetReadAccountSchema, walletAvailabilitySchema } from "./walletConnection.js";
|
|
3
3
|
import { LOCAL_DATA_EXPORT_FORMAT } from "../activity/localDataTypes.js";
|
|
4
4
|
import { suiAddressStringSchema } from "../suiAddress.js";
|
|
5
|
-
// This is the existing backup input limit, measured before Base64/JSON wrapping.
|
|
6
5
|
export const SETTINGS_IMPORT_MAX_SIZE = 16 * 1024 * 1024;
|
|
7
6
|
const identity = z.string().uuid();
|
|
8
7
|
const size = z.number().int().nonnegative().safe();
|
|
@@ -89,9 +88,6 @@ export const settingsActionSchema = z.discriminatedUnion("action", [
|
|
|
89
88
|
z.object({ action: z.literal("settings_back"), operationId: identity }).strict(),
|
|
90
89
|
z.object({ action: z.literal("settings_cancel") }).strict()
|
|
91
90
|
]);
|
|
92
|
-
// A known rejection belongs to the selected complete file, not to the current
|
|
93
|
-
// local-data revision or short-lived confirmation. Selection/owner/epoch rules
|
|
94
|
-
// remain enforced by the operation's store and action handlers.
|
|
95
91
|
export function retainedBackupRejection(operation, file) {
|
|
96
92
|
const saved = storedRestoreCheckSchema.safeParse(operation.preparation?.restoreCheck);
|
|
97
93
|
if (operation.draft?.kind !== "import_local_data" || !file || operation.fileId !== file.fileId ||
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// The trigger and explicit Back use the same stored preparation transition.
|
|
2
|
-
// A sealed upload is separate from the short-lived confirmation of its impact.
|
|
3
1
|
export const SETTINGS_REUSABLE_UPLOAD_SQL = `
|
|
4
2
|
kind='import_local_data' AND EXISTS (
|
|
5
3
|
SELECT 1 FROM settings_files f
|
|
@@ -11,13 +11,11 @@ import { isSettingsDownloadFile, settingsDownloadPath } from "./settingsFileIden
|
|
|
11
11
|
import { SettingsDownloadPathExists, settingsFileDigest, decodeSettingsChunk } from "./settingsFileStore.js";
|
|
12
12
|
import { settingsIdentity } from "./sqliteSettingsWorkflowStore.js";
|
|
13
13
|
import { SettingsConflict, settingsDownload, isSettingsDownloadFileAction, settingsViewSchema, endpointPreparationSchema, storedRestoreCheckSchema, retainedBackupRejection, permitsBackupRecheck } from "./settingsOperation.js";
|
|
14
|
-
// One existing product-session length from physical snapshot readiness.
|
|
15
14
|
export const DOWNLOAD_RETRY_WINDOW_MS = DEFAULT_SESSION_TTL_MS;
|
|
16
15
|
export class SettingsWorkflow {
|
|
17
16
|
options;
|
|
18
17
|
pendingResults = new Map();
|
|
19
18
|
finishedFileIO = new Set();
|
|
20
|
-
// This tracks actual pending I/O only; SQLite owns check results and permission.
|
|
21
19
|
checkingFiles = new Set();
|
|
22
20
|
downloadTimer;
|
|
23
21
|
closing = false;
|
|
@@ -34,16 +32,12 @@ export class SettingsWorkflow {
|
|
|
34
32
|
create(record) { this.records.create(record); }
|
|
35
33
|
operationKind(record) { return this.records.forCard(record).draft?.kind; }
|
|
36
34
|
recoverOwner() {
|
|
37
|
-
// Invalidate old input synchronously. Verifying a prior backup and removing
|
|
38
|
-
// temporary files must not delay unrelated MCP startup or completed reads.
|
|
39
35
|
this.records.transaction(() => {
|
|
40
36
|
const downloads = this.records.db.prepare("SELECT operation_id FROM settings_operations WHERE kind='download_backup' AND owner_id!=?")
|
|
41
37
|
.all(this.records.ownerId);
|
|
42
38
|
for (const row of downloads) {
|
|
43
39
|
const operation = this.records.get(row.operation_id);
|
|
44
40
|
if (settingsDownload(operation)?.localSave?.status === "saving") {
|
|
45
|
-
// Inherit settlement, never the departed card's input permission.
|
|
46
|
-
// Keep its final/file reservation until the existing artifact is checked.
|
|
47
41
|
this.records.db.prepare("UPDATE settings_operations SET owner_id=? WHERE operation_id=?").run(this.records.ownerId, operation.operationId);
|
|
48
42
|
}
|
|
49
43
|
else
|
|
@@ -76,10 +70,6 @@ export class SettingsWorkflow {
|
|
|
76
70
|
if (file)
|
|
77
71
|
this.records.releaseFileIO(file.fileId);
|
|
78
72
|
}
|
|
79
|
-
// The departed owner may have committed a terminal result and finished
|
|
80
|
-
// physical I/O, but failed to persist its final I/O release. Terminal
|
|
81
|
-
// operations were intentionally not reopened above. Release that stale
|
|
82
|
-
// reservation without replaying the operation or touching a current owner.
|
|
83
73
|
const ioId = this.records.control().file_io_id;
|
|
84
74
|
if (ioId) {
|
|
85
75
|
const file = this.records.file(ioId);
|
|
@@ -126,8 +116,6 @@ export class SettingsWorkflow {
|
|
|
126
116
|
this.downloadTimer = setTimeout(() => {
|
|
127
117
|
this.downloadTimer = undefined;
|
|
128
118
|
void this.maintenance(() => this.maintainDownloads()).catch(() => {
|
|
129
|
-
// Durable cleanup remains pending/failed; a same-operation read or
|
|
130
|
-
// startup can retry. Never spin on an already elapsed deadline.
|
|
131
119
|
});
|
|
132
120
|
}, Math.max(0, Math.min(...deadlines) - this.now().getTime()));
|
|
133
121
|
this.downloadTimer.unref?.();
|
|
@@ -149,16 +137,11 @@ export class SettingsWorkflow {
|
|
|
149
137
|
this.options.assertCurrent();
|
|
150
138
|
failure ??= error;
|
|
151
139
|
}
|
|
152
|
-
// A failed, already-due journal stays retryable through Check status/startup;
|
|
153
|
-
// it cannot spin at zero delay or cancel another snapshot's future deadline.
|
|
154
140
|
this.scheduleDownloadExpiry(failure !== undefined);
|
|
155
141
|
if (failure)
|
|
156
142
|
throw failure;
|
|
157
143
|
}
|
|
158
144
|
dataReplaced() {
|
|
159
|
-
// The replacement action owns and awaits its transferred cleanup journal.
|
|
160
|
-
// Starting another cleanup here would race its I/O reservation and return
|
|
161
|
-
// before the action's required cleanup result is known.
|
|
162
145
|
this.scheduleDownloadExpiry();
|
|
163
146
|
}
|
|
164
147
|
async close() {
|
|
@@ -186,7 +169,7 @@ export class SettingsWorkflow {
|
|
|
186
169
|
result = { outcome: "completed", recordedAt: this.now().toISOString(), message: "The previously created local backup was verified.",
|
|
187
170
|
details: { path: this.options.files.exportPath(file), filename: file.displayName, fileId: file.fileId, sha256: file.sha256, totalSize: file.totalSize } };
|
|
188
171
|
}
|
|
189
|
-
catch {
|
|
172
|
+
catch { }
|
|
190
173
|
}
|
|
191
174
|
this.options.assertCurrent();
|
|
192
175
|
this.retainResult(operation.operationId, result, false, file && result.outcome === "completed" ? { ...file, sealed: true, receivedSize: file.totalSize } : undefined);
|
|
@@ -218,8 +201,6 @@ export class SettingsWorkflow {
|
|
|
218
201
|
return { file: checked.file, offset, chunkBase64: data.toString("base64") };
|
|
219
202
|
}
|
|
220
203
|
finally {
|
|
221
|
-
// A read may have been the last physical I/O keeping an ended snapshot
|
|
222
|
-
// alive. Settle its own journal; a saved-result read need not await it.
|
|
223
204
|
await this.cleanupTemporaryFiles(operation.operationId);
|
|
224
205
|
}
|
|
225
206
|
}
|
|
@@ -260,8 +241,6 @@ export class SettingsWorkflow {
|
|
|
260
241
|
this.records.transaction(() => {
|
|
261
242
|
this.records.endDownload(file.operationId, "source_unavailable");
|
|
262
243
|
const existing = this.records.file(file.fileId);
|
|
263
|
-
// An exclusive-create collision never made this request the physical
|
|
264
|
-
// file's owner. Do not delete that pre-existing item as cleanup.
|
|
265
244
|
this.records.writeFile({ ...existing, invalidated: true, cleanup: "complete" });
|
|
266
245
|
});
|
|
267
246
|
result = { outcome: "failed", completionScope: "backup_snapshot", recordedAt: this.now().toISOString(),
|
|
@@ -325,8 +304,6 @@ export class SettingsWorkflow {
|
|
|
325
304
|
retained = { saveAttemptId, outcome };
|
|
326
305
|
this.localSaveResults.set(file.operationId, retained);
|
|
327
306
|
}
|
|
328
|
-
// Retain the ended physical outcome if the DB write fails. A read retries
|
|
329
|
-
// this same settlement, not link(), a new snapshot, or a Host request.
|
|
330
307
|
this.records.settleLocalSave(file.operationId, retained.saveAttemptId, retained.outcome);
|
|
331
308
|
this.localSaveResults.delete(file.operationId);
|
|
332
309
|
this.scheduleDownloadExpiry();
|
|
@@ -498,8 +475,6 @@ export class SettingsWorkflow {
|
|
|
498
475
|
this.observeLocalSave(operation);
|
|
499
476
|
let downloadExpiryPending = false;
|
|
500
477
|
if (operation.result) {
|
|
501
|
-
// The result is already committed. A failed release of known-ended I/O
|
|
502
|
-
// must remain retryable without making that result depend on the write.
|
|
503
478
|
try {
|
|
504
479
|
this.synchronize(operation.operationId);
|
|
505
480
|
}
|
|
@@ -541,7 +516,6 @@ export class SettingsWorkflow {
|
|
|
541
516
|
await this.cleanupFile(file, false).catch(() => { this.options.assertCurrent(); });
|
|
542
517
|
}
|
|
543
518
|
}
|
|
544
|
-
// Completed facts do not depend on current wallet or unrelated cleanup writes.
|
|
545
519
|
const facts = operation.result ? undefined : await this.options.readFacts();
|
|
546
520
|
this.options.assertCurrent();
|
|
547
521
|
record = this.records.cards.get(record.state.cardId);
|
|
@@ -566,8 +540,6 @@ export class SettingsWorkflow {
|
|
|
566
540
|
if (fileIOAvailable && file && !file.invalidated && file.receivedSize === file.totalSize && !file.reservation &&
|
|
567
541
|
restoreCheck && permitsBackupRecheck(restoreCheck))
|
|
568
542
|
allowed.push("settings_prepare");
|
|
569
|
-
// A matching upload may resume without allocating another file. New
|
|
570
|
-
// writes still require the shared reservation check at their admission.
|
|
571
543
|
if (fileIOAvailable && ["draft", "uploading"].includes(operation.phase) &&
|
|
572
544
|
(file && !file.invalidated || this.records.canReserveFileIO(operation.operationId)))
|
|
573
545
|
allowed.push("settings_begin_upload");
|
|
@@ -677,8 +649,6 @@ export class SettingsWorkflow {
|
|
|
677
649
|
return;
|
|
678
650
|
}
|
|
679
651
|
if (action.action === "settings_select" && action.draft.kind === "set_endpoint") {
|
|
680
|
-
// Drafts are public display state. Reject credentials and invalid URLs
|
|
681
|
-
// before storing or projecting the user's endpoint input.
|
|
682
652
|
action = { ...action, draft: { ...action.draft,
|
|
683
653
|
value: (action.draft.endpoint === "grpc" ? parseGrpcUrl : parseGraphqlUrl)(action.draft.value) } };
|
|
684
654
|
}
|
|
@@ -980,8 +950,6 @@ export class SettingsWorkflow {
|
|
|
980
950
|
}
|
|
981
951
|
catch (error) {
|
|
982
952
|
this.options.assertCurrent();
|
|
983
|
-
// A rejected final verification does not apply data or end the card,
|
|
984
|
-
// but it must revoke the previous ready result for this exact file.
|
|
985
953
|
this.records.transaction(() => {
|
|
986
954
|
const { card, operation } = this.records.requireInput(expected, action.operationId);
|
|
987
955
|
if (operation.fileId !== file.fileId)
|
|
@@ -1128,8 +1096,6 @@ export class SettingsWorkflow {
|
|
|
1128
1096
|
}
|
|
1129
1097
|
return this.records.finish(id, retained.result);
|
|
1130
1098
|
});
|
|
1131
|
-
// A later cleanup can reserve this same file. The ended publication must
|
|
1132
|
-
// not be reused as evidence that this new I/O has also ended.
|
|
1133
1099
|
if (settledFileId)
|
|
1134
1100
|
this.finishedFileIO.delete(settledFileId);
|
|
1135
1101
|
if (applied && !ready)
|
|
@@ -1167,8 +1133,6 @@ export class SettingsWorkflow {
|
|
|
1167
1133
|
for (const [id, pending] of this.pendingResults) {
|
|
1168
1134
|
if (operationId !== undefined && id !== operationId || recoveryOnly && this.records.get(id)?.draft?.kind !== "restart_wallet_service")
|
|
1169
1135
|
continue;
|
|
1170
|
-
// Ready is replayed only inside the SDK snapshot transaction; a previous
|
|
1171
|
-
// failed transaction did not prove that its observation was committed.
|
|
1172
1136
|
if (!pending.ready)
|
|
1173
1137
|
this.retainResult(id, pending.result, false, pending.file);
|
|
1174
1138
|
}
|
|
@@ -60,8 +60,6 @@ export class SqliteCardRecordStore {
|
|
|
60
60
|
@input, @accepted, @result, @display, @scope, @operation)`).run(values(record));
|
|
61
61
|
}
|
|
62
62
|
replace(expected, next) {
|
|
63
|
-
// This single statement commits admission or a result atomically. A removed
|
|
64
|
-
// card is never recreated by a delayed network response.
|
|
65
63
|
return this.db.prepare(`UPDATE live_read_cards SET state=@state, reason=@reason, error=@error,
|
|
66
64
|
revision=@revision, input_json=@input, accepted_input_json=@accepted, result_json=@result,
|
|
67
65
|
receipt_display_json=@display, scope=@scope, operation_id=@operation
|
|
@@ -38,13 +38,6 @@ export function updateLiveReviewSessionRow(db, expectedRevision, session) {
|
|
|
38
38
|
.run({ ...serialized, expectedRevision });
|
|
39
39
|
return result.changes > 0;
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
* SQLite-backed live review-session records. Runtime product transitions are
|
|
43
|
-
* committed by SqliteActivityStore when the store is created through that owner,
|
|
44
|
-
* so live session state and review activity stay in one SQLite transaction.
|
|
45
|
-
* Direct instances keep a low-level transition commit path for tests and mixed
|
|
46
|
-
* harnesses that do not couple live state with activity rows.
|
|
47
|
-
*/
|
|
48
41
|
export class SqliteSessionRecordStore {
|
|
49
42
|
db;
|
|
50
43
|
usesActivityStoreLiveSessionMutations;
|
|
@@ -107,8 +100,6 @@ export class SqlitePrivateReviewArtifactStore {
|
|
|
107
100
|
if (!row) {
|
|
108
101
|
return undefined;
|
|
109
102
|
}
|
|
110
|
-
// clonePrivateReviewArtifacts re-parses/validates the evidence on read, matching
|
|
111
|
-
// the in-memory store's clone-on-read behaviour.
|
|
112
103
|
return clonePrivateReviewArtifacts(JSON.parse(row.artifacts_json));
|
|
113
104
|
}
|
|
114
105
|
set(reviewSessionId, artifacts) {
|
|
@@ -126,8 +117,6 @@ export class SqlitePrivateReviewArtifactStore {
|
|
|
126
117
|
this.db.prepare(`DELETE FROM live_private_review_artifacts`).run();
|
|
127
118
|
}
|
|
128
119
|
}
|
|
129
|
-
// Reconstruct the ReviewSession from columns + JSON blobs. Optional fields are
|
|
130
|
-
// omitted (not set to undefined) so the shape matches the in-memory store exactly.
|
|
131
120
|
export function sessionFromLiveReviewSessionRow(row) {
|
|
132
121
|
return {
|
|
133
122
|
id: row.id,
|