@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
|
@@ -4,8 +4,6 @@ import { SqliteCardRecordStore } from "./sqliteCardStore.js";
|
|
|
4
4
|
import { SettingsConflict, settingsDownload, storedSettingsDownloadSchema, settingsDraftSchema, settingsResultSchema, retainedBackupRejection } from "./settingsOperation.js";
|
|
5
5
|
import { isSettingsDownloadFile, settingsBackupPath, SETTINGS_FILE_CLEANUP_SQL } from "./settingsFileIdentity.js";
|
|
6
6
|
import { SETTINGS_REUSABLE_UPLOAD_SQL, SETTINGS_RESET_PREPARATION_SQL } from "./settingsPreparationSql.js";
|
|
7
|
-
/** Settings transitions share the activity owner's SQLite connection and card CAS.
|
|
8
|
-
* Network and filesystem work must remain outside these transactions. */
|
|
9
7
|
export class SqliteSettingsWorkflowStore {
|
|
10
8
|
db;
|
|
11
9
|
ownerId;
|
|
@@ -118,7 +116,6 @@ export class SqliteSettingsWorkflowStore {
|
|
|
118
116
|
const control = this.control();
|
|
119
117
|
return this.fileIOAvailable(control) && (control.active_operation_id === null || control.active_operation_id === operationId);
|
|
120
118
|
}
|
|
121
|
-
// Read-only eligibility check shared by preparation and final admission.
|
|
122
119
|
requireFinalSlot(replaceId) {
|
|
123
120
|
const control = this.control();
|
|
124
121
|
if (!this.fileIOAvailable(control) || control.active_operation_id !== (replaceId ?? null)) {
|
|
@@ -145,8 +142,6 @@ export class SqliteSettingsWorkflowStore {
|
|
|
145
142
|
return next;
|
|
146
143
|
});
|
|
147
144
|
}
|
|
148
|
-
// Pure admission checks let deadline reconciliation commit an expected refusal
|
|
149
|
-
// without claiming a final slot or consuming input. Admission uses the same rule.
|
|
150
145
|
requireAdmission(expected, input, currentFingerprint, replaceOperationId, now = this.now()) {
|
|
151
146
|
const result = this.requireInput(expected, input.operationId, now), operation = result.operation;
|
|
152
147
|
if (operation.phase !== "prepared" || !operation.draft || operation.preparationId !== input.preparationId ||
|
|
@@ -212,8 +207,6 @@ export class SqliteSettingsWorkflowStore {
|
|
|
212
207
|
if (!isDeepStrictEqual(this.get(expected.operationId), expected) || expected.draft?.kind !== "download_backup") {
|
|
213
208
|
throw new SettingsConflict("The download state changed before it could be saved.");
|
|
214
209
|
}
|
|
215
|
-
// A terminal result is immutable serialized evidence. Updating delivery
|
|
216
|
-
// state must not reserialize it, even with semantically identical keys.
|
|
217
210
|
this.db.prepare("UPDATE settings_operations SET preparation_json=? WHERE operation_id=?")
|
|
218
211
|
.run(JSON.stringify({ download }), expected.operationId);
|
|
219
212
|
if (!this.cards.replace(card, { ...card, state: { ...card.state, revision: card.state.revision + 1 } })) {
|
|
@@ -259,7 +252,6 @@ export class SqliteSettingsWorkflowStore {
|
|
|
259
252
|
}
|
|
260
253
|
continueDownload(expected, input) {
|
|
261
254
|
return this.transaction(() => {
|
|
262
|
-
// Validate the caller's scope before any target reads or expiry writes.
|
|
263
255
|
this.downloadContext(expected, input.operationId, input.fileId);
|
|
264
256
|
this.expireDownloads(input.operationId);
|
|
265
257
|
const { card, operation, file, download } = this.downloadContext(expected, input.operationId, input.fileId);
|
|
@@ -454,8 +446,6 @@ export class SqliteSettingsWorkflowStore {
|
|
|
454
446
|
}
|
|
455
447
|
}
|
|
456
448
|
}
|
|
457
|
-
// The input is already a strict typed action. Sort object keys recursively so
|
|
458
|
-
// JSON-RPC ids and object insertion order never change business idempotency.
|
|
459
449
|
export function settingsIdentity(value) {
|
|
460
450
|
const canonical = (input) => Array.isArray(input) ? input.map(canonical) : input !== null && typeof input === "object"
|
|
461
451
|
? Object.fromEntries(Object.entries(input).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonical(item)])) : input;
|
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
import { buildTransactionMaterialRecord, sameHandle, toMaterialHandle } from "./transactionMaterialStore.js";
|
|
2
|
-
/**
|
|
3
|
-
* SQLite-backed transaction material store. Holds the same synchronous contract as
|
|
4
|
-
* InMemoryLocalTransactionMaterialStore so producers and the wallet handoff path are
|
|
5
|
-
* unchanged, but persists records (including the unsigned transaction BLOB) to the
|
|
6
|
-
* shared local database so any review-server process can read them. Validation,
|
|
7
|
-
* handle identity, and expiry semantics are shared via transactionMaterialStore.ts,
|
|
8
|
-
* so the two backends never drift. The unsigned bytes are protected at rest by the
|
|
9
|
-
* data directory (0700) and database file (0600) permissions set in SqliteActivityStore.
|
|
10
|
-
*/
|
|
11
2
|
export class SqliteTransactionMaterialStore {
|
|
12
3
|
db;
|
|
13
4
|
constructor(db) {
|
|
@@ -44,8 +35,6 @@ export class SqliteTransactionMaterialStore {
|
|
|
44
35
|
this.db.prepare(`DELETE FROM live_transaction_materials WHERE review_session_id = ?`).run(reviewSessionId);
|
|
45
36
|
}
|
|
46
37
|
}
|
|
47
|
-
// Each read rebuilds a fresh record (new Uint8Array from the BLOB, parsed
|
|
48
|
-
// diagnostics) so a caller mutating the result never touches the stored row.
|
|
49
38
|
function recordFromRow(row) {
|
|
50
39
|
return {
|
|
51
40
|
materialId: row.material_id,
|
|
@@ -34,8 +34,6 @@ const preparationInterruptionReasons = {
|
|
|
34
34
|
wallet_service_restarted: "The wallet service was restarted. Update this review before requesting approval.",
|
|
35
35
|
wallet_service_lost: "The wallet service stopped. Restore the wallet connection and update this review before requesting approval."
|
|
36
36
|
};
|
|
37
|
-
/** Purpose-specific mutations on the activity owner's connection. Each method
|
|
38
|
-
* commits card, request and public history together; no network work runs here. */
|
|
39
37
|
export class SqliteWalletWorkflowStore {
|
|
40
38
|
db;
|
|
41
39
|
ownerId;
|
|
@@ -103,8 +101,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
103
101
|
return recovery;
|
|
104
102
|
}
|
|
105
103
|
recordWalletFailure(input) {
|
|
106
|
-
// Retried by the workflow until revocation is durably recorded. The SDK
|
|
107
|
-
// run is already fenced, so a failed write cannot preserve usable authority.
|
|
108
104
|
this.invalidateWalletRun("wallet_service_lost", new Date(input.observedAt), input.runId);
|
|
109
105
|
}
|
|
110
106
|
publishWalletState() { this.publishAvailableWallets(); }
|
|
@@ -115,8 +111,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
115
111
|
continue;
|
|
116
112
|
const observed = snapshot.sessions.find((item) => item.topic === record.topic);
|
|
117
113
|
if (record.connection.status !== "connected") {
|
|
118
|
-
// Startup can transfer management ownership; ordinary observations
|
|
119
|
-
// update the current run's facts without restoring terminal authority.
|
|
120
114
|
if ((ready || record.ownerId === this.ownerId && record.runId === snapshot.runId) &&
|
|
121
115
|
observed?.status === "present" && !record.sdkPending &&
|
|
122
116
|
!this.pendingDisconnect(record.connection.connectionId, record.ownerId)) {
|
|
@@ -208,8 +202,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
208
202
|
return { confirmed: true, connections, unrecorded, pendingConnectionIds };
|
|
209
203
|
}
|
|
210
204
|
readConnectionFacts(now) {
|
|
211
|
-
// A deferred read transaction reuses an outer admission transaction when
|
|
212
|
-
// present. It never repairs callbacks, advances deadlines or reserves I/O.
|
|
213
205
|
return this.db.transaction(() => {
|
|
214
206
|
const records = this.connections(), snapshot = this.readSdkSnapshot(), account = this.readActiveAccount();
|
|
215
207
|
return { evaluatedAt: now.toISOString(), activeAccount: account?.address, activeAccountConnectionId: account?.walletId,
|
|
@@ -239,9 +231,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
239
231
|
if (!card || card.ownerId !== this.ownerId || card.operationId !== id || card.acceptedInput?.action !== source.action ||
|
|
240
232
|
card.acceptedInput.walletRunId !== source.runId)
|
|
241
233
|
return;
|
|
242
|
-
// A connection may progress from pairing to one admitted disconnect.
|
|
243
|
-
// That later operation owns sdk_pending even if its result is already
|
|
244
|
-
// visible and the original pairing's failed cleanup write arrives late.
|
|
245
234
|
const disconnect = this.db.prepare(`SELECT id FROM live_read_cards WHERE owner_id=? AND scope='connect' AND operation_id=?
|
|
246
235
|
AND json_extract(accepted_input_json,'$.action')='disconnect'`).get(this.ownerId, id);
|
|
247
236
|
if (source.action === "connect" && disconnect && disconnect.id !== source.cardId)
|
|
@@ -314,8 +303,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
314
303
|
if (card.state.kind === "review" && !session)
|
|
315
304
|
throw new WorkflowConflict("The saved review is unavailable.");
|
|
316
305
|
const connection = card.state.kind === "connect" && card.operationId ? this.connection(card.operationId)?.connection : undefined;
|
|
317
|
-
// Wallet-dependent commands already passed the workflow availability guard.
|
|
318
|
-
// This store checks DB eligibility; it does not measure SDK health.
|
|
319
306
|
const connectionFacts = this.readConnectionFacts(now), targetAccount = session?.account ?? connectionFacts.activeAccount;
|
|
320
307
|
const facts = { ...connectionFacts, ownerId: this.ownerId, record: card, session, request,
|
|
321
308
|
authority: request ? this.authority(request.attemptId) : undefined, connection,
|
|
@@ -380,8 +367,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
380
367
|
WHERE operation_id=@id`).run({ terminal: terminal ? 1 : 0, id: operationId });
|
|
381
368
|
}
|
|
382
369
|
publishConnection(connectionId, status) {
|
|
383
|
-
// Connection updates are not completion of every operation referencing it.
|
|
384
|
-
// In particular, session_update(connected) must not finish a pending disconnect.
|
|
385
370
|
const outcome = this.connection(connectionId)?.connection;
|
|
386
371
|
this.db.prepare(`UPDATE live_read_cards SET revision=revision+1,
|
|
387
372
|
result_json=CASE WHEN state='running' AND json_extract(accepted_input_json,'$.action')='disconnect'
|
|
@@ -447,8 +432,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
447
432
|
this.db.prepare("INSERT INTO live_wallet_connections (id,owner_id,status,revision,topic,sdk_pending,connection_json,run_id) VALUES (?,?,?,?,?,?,?,?)")
|
|
448
433
|
.run(connection.connectionId, this.ownerId, connection.status, 0, null, 1, JSON.stringify(connection), this.runId ?? null);
|
|
449
434
|
this.consume(card, input, connection.connectionId);
|
|
450
|
-
// Every other live input observes the admitted connection before the SDK
|
|
451
|
-
// responds, so an older selection cannot bypass the one-connection rule.
|
|
452
435
|
this.publishAvailableWallets();
|
|
453
436
|
return this.connection(connection.connectionId);
|
|
454
437
|
}).immediate();
|
|
@@ -494,7 +477,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
494
477
|
owner_id=excluded.owner_id, status=excluded.status, revision=excluded.revision, topic=excluded.topic,
|
|
495
478
|
sdk_pending=0, connection_json=excluded.connection_json,run_id=excluded.run_id`).run(id, this.ownerId, next.status, next.revision, session.topic, 0, JSON.stringify(next), this.runId ?? null);
|
|
496
479
|
this.publishConnection(id, next.status);
|
|
497
|
-
// Restoring the SDK session never restores a cleared read-account context.
|
|
498
480
|
return this.connection(id);
|
|
499
481
|
}
|
|
500
482
|
useAccount(expected, input, connectionId, account) {
|
|
@@ -532,7 +514,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
532
514
|
finishDisconnect(cardId, connectionId, succeeded, now) {
|
|
533
515
|
this.db.transaction(() => {
|
|
534
516
|
const pending = this.pendingDisconnect(connectionId);
|
|
535
|
-
// A session deletion event may have already completed this operation.
|
|
536
517
|
if (pending?.state.cardId !== cardId)
|
|
537
518
|
return;
|
|
538
519
|
this.updateConnection(connectionId, succeeded ? { status: "disconnected" } : {
|
|
@@ -587,8 +568,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
587
568
|
const artifacts = new SqlitePrivateReviewArtifactStore(this.db).get(material.reviewSessionId);
|
|
588
569
|
const stored = this.materialStore.getTransactionMaterial(material.transactionMaterial, now);
|
|
589
570
|
if (!current || !materialStillMatches(material, current, sessions.revision(current.id), artifacts, stored, now)) {
|
|
590
|
-
// Expected expiry commits its normal cleanup, but no request authority.
|
|
591
|
-
// A changed candidate is a conflict and must not invalidate newer data.
|
|
592
571
|
const evaluated = this.evaluateState({ purpose: "review", reviewSessionId: material.reviewSessionId,
|
|
593
572
|
readTarget: this.reviewReadTarget(material.reviewSessionId, card), expectedCard: card, candidate: { session: material.review, rowRevision: material.rowRevision,
|
|
594
573
|
artifacts: material.artifacts, material }, walletAvailability: { status: "available", walletRunId } }, now);
|
|
@@ -709,8 +688,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
709
688
|
if (old.requestStatus === "awaiting_signature" && next !== old.requestStatus)
|
|
710
689
|
this.publishAvailableWallets();
|
|
711
690
|
if (["stopped", "request_failed", "outcome_unknown", "completed"].includes(next)) {
|
|
712
|
-
// A historical attempt may finish while a newer review is preparing.
|
|
713
|
-
// Clean only the material still owned by this admitted revision.
|
|
714
691
|
for (const table of ["live_transaction_materials", "live_private_review_artifacts"]) {
|
|
715
692
|
this.db.prepare(`DELETE FROM ${table} WHERE review_session_id=? AND EXISTS
|
|
716
693
|
(SELECT 1 FROM live_review_sessions WHERE id=? AND review_revision=? AND preparation_id IS NULL)`)
|
|
@@ -741,8 +718,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
741
718
|
this.db.transaction(() => {
|
|
742
719
|
const now = this.clock();
|
|
743
720
|
this.advanceRequestDeadlines(now, id);
|
|
744
|
-
// Observation may have finished, or its window may have closed, while
|
|
745
|
-
// submit was pending. A late response cannot reopen either outcome.
|
|
746
721
|
if (this.request(id)?.requestStatus === "submitting")
|
|
747
722
|
this.transitionRequest(id, "awaiting_chain_result", now);
|
|
748
723
|
}).immediate();
|
|
@@ -757,8 +732,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
757
732
|
return request;
|
|
758
733
|
if (request.requestStatus === "awaiting_signature")
|
|
759
734
|
return this.transitionRequest(id, cause === "wallet_service_lost" ? "request_failed" : "stopped", now, { reason: interruptionReasons[cause] });
|
|
760
|
-
// Once submitted, connection changes cannot revoke a transaction or the
|
|
761
|
-
// independent lookup of its digest. Only explicit user stopping ends observation.
|
|
762
735
|
if (cause === "user_stop" && authority.observation_stopped === 0 &&
|
|
763
736
|
["submitting", "awaiting_chain_result", "outcome_unknown"].includes(request.requestStatus)) {
|
|
764
737
|
this.db.prepare("UPDATE live_request_authority SET observation_stopped=1 WHERE attempt_id=?").run(id);
|
|
@@ -778,8 +751,6 @@ export class SqliteWalletWorkflowStore {
|
|
|
778
751
|
.all(connectionId, this.ownerId, runId ?? null, runId ?? null);
|
|
779
752
|
for (const row of rows) {
|
|
780
753
|
const state = row.review_state_json ? JSON.parse(row.review_state_json) : undefined;
|
|
781
|
-
// Later session observations cannot rewrite why an already interrupted
|
|
782
|
-
// preparation ended. A new admitted preparation clears this error.
|
|
783
754
|
if (!row.preparation_id && row.preparation_error && !state?.transactionReviewData)
|
|
784
755
|
continue;
|
|
785
756
|
if (state) {
|
|
@@ -32,8 +32,6 @@ export function isInteractionPendingReviewStatus(status) {
|
|
|
32
32
|
return executionStatusCategory(status) !== "final";
|
|
33
33
|
}
|
|
34
34
|
export function isReviewInteractionPending({ hasReviewInput, request, progress }) {
|
|
35
|
-
// Historical preparation state is not proof that a live input remains.
|
|
36
|
-
// Consumed input, in turn, does not end its admitted request or observation.
|
|
37
35
|
return hasReviewInput || progress.status !== "idle" || !!request && isRequestWaiting(request.requestStatus);
|
|
38
36
|
}
|
|
39
37
|
export function executionPollingHint() {
|
|
@@ -115,9 +115,6 @@ export class LocalTransactionMaterialStoreError extends Error {
|
|
|
115
115
|
super(message);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
// Validate the store input and build the stored record (with cloned bytes). Shared
|
|
119
|
-
// by every LocalTransactionMaterialStore implementation so validation never drifts
|
|
120
|
-
// between the in-memory and SQLite backends.
|
|
121
118
|
export function buildTransactionMaterialRecord(input, now = new Date()) {
|
|
122
119
|
const account = parseSuiAddress(input.account);
|
|
123
120
|
if (!account) {
|
|
@@ -158,7 +155,6 @@ export function buildTransactionMaterialRecord(input, now = new Date()) {
|
|
|
158
155
|
: { redactedDiagnostics: structuredClone(input.redactedDiagnostics) })
|
|
159
156
|
};
|
|
160
157
|
}
|
|
161
|
-
// Project the public (redacted) handle out of a stored record.
|
|
162
158
|
export function toMaterialHandle(record) {
|
|
163
159
|
return {
|
|
164
160
|
materialId: record.materialId,
|
|
@@ -6,14 +6,10 @@ export const TRANSACTION_REQUEST_STATUSES = [
|
|
|
6
6
|
"request_failed", "outcome_unknown", "completed"
|
|
7
7
|
];
|
|
8
8
|
export const transactionRequestStatusSchema = z.enum(TRANSACTION_REQUEST_STATUSES);
|
|
9
|
-
// The submission was admitted durably before external I/O. A failed follow-up
|
|
10
|
-
// write must not remove its initial observation window or replacement guard.
|
|
11
9
|
export const INITIAL_CHAIN_OBSERVATION_STATUSES = ["submitting", "awaiting_chain_result"];
|
|
12
10
|
export function isInitialChainObservation(status) {
|
|
13
11
|
return INITIAL_CHAIN_OBSERVATION_STATUSES.some((value) => value === status);
|
|
14
12
|
}
|
|
15
|
-
// This is the existing signing surface's local wait, independent of protocol
|
|
16
|
-
// request expiry, card input expiry and the chain observation window.
|
|
17
13
|
export const SIGNATURE_WAIT_MS = 90_000;
|
|
18
14
|
export const CHAIN_RECEIPT_LOOKUP_MAX_AGE_MS = 10 * 60 * 1000;
|
|
19
15
|
export const transactionExecutionSummarySchema = z.object({
|
|
@@ -46,7 +46,6 @@ export function workflowProgress(waiting, requiresWallet, availability) {
|
|
|
46
46
|
export const walletConnectionStatusSchema = z.enum([
|
|
47
47
|
"awaiting_approval", "connected", "rejected", "failed", "expired", "stopped", "disconnected"
|
|
48
48
|
]);
|
|
49
|
-
// Recorded recovery facts, not proof of a currently usable wallet connection.
|
|
50
49
|
export const walletRecoveryImpactSchema = z.object({
|
|
51
50
|
runId: z.string().uuid(),
|
|
52
51
|
connections: z.array(z.object({ connectionId: z.string().min(1), revision: z.number().int().nonnegative(),
|
|
@@ -104,8 +103,6 @@ export function walletConnectionSelection(connections, at, presence, stored) {
|
|
|
104
103
|
export class WalletUserRejectedError extends Error {
|
|
105
104
|
constructor() { super("The wallet request was rejected by the user."); }
|
|
106
105
|
}
|
|
107
|
-
// Shared recorded connection facts only. Each operation checks its own expiry,
|
|
108
|
-
// account, method and revision requirements at its decision boundary.
|
|
109
106
|
export function isOwnedConnectedWallet(record, ownerId, disconnectPending) {
|
|
110
107
|
return !!record && record.ownerId === ownerId && record.connection.status === "connected" && !disconnectPending;
|
|
111
108
|
}
|
|
@@ -19,8 +19,6 @@ export class WalletWorkflow {
|
|
|
19
19
|
observing = new Map();
|
|
20
20
|
preparing = new Set();
|
|
21
21
|
pendingWrites = new Map();
|
|
22
|
-
// Child exit ends child I/O, not verification or submit work already running
|
|
23
|
-
// in this parent. The owning signature continuation alone ends that work.
|
|
24
22
|
parentSignatureWork = new Set();
|
|
25
23
|
eventListener;
|
|
26
24
|
settingsRecovery;
|
|
@@ -47,7 +45,7 @@ export class WalletWorkflow {
|
|
|
47
45
|
report(stage) { try {
|
|
48
46
|
this.options.logger.error("Wallet operation failed", { stage });
|
|
49
47
|
}
|
|
50
|
-
catch {
|
|
48
|
+
catch { } }
|
|
51
49
|
async start() {
|
|
52
50
|
this.assertCurrent();
|
|
53
51
|
if (this.started)
|
|
@@ -130,7 +128,6 @@ export class WalletWorkflow {
|
|
|
130
128
|
this.retainWrite(completion);
|
|
131
129
|
}
|
|
132
130
|
retainWrite(completion) {
|
|
133
|
-
// An old data generation cannot add repair work to the replacement data.
|
|
134
131
|
try {
|
|
135
132
|
this.assertCurrent();
|
|
136
133
|
}
|
|
@@ -173,8 +170,6 @@ export class WalletWorkflow {
|
|
|
173
170
|
failure = error;
|
|
174
171
|
}
|
|
175
172
|
}
|
|
176
|
-
// A completed callback's pending flag is housekeeping, not authority to
|
|
177
|
-
// read recorded facts. Retain failed repairs for the next applicable read.
|
|
178
173
|
if (failure && walletDependent)
|
|
179
174
|
throw failure;
|
|
180
175
|
if (walletDependent)
|
|
@@ -210,14 +205,10 @@ export class WalletWorkflow {
|
|
|
210
205
|
walletObservation: state.walletObservation, manageableConnectionIds: state.manageableConnectionIds,
|
|
211
206
|
connectionReconciliationRequired: state.connectionReconciliationRequired, assetReadAccount: state.assetReadAccount ?? { status: "address_required" } };
|
|
212
207
|
}
|
|
213
|
-
// Diagnostic facts are collected without maintenance. Reconciliation and
|
|
214
|
-
// admission own durable transitions and cannot use this snapshot as authority.
|
|
215
208
|
currentConnectionContext() {
|
|
216
209
|
this.assertCurrent();
|
|
217
210
|
const facts = this.options.records.readConnectionFacts(this.now()), availability = this.walletAvailability();
|
|
218
211
|
const { selection: _selection, ...eligibility } = workflowConnectionEligibility({ ...facts, walletAvailability: availability });
|
|
219
|
-
// The shared projection gates qualification on published availability;
|
|
220
|
-
// a memory snapshot alone does not prove its journal/result has committed.
|
|
221
212
|
return { connections: facts.connections, walletAvailability: availability, walletObservation: facts.walletObservation,
|
|
222
213
|
connectionConflict: eligibility.connectionConflict, manageableConnectionIds: eligibility.manageableConnectionIds,
|
|
223
214
|
connectionReconciliationRequired: eligibility.connectionReconciliationRequired,
|
|
@@ -231,7 +222,6 @@ export class WalletWorkflow {
|
|
|
231
222
|
const pending = this.options.records.pendingDisconnect(record.connection.connectionId);
|
|
232
223
|
if (!pending)
|
|
233
224
|
continue;
|
|
234
|
-
// Disabled transports also return undefined; that is not session absence.
|
|
235
225
|
const absent = available && !!record.topic && this.observed(record.topic)?.status === "absent";
|
|
236
226
|
this.options.records.recoverDisconnect(pending.state.cardId, record.connection.connectionId, record.connection.revision, absent);
|
|
237
227
|
}
|
|
@@ -263,8 +253,6 @@ export class WalletWorkflow {
|
|
|
263
253
|
if (explicitResult) {
|
|
264
254
|
await this.observing.get(state.request.attemptId);
|
|
265
255
|
this.assertCurrent();
|
|
266
|
-
// An awaited receipt may have committed new facts. Return one freshly
|
|
267
|
-
// evaluated snapshot; ordinary status reads do not await external I/O.
|
|
268
256
|
state = await this.evaluateReview(id, undefined, false, state.request.attemptId);
|
|
269
257
|
}
|
|
270
258
|
}
|
|
@@ -447,8 +435,6 @@ export class WalletWorkflow {
|
|
|
447
435
|
catch (error) {
|
|
448
436
|
displayFailure = error;
|
|
449
437
|
}
|
|
450
|
-
// The approval remains pending even if setup/QR recording fails or the
|
|
451
|
-
// wallet dependency is disabled. Settle only after that promise ends.
|
|
452
438
|
const session = await result.approval;
|
|
453
439
|
if (displayFailure)
|
|
454
440
|
throw displayFailure;
|
|
@@ -552,8 +538,6 @@ export class WalletWorkflow {
|
|
|
552
538
|
return;
|
|
553
539
|
}
|
|
554
540
|
catch (error) {
|
|
555
|
-
// SDK verification can itself depend on a remote source. An exception is
|
|
556
|
-
// not proof of a mismatched transaction or signer.
|
|
557
541
|
if (!this.stopped)
|
|
558
542
|
this.options.records.transitionRequest(request.attemptId, "request_failed", this.now(), {
|
|
559
543
|
reason: this.walletAvailability().status !== "available" ? "Wallet operations became unavailable before submission. The returned signature will not be submitted."
|
|
@@ -564,7 +548,7 @@ export class WalletWorkflow {
|
|
|
564
548
|
try {
|
|
565
549
|
await this.options.submitTransaction(bytes, response.signature);
|
|
566
550
|
}
|
|
567
|
-
catch {
|
|
551
|
+
catch { }
|
|
568
552
|
finally {
|
|
569
553
|
this.completeCallback({ kind: "request", id: request.attemptId, field: "submit_pending" });
|
|
570
554
|
}
|
|
@@ -585,8 +569,6 @@ export class WalletWorkflow {
|
|
|
585
569
|
const current = this.options.records.request(id);
|
|
586
570
|
if (!current || !(isInitialChainObservation(current.requestStatus) || explicit && current.requestStatus === "outcome_unknown"))
|
|
587
571
|
return;
|
|
588
|
-
// Starting another lookup needs its own previous callback to be settled.
|
|
589
|
-
// Stored terminal facts above never depend on this write succeeding.
|
|
590
572
|
for (const [key, write] of this.pendingWrites)
|
|
591
573
|
if (write.kind === "request" && write.id === id && write.field === "lookup_pending") {
|
|
592
574
|
this.settlePendingWrite(key, write);
|
|
@@ -607,7 +589,7 @@ export class WalletWorkflow {
|
|
|
607
589
|
if (receipt.status === "found")
|
|
608
590
|
details = projectReadCardResult("receipt", { digest: request.transactionDigest }, receipt);
|
|
609
591
|
}
|
|
610
|
-
catch {
|
|
592
|
+
catch { }
|
|
611
593
|
}
|
|
612
594
|
this.assertCurrent();
|
|
613
595
|
this.options.records.transitionRequest(id, "completed", this.now(), { execution: {
|
|
@@ -2,8 +2,6 @@ import { walletConnectionSelection } from "./walletConnection.js";
|
|
|
2
2
|
import { isInitialChainObservation } from "./transactionRequest.js";
|
|
3
3
|
export const workflowActions = ["connect", "disconnect", "use_account", "stop_connection", "prepare_review",
|
|
4
4
|
"request_signature", "cancel", "stop_waiting", "read_result"];
|
|
5
|
-
// Read context may change; an existing review's account binding may not.
|
|
6
|
-
// Both evaluated choices and admission consume this relation with current facts.
|
|
7
5
|
export function reviewPreparationAccount(boundAccount, activeAccount, selectedAccount = activeAccount) {
|
|
8
6
|
if (!activeAccount)
|
|
9
7
|
return { allowed: false, message: "No account from a connected wallet is selected for this review." };
|
|
@@ -23,18 +21,12 @@ export function workflowConnectionEligibility(facts) {
|
|
|
23
21
|
: usableConnectionId ? selection.assetReadAccount : { status: "address_required" };
|
|
24
22
|
return { selection, usableConnectionId, assetReadAccount, connectionConflict: selection.conflict,
|
|
25
23
|
manageableConnectionIds: available ? selection.manageableConnectionIds : [],
|
|
26
|
-
// A pending memory snapshot is not a published SDK observation. Startup
|
|
27
|
-
// and state-sync failures retain their own result and neutral diagnosis.
|
|
28
24
|
connectionReconciliationRequired: available && dependent ? selection.connectionReconciliationRequired : undefined };
|
|
29
25
|
}
|
|
30
|
-
// Shared domain eligibility. The DB supplies current facts at its decision
|
|
31
|
-
// boundary. Returned choices are advisory; admission still binds concrete input.
|
|
32
26
|
export function workflowEligibility(facts) {
|
|
33
27
|
const { record, session, request, authority, walletAvailability } = facts;
|
|
34
28
|
const at = Date.parse(facts.evaluatedAt);
|
|
35
29
|
const cardRemaining = record?.ownerId === facts.ownerId ? Math.max(0, Date.parse(record.state.expiresAt) - at) : 0;
|
|
36
|
-
// A later-created input card cannot extend its review session. Admitted
|
|
37
|
-
// requests and management cards retain their separate observation authority.
|
|
38
30
|
const remaining = record?.scope === "review" && session && !request
|
|
39
31
|
? Math.min(cardRemaining, Math.max(0, Date.parse(session.expiresAt) - at)) : cardRemaining;
|
|
40
32
|
const inputAvailable = remaining > 0 && record?.state.state === "ready" && record.acceptedInput === undefined;
|
|
@@ -9,7 +9,6 @@ import { reviewProgress, EXECUTION_POLLING_INTERVAL_SECONDS } from "./status.js"
|
|
|
9
9
|
import { walletRunIdSchema, walletRecoverySchema, walletObservationSchema } from "./walletRuntime.js";
|
|
10
10
|
export const CONNECT_BOUNDARY = "A wallet connection provides account context; it is not a transaction approval or proof of address ownership.";
|
|
11
11
|
export const REVIEW_BOUNDARY = "Review evidence is not a safety guarantee. Only an explicit card action and wallet approval may authorize this exact transaction.";
|
|
12
|
-
// A projection of an admitted card operation, not a new wallet connection state.
|
|
13
12
|
export const connectionViewSchema = walletConnectionSchema.extend({ pendingAction: z.literal("disconnect").optional() });
|
|
14
13
|
export { assetReadAccountSchema } from "./walletConnection.js";
|
|
15
14
|
export const pendingConnectionStatusSchema = z.enum(["input_required", "awaiting_approval", "disconnect_pending"]);
|
|
@@ -80,8 +79,6 @@ export const workflowViewSchema = z.object({
|
|
|
80
79
|
context.addIssue({ code: "custom", path: ["assetReadAccount"], message: "The default address must belong to the usable connection and stored selection." });
|
|
81
80
|
}
|
|
82
81
|
});
|
|
83
|
-
// Automatic preparation uses the same current selection and typed admission as
|
|
84
|
-
// a deliberate retry. A failed computation is not a reason to compute in a loop.
|
|
85
82
|
function automaticReviewAction(input) {
|
|
86
83
|
const { record, session, request } = input;
|
|
87
84
|
if (!session || record?.scope !== "review" || request || session.preparationError ||
|
|
@@ -98,7 +95,6 @@ function automaticReviewAction(input) {
|
|
|
98
95
|
return { action: "prepare_review", walletRunId: input.walletAvailability.walletRunId, connectionId: candidates[0].connectionId,
|
|
99
96
|
account, reviewRevision: session.reviewRevision };
|
|
100
97
|
}
|
|
101
|
-
// Method support filters wallet choices; it does not authorize a signature.
|
|
102
98
|
export function reviewWalletChoices(selection, account, requireSigningMethod = false) {
|
|
103
99
|
return selection.connections.filter((item) => item.connectionId === selection.usableConnectionId && item.accounts.includes(account) &&
|
|
104
100
|
(!requireSigningMethod || item.methods.includes("sui_signTransaction")));
|
|
@@ -112,8 +108,6 @@ function nextStateRead(input) {
|
|
|
112
108
|
.filter((value) => value !== undefined && value > 0);
|
|
113
109
|
return delays.length ? Math.min(...delays) : undefined;
|
|
114
110
|
}
|
|
115
|
-
// These projections do not read clocks, storage, or admission rules. The
|
|
116
|
-
// evaluated facts and choices were collected together by the database owner.
|
|
117
111
|
export function projectConnectionView(input) {
|
|
118
112
|
const { walletAvailability } = input;
|
|
119
113
|
const stored = input.record?.state.data;
|
package/dist/core/suiAddress.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { isValidSuiAddress, isValidTransactionDigest, normalizeSuiAddress } from "@mysten/sui/utils";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
// Use the string-only schema at MCP/HTTP output boundaries because zod transforms
|
|
4
|
-
// cannot be represented in MCP JSON Schema. Use the normalized schema for values
|
|
5
|
-
// before storing them as local session state.
|
|
6
3
|
export const suiAddressStringSchema = z
|
|
7
4
|
.string()
|
|
8
5
|
.refine(isValidSuiAddress, "Expected a 32-byte Sui address");
|
|
9
|
-
// Shared Sui transaction digest schema (pinned SDK source of truth). Used at
|
|
10
|
-
// MCP/HTTP boundaries and by the signable-adapter commitment fields.
|
|
11
6
|
export const suiTransactionDigestSchema = z
|
|
12
7
|
.string()
|
|
13
8
|
.refine(isValidTransactionDigest, "Expected a Sui transaction digest");
|
package/dist/core/suiEndpoint.js
CHANGED
|
@@ -35,9 +35,6 @@ export function parseGrpcUrl(value) {
|
|
|
35
35
|
}
|
|
36
36
|
return `${parsed.protocol}//${authority}`;
|
|
37
37
|
}
|
|
38
|
-
// GraphQL endpoints commonly include a path such as /graphql and may omit an
|
|
39
|
-
// explicit port. gRPC endpoints stay stricter because the pinned gRPC client
|
|
40
|
-
// runtime expects only scheme, host, and explicit port.
|
|
41
38
|
export function parseGraphqlUrl(value) {
|
|
42
39
|
let parsed;
|
|
43
40
|
if (value.length > MAX_SUI_GRAPHQL_URL_LENGTH) {
|
package/dist/mcp/prompts.js
CHANGED
|
@@ -24,9 +24,6 @@ export const MCP_PROMPTS = [
|
|
|
24
24
|
].join("\n")
|
|
25
25
|
}
|
|
26
26
|
];
|
|
27
|
-
// Platform-owned boundary language appended to every adapter prompt surface.
|
|
28
|
-
// Adapters contribute copy about their own action only; they cannot weaken
|
|
29
|
-
// or omit these lines.
|
|
30
27
|
const PLATFORM_PROMPT_BOUNDARY_LINES = [
|
|
31
28
|
"Present the internal Review card and summarize the returned review checks.",
|
|
32
29
|
"This MCP response never contains signing data, transaction bytes, or signing readiness; only an explicit UI action and wallet approval authorize the backend request; request status is separate from server-verified chain execution facts."
|
|
@@ -76,18 +73,9 @@ export function registerMcpPrompts(server, surfaces = []) {
|
|
|
76
73
|
]
|
|
77
74
|
}));
|
|
78
75
|
}
|
|
79
|
-
// Adapter prompt surfaces: action-first names (`swap-deep`) with one
|
|
80
|
-
// free-text intent argument so clients can pass the whole request in one
|
|
81
|
-
// line (Claude Code: /mcp__sui-mcp__swap-deep 10 sui to usdc;
|
|
82
|
-
// Claude Desktop shows a single input field). The model parses the
|
|
83
|
-
// intent; this server never does.
|
|
84
76
|
for (const surface of surfaces) {
|
|
85
77
|
registerSurfacePrompt(server, promptNameFor(surface), surface, surface.description);
|
|
86
78
|
}
|
|
87
|
-
// Bare action prompts are always registered. One protocol: straight to it.
|
|
88
|
-
// Several protocols: an optional `protocol` argument (completion suggests
|
|
89
|
-
// the slugs) and explicit instructions to ask the user - the venue is the
|
|
90
|
-
// user's choice, never the server's.
|
|
91
79
|
for (const [action, group] of actionGroups(surfaces)) {
|
|
92
80
|
registerBareActionPrompt(server, action, group);
|
|
93
81
|
}
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
// No-parameter tools. The `.optional()` is load-bearing: it lets the tool be called with
|
|
3
|
-
// no arguments at all (undefined args validate). A bare `{}`/`z.object({})` would instead
|
|
4
|
-
// REQUIRE an (empty) object and reject an argument-less call, so this is not a no-op.
|
|
5
2
|
export const noParamsInputSchema = z.object({}).optional();
|
|
6
3
|
export function successOutputSchema(dataShape) {
|
|
7
4
|
return {
|
|
@@ -32,12 +32,6 @@ export function registerActionTools(server, deps) {
|
|
|
32
32
|
if (!supportsCards(server))
|
|
33
33
|
return errorToolResult({ kind: "ui_unavailable", details: { reason: "An internal review card is required." } });
|
|
34
34
|
const now = new Date();
|
|
35
|
-
// A swap review is account-bound: its evidence (balances, transaction
|
|
36
|
-
// material, digest, simulation) is computed for a specific sender, and
|
|
37
|
-
// no transaction can be built without that account. Refuse here when no
|
|
38
|
-
// wallet account is connected instead of creating a hollow proposal that
|
|
39
|
-
// can never be computed or signed. Connect first via
|
|
40
|
-
// session.create_wallet_connection.
|
|
41
35
|
let activeAccount;
|
|
42
36
|
try {
|
|
43
37
|
activeAccount = await deps.activityStore.getActiveAccount();
|
|
@@ -5,7 +5,6 @@ import { MCP_PROMPTS } from "../../prompts.js";
|
|
|
5
5
|
import { MCP_RESOURCES } from "../../resources.js";
|
|
6
6
|
import { EVIDENCE_POLICY, FAIL_CLOSED_TOOLS, IMPLEMENTED_TOOLS, PACKAGE_NAME, SERVER_LIMITATIONS, SERVER_NAME, SERVER_NETWORK, SERVER_RUNTIME, SERVER_TRANSPORT, SERVER_VERSION } from "../../serverInfo.js";
|
|
7
7
|
import { TOOL_NAMES } from "../../toolNames.js";
|
|
8
|
-
// Executable swap adapters precede protocol-notes-only entries.
|
|
9
8
|
export const SUPPORTED_PROTOCOLS = [
|
|
10
9
|
{ id: "deepbook-v3", status: "mainnet", support: "read_and_local_review" },
|
|
11
10
|
{ id: "deepbook-margin", status: "mainnet", support: "protocol_notes_only" }
|
|
@@ -27,8 +27,6 @@ export function latest(items) {
|
|
|
27
27
|
const sorted = [...items].sort((left, right) => right.lastActivityAt.localeCompare(left.lastActivityAt));
|
|
28
28
|
return { limit: INTERACTION_STATUS_LIMIT, items: sorted.slice(0, INTERACTION_STATUS_LIMIT), truncated: sorted.length > INTERACTION_STATUS_LIMIT };
|
|
29
29
|
}
|
|
30
|
-
// Runtime workflows own reconciliation/observation. A server without a wallet
|
|
31
|
-
// workflow can still expose ordinary preparation facts, with explicit absence.
|
|
32
30
|
export async function readCurrentReview(deps, id, explicitResult = false) {
|
|
33
31
|
if (deps.workflow)
|
|
34
32
|
return deps.workflow.readReview(id, explicitResult);
|
|
@@ -34,8 +34,6 @@ export function registerSessionStatusTools(server, deps) {
|
|
|
34
34
|
return activityStoreToolError(error, deps.logger);
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
// Connection reconciliation can invalidate reviews and interrupt requests.
|
|
38
|
-
// Complete that maintenance before projecting its dependent review facts.
|
|
39
37
|
const context = deps.workflow?.reconcileConnectionContext() ?? { connections: [], manageableConnectionIds: [],
|
|
40
38
|
assetReadAccount: { status: "address_required" }, walletAvailability: walletUnavailable("initialization_failed") };
|
|
41
39
|
const connections = deps.workflow?.pendingConnections() ?? [];
|