Skip to content

feat(cli): share durable Tempo session state - #713

Open
gakonst wants to merge 7 commits into
mainfrom
fix/cli-shared-tempo-wallet-state
Open

feat(cli): share durable Tempo session state#713
gakonst wants to merge 7 commits into
mainfrom
fix/cli-shared-tempo-wallet-state

Conversation

@gakonst

@gakonst gakonst commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What this does

This PR moves durable TIP-1034 client session storage and administration out of the CLI-private implementation and into the public Node APIs. The MPPx CLI then consumes those same APIs for normal paid requests and mppx sessions commands.

Shared Node session APIs

  • expands mppx/client/node with the payer-scoped createSqliteChannelStore and createSessionAdministration APIs
  • defaults to ~/.tempo/wallet/channels.db and remains compatible with existing v2 channels rows
  • isolates reusable channels by payer, service origin, and payment scope
  • adds cross-process SQLite leases so concurrent clients serialize work for the same payer/payment scope
  • retains superseded and recovered channels so callers can inspect or explicitly select them

CLI behavior

  • uses the shared SQLite store for local/private-key accounts instead of maintaining a second CLI session registry
  • preserves --session auto, --session new, and explicit --session <channel-id> selection
  • uses the normal server bootstrap flow for cold-client rehydration
  • provides mppx sessions list, view, sync, and close
  • discovers unknown payer channels from ChannelOpened logs and reconciles retained rows with canonical precompile state
  • supports cooperative close as well as on-chain request-close/finalize flows
  • requires --yes for sessions close --all

Durable-state safety

  • only deletes local state after a cooperative close returns an authoritative MPP receipt
  • only advances request-close state after a successful transaction receipt and on-chain readback
  • only deletes a finalized session after successful withdrawal and confirmation that the channel is closed

Scope boundaries

  • this does not parse or manage Tempo Wallet store.json
  • this does not implement wallet login or access-key lifecycle management
  • tempo:* accounts remain on the existing Tempo Wallet plugin path; local/private-key accounts use the shared Node SQLite path
  • MPPx does not add a second rehydration protocol or session registry: recovery uses normal session bootstrap plus canonical on-chain state

The SQLite-backed Node APIs and persistent session CLI paths require Node.js 22.5 or newer. Other CLI commands retain the package's existing Node.js baseline.

Dependency follow-up included here

  • replaces the expired Viem PR preview with Viem 2.55.8
  • raises the Viem peer floor to >=2.55.7, the first published release containing the Tempo relay signature fix
  • rejects non-primitive subscription authorization signatures before encoding
  • resolves the current brace-expansion audit advisory

Validation

  • pnpm check:ci
  • pnpm check:types
  • pnpm build
  • pnpm check:types:examples
  • VITE_TEMPO_NETWORK=none pnpm exec vp test run src/client/node.test.ts --project node
  • VITE_TEMPO_NETWORK=none pnpm exec vp test run src/cli/sessions/request.test.ts src/cli/mcp.test.ts --project cli
  • focused CLI regressions for Tempo Wallet routing, explicit channel selection, and sessions close --all --yes

The latest fix commit is signed and verified.

@pkg-pr-new

pkg-pr-new Bot commented Jul 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/mppx@713

commit: ad5fa3e

@gakonst
gakonst force-pushed the fix/cli-shared-tempo-wallet-state branch from e767e35 to 5dcf969 Compare July 24, 2026 21:52
@gakonst gakonst changed the title fix(cli): share Tempo Wallet state feat(cli): share Tempo Wallet and durable session state Jul 24, 2026
Comment thread src/cli/TempoWallet.ts Outdated
Comment thread .changeset/calm-sessions-share.md
@gakonst
gakonst force-pushed the fix/cli-shared-tempo-wallet-state branch from f5c2847 to 07a3106 Compare July 25, 2026 02:02
@gakonst gakonst changed the title feat(cli): share Tempo Wallet and durable session state feat(cli): share durable Tempo session state Jul 25, 2026
@gakonst
gakonst force-pushed the fix/cli-shared-tempo-wallet-state branch from 07a3106 to 95b59a8 Compare July 25, 2026 02:07
@gakonst
gakonst force-pushed the fix/cli-shared-tempo-wallet-state branch from 95b59a8 to b3c198b Compare July 25, 2026 02:07
@socket-security

socket-security Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedviem@​2.55.89710010098100
Updatedox@​0.14.29 ⏵ 0.14.32991009997 +1100

View full report

@brendanjryan

Copy link
Copy Markdown
Collaborator

@codex review

const signature = await account.sign({
hash: KeyAuthorization.getSignPayload(authorization),
})
const signatureEnvelope = SignatureEnvelope.from(signature)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread src/cli/cli.ts
url: z.string().optional().describe('Override the stored resource URL'),
yes: z.boolean().optional().default(false).describe('Confirm closing every session'),
}),
output: z.object({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad5fa3e402

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/node.ts
Comment on lines +775 to +777
if (selection.orphaned || selection.finalize || (selection.all && !selection.cooperative))
await discover()
const selected = selectSessions(list(), selection)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile closing rows before filtering finalizable sessions

When a grace period elapses, the persisted row remains closing until reconcile() runs, but --finalize calls only discover() and then filters for rows already marked finalizable. Since discovery skips existing channel IDs, a channel placed into closing by an earlier request-close is silently omitted from every later bulk finalize unless the user separately runs sessions sync first. Reconcile closing candidates before applying the finalizable filter.

Useful? React with 👍 / 👎.

Comment thread src/client/node.ts
Comment on lines +679 to +680
const challenge = Challenge.fromResponseList(challengeResponse).find(isTempoSessionChallenge)
if (!challenge || !challengeMatchesSession(challenge, record))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match every returned challenge before rejecting cooperative close

If a service returns multiple tempo/session challenges, this selects the first one and only afterward checks whether it matches the retained channel. A first offer for another currency, recipient, chain, escrow, or protocol therefore causes cooperative close to fail even when a later offer matches exactly. Search the challenge list using both the session predicate and challengeMatchesSession instead.

Useful? React with 👍 / 👎.

Comment on lines +85 to +87
return withAdministration(options, (administration) =>
administration.list().map(serializeSession),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge List all chains when no network filter is requested

Without --network or an RPC override, withAdministration creates a mainnet client, and createSessionAdministration().list() always filters rows to that client's chain ID. Consequently plain mppx sessions list hides every retained testnet session, and the same chain binding means an unqualified sessions close --all --yes does not actually consider all stored sessions. Read the store directly for listing, or apply the chain filter only when the caller explicitly selected a network.

Useful? React with 👍 / 👎.

Comment thread src/client/node.ts
Comment on lines +356 to +358
const scopeKey = record.origin
? scopedKey(payer, normalizeOrigin(record.origin || namespace), entryKey(record.entry))
: null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve preferred scope ownership during reconciliation

When multiple retained channels share an origin and payment scope, superseded rows intentionally have scope_key = NULL, but setSession() reconstructs a non-null key for every row with an origin. During sync(), reconciliation processes all retained rows and each update clears the other scope owner, so the least-recently-used superseded channel processed last becomes the channel returned by subsequent automatic get() calls. Preserve whether the row owned the scope instead of promoting every reconciled record.

Useful? React with 👍 / 👎.

Comment on lines 253 to +254
const signature = authorization.signature
if (!signature || signature.type === 'keychain') {
if (!signature) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject non-primitive signatures during verification

A client that constructs and serializes a key authorization directly can still submit a keychain or multisig signature: the signing helper rejects those envelope types, but verification now checks only that a signature exists and passes the non-primitive envelope to address recovery. This regresses the previous server-side rejection and allows credentials that violate the primitive-signature requirement to be accepted; apply the same envelope-type check here as in signSubscriptionKeyAuthorization().

Useful? React with 👍 / 👎.

Comment thread src/client/node.ts
Comment on lines 128 to 130
export function defaultChannelDatabasePath(): string {
return join(homedir(), '.tempo', 'wallet', 'channels.db')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Migrate the released CLI session registry

Upgrading switches persistence from the released ~/.local/state/mppx/sessions/v1 registry to ~/.tempo/wallet/channels.db, while the old registry implementation is deleted and no remaining code imports its records. Server bootstrap only helps when the original service remains reachable, and CLI discovery scans just the latest 100,000 blocks, so older retained channels can disappear from list/close and leave their deposits inaccessible through the CLI after this patch update. Add a one-time migration or fallback reader before abandoning the old state path.

AGENTS.md reference: AGENTS.md:L152-L154

Useful? React with 👍 / 👎.

Comment thread src/client/node.ts
Comment on lines +668 to +672
const signingAccount = resolveAccount
? await resolveAccount(record.entry.descriptor.authorizedSigner)
: account
if (!canSignDescriptor(signingAccount, record.entry.descriptor))
throw new Error('Resolved session signer does not control the configured payer account.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow payer-only accounts to perform on-chain recovery

The voucher-authority check runs before the cooperative/on-chain branch, so requestClose and withdraw are rejected when the configured payer no longer controls the descriptor's delegated authorizedSigner. Those on-chain helpers submit descriptor-only transactions from the payer and do not create a voucher signature, making this delegated-key check unnecessary for the escape hatch; otherwise losing an access key also prevents the payer from recovering the channel deposit. Restrict canSignDescriptor to cooperative close and validate only payer control for on-chain operations.

Useful? React with 👍 / 👎.

Comment thread src/client/node.ts
Comment on lines 1000 to 1002
if (!row.descriptor_json) throw new Error('v2 channel row is missing its descriptor')
const descriptor = JSON.parse(row.descriptor_json) as ChannelEntry['descriptor']
return {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify stored descriptors derive their channel IDs

Rows from the shared SQLite database are trusted by casting channel_id and parsing descriptor_json without checking that the descriptor, chain, and escrow derive that ID. Reconciliation reads state using the stored ID, but requestCloseOnChain() and withdrawOnChain() operate on the descriptor, so a malformed or partially updated shared row can pass the state check and then administer a different channel. Validate the computed channel ID before exposing or administering the record.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants