Skip to content

feat(smart-sessions): OneTimeUseId session wiring + burn-op helpers (RHI-5798) - #792

Open
highskore wants to merge 5 commits into
mainfrom
feat/rhi-5798-sdk-onetime
Open

feat(smart-sessions): OneTimeUseId session wiring + burn-op helpers (RHI-5798)#792
highskore wants to merge 5 commits into
mainfrom
feat/rhi-5798-sdk-onetime

Conversation

@highskore

Copy link
Copy Markdown
Contributor

What

SDK support for one-time-use sessions (RHI-5798, smart-sessions-v2#56). A session pins an id via oneTimeUse: { id } (requires policyAddresses.oneTimeUseId) and settles at most once per chain, across both settlement routes. Pairs with the orchestrator witness-stamping (orchestrator#1959).

Wiring — the policy is installed where each route actually enforces it

Matching the on-chain policy's install requirements and the E2E harness (OneTimeUseIdMatrixE2E):

  • Executor route validates via action policies (checkAction), not the 1271 list — so the once-policy is added to every action of the session. (An action without it falls through to the permissive sudo fallback → unbounded settlement.)
  • Permit2/arbiter route validates via the 1271 list (checkERC1271) — so the Permit2 claim policy (the digest-binding partner) is co-located with the once-policy on that list (an AND: claim policy binds what settles, once-policy binds how many times). claimPolicies moves onto that surface.

Each settlement carries the burn op (buildOneTimeUseBurnOp) in preClaimExecutions: consume(id) (executor) or consumeFor(id, 0) (permit2 — the orchestrator stamps the real order nonce as the witness before signing).

Changes

  • New one-time-use.ts — ABI + encodeOneTimeUseIdInitData + oneTimeUseIdErc1271Policy + buildOneTimeUseBurnOp.
  • resolveSessionData — once-policy on every action + Permit2-claim/once on the 1271 list for one-time-use sessions.
  • oneTimeUse / oneTimeUseId exposed on the public SessionDefinition / SessionPolicyAddresses; helpers exported from @rhinestone/sdk/smart-sessions.
  • Tests assert the policy lands on every action and the 1271 list, that claimPolicies is emptied, and that a normal session is untouched.

Review

A 3-reviewer local gate caught (and this PR fixes) a CRITICAL — the first cut only appended the policy to the 1271 list, leaving the executor route unbounded — and a HIGH — the permit2 binding partner was on the wrong surface. Verified against the deployed policy + the E2E harness. Encoding (id→bytes32), the consumeFor(id,0) placeholder's fail-closed behavior, and permissionId-invariance were all confirmed sound.

Closes RHI-5798 (SDK portion)

🤖 Generated with Claude Code

https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ

@highskore highskore added the show Same as ship; a human eye would be appreciated label Aug 24, 2026
@highskore
highskore requested a lite review from Copilot August 24, 2026 11:41

@rhinestone-kevin rhinestone-kevin 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.

Request changes — 1 blocker in one-time-use session data.

  • design-smell — CLAUDE.md says every fix or feature needs a changeset, and public export additions require a minor changeset; this PR adds oneTimeUse session config and new @rhinestone/sdk/smart-sessions exports without a .changeset/*.md, so the released SDK will not be versioned or changeloged for the new API.

Kevin score: 6/10 · impact 6 · economy 0

Comment thread src/modules/validators/smart-sessions/resolve.ts

Copilot AI 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.

Pull request overview

Adds SDK support for one-time-use smart sessions by wiring the OneTimeUseId policy into smart-session resolution and exposing helper utilities under @rhinestone/sdk/smart-sessions.

Changes:

  • Introduces one-time-use.ts helpers (ABI, encodeOneTimeUseIdInitData, oneTimeUseIdErc1271Policy, buildOneTimeUseBurnOp) and exports them from the smart-sessions entrypoint.
  • Updates resolveSessionData to install the once-policy on every action and relocate Permit2 claim policies onto the ERC-1271 policy surface for one-time-use sessions.
  • Adds unit tests covering the one-time-use wiring and updates public/session config types to include oneTimeUse + policyAddresses.oneTimeUseId.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/smart-sessions/index.ts Re-exports the one-time-use helpers/types from the smart-sessions public entrypoint.
src/modules/validators/smart-sessions/types.ts Extends SessionDefinition/SessionPolicyAddresses to model one-time-use session configuration.
src/modules/validators/smart-sessions/resolve.ts Implements the one-time-use wiring (once-policy on every action; Permit2 claim policies moved onto ERC-1271 surface).
src/modules/validators/smart-sessions/resolve.one-time-use.test.ts Adds example-based tests validating one-time-use wiring in resolveSessionData.
src/modules/validators/smart-sessions/policies/addresses.ts Adds optional oneTimeUseId policy address override (no default).
src/modules/validators/smart-sessions/one-time-use.ts New module implementing the OneTimeUseId policy ABI + encoding/burn-op helpers.
src/modules/validators/smart-sessions/one-time-use.test.ts Adds tests for initData encoding, policy entry generation, and burn-op construction.
src/config/account.ts Mirrors the public config surface updates (adds oneTimeUse + oneTimeUseId docs/fields).

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/modules/validators/smart-sessions/one-time-use.ts Outdated
Comment thread src/modules/validators/smart-sessions/resolve.ts
Comment thread src/modules/validators/smart-sessions/types.ts
Comment thread src/smart-sessions/index.ts Outdated
SDK support for one-time-use sessions (RHI-5798): a session pins an id via
`oneTimeUse: { id }` (requires `policyAddresses.oneTimeUseId`) and settles at most
once per chain, across both settlement routes.

The policy is installed where each route actually enforces it, matching the
on-chain policy's install requirements and the E2E harness:
  - executor route (checkAction): the once-policy is added to EVERY action's
    policies — an action without it would fall through to the sudo fallback and
    settle unbounded.
  - permit2/arbiter route (checkERC1271): the Permit2 claim policy is co-located
    with the once-policy on the 1271 list (an AND — the claim policy binds WHAT
    settles, the once-policy binds HOW MANY TIMES), so claimPolicies moves onto
    that surface.

Each settlement carries the burn op (buildOneTimeUseBurnOp) in preClaimExecutions:
consume(id) on the executor route, consumeFor(id, 0) on the permit2 route (the
orchestrator stamps the real order nonce as the witness before signing).

Exposes buildOneTimeUseBurnOp / oneTimeUseIdErc1271Policy / encodeOneTimeUseIdInitData
and the `oneTimeUse` session option on the public API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ

@rhinestone-kevin rhinestone-kevin 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.

Approve — adds one-time-use smart-session wiring; no blockers found.


Kevin score: 6/10 · impact 6 · economy 0

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread src/modules/validators/smart-sessions/resolve.ts Outdated
A one-time-use session enforces its Permit2 claim policy via the ERC-1271
policy list, so the on-chain claim (lockTag) surface must stay empty. The
previous cut emptied the high-level `Session.claimPolicies` to achieve that,
but the permit2 settlement signature builds the claim policy's calldata from
that same list (claimPolicyData in session-signing), so it shipped an empty
policy-data segment and the erc1271-resident claim policy had nothing to
validate against.

Keep the raw claim policies on the high-level session (as a normal permit2
session does) so the signature calldata is built, and add
`claimPoliciesEnforcedVia1271` so getSessionData omits them from the on-chain
claim surface instead. Result: enforced once via erc1271, encoded once
on-chain, and the permit2 route settles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ

@rhinestone-kevin rhinestone-kevin 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.

Request changes — 1 blocker in one-time-use executor verification.


Kevin score: 6/10 · impact 6 · economy 0

Comment thread src/modules/validators/smart-sessions/resolve.ts

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/modules/validators/smart-sessions/one-time-use.ts:69

  • buildOneTimeUseBurnOp does not validate that id is a valid non-zero uint256 (even though encodeOneTimeUseIdInitData/session wiring enforce that invariant). Adding the same validation here makes the helper fail fast and avoids producing burn ops that are guaranteed to revert.
  const { policy, id, route } = params
  const data =

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread src/modules/validators/smart-sessions/one-time-use.ts
Reject a zero id in buildOneTimeUseBurnOp (not just in encodeOneTimeUseIdInitData)
so a caller can't emit a burn op whose id can never match a validly-pinned session
(the policy rejects id 0 as "not configured"). Extracted a shared assertValidOneTimeUseId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ

@rhinestone-kevin rhinestone-kevin 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.

Approve — adds one-time-use smart-session wiring; no blockers found.


Kevin score: 6/10 · impact 6 · economy 0

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/modules/validators/smart-sessions/digest.ts:43

  • getSessionData() omits claimPolicies only when session.claimPoliciesEnforcedVia1271 is truthy. Because this flag is optional and not part of the public Session type in src/config/account.ts (see interface Session around lines 592-603), consumers can easily construct/deserialize a Session without it and accidentally re-encode Permit2 claim policies onto the lockTag surface (breaking the intended one-time-use wiring). Consider inferring enforcement from erc7739Policies.erc1271Policies as a fallback (e.g., detect PERMIT2_CLAIM_POLICY_ADDRESS on the 1271 list).
    // When the claim policies are enforced via the erc1271 surface they're
    // already in erc7739Policies; encoding them here too would settle them on
    // the on-chain claim (lockTag) surface as well.
    claimPolicies: session.claimPoliciesEnforcedVia1271
      ? []

src/config/account.ts:559

  • The new JSDoc uses {@link buildOneTimeUseBurnOp}, but buildOneTimeUseBurnOp isn’t in scope in this module (no import and no local declaration), so reference generation/linking is likely to produce an unresolved/broken link. Prefer plain inline code formatting here (or import the symbol if that’s safe for the declaration closure).
   * Pins a one-time-use id on the session (RHI-5798): the session settles at most
   * once per chain. Requires `policyAddresses.oneTimeUseId`. Each settlement must
   * carry the matching burn op ({@link buildOneTimeUseBurnOp}) in its
   * `preClaimExecutions`; a Permit2-route session must also supply a

…inding

Executor-only one-time-use now asserts the 1271 list collapses to exactly
[once] (the permissive sudo entry is replaced, no claim policy) while the burn
still lands on every action. Also pins the documented invariant that the pinned
id does not flow into the permissionId (it's authorized via the enable-signed
config), so same-owner one-time-use sessions must not be installed concurrently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ

@rhinestone-kevin rhinestone-kevin 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.

Request changes — 1 blocker in steady-state one-time-use executor enforcement.


Kevin score: 6/10 · impact 6 · economy 0

Comment thread src/modules/validators/smart-sessions/resolve.ts

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/modules/validators/smart-sessions/one-time-use.ts:55

  • oneTimeUseIdErc1271Policy is used for both ERC-1271 policy entries and action policy entries (see resolveSessionData appending once to action.actionPolicies). The Erc1271-specific name is misleading for consumers and makes it easy to misuse/overlook in the executor route wiring. Consider renaming to a surface-agnostic helper name (e.g. oneTimeUseIdPolicyEntry / oneTimeUseIdPolicyData) and updating re-exports + changeset accordingly.
// The erc1271 policy entry to add to a session's erc7739Policies.erc1271Policies.
export function oneTimeUseIdErc1271Policy(params: {
  policy: Address
  id: bigint
}): { readonly policy: Address; readonly initData: Hex } {

src/modules/validators/smart-sessions/resolve.one-time-use.test.ts:12

  • The comment claims fast-check is "declared in package.json but not installed in every working copy", but fast-check is a devDependency (and is used by several test files). This wording can confuse contributors since a standard dev install will include it. Consider rephrasing to focus on why this file is separated (keeping tests example-based / lightweight) without implying the dependency is generally unavailable.
// Kept out of resolve.test.ts because that file imports fast-check (declared in
// package.json but not installed in every working copy); these are plain
// example-based tests for the OneTimeUseId wiring (RHI-5798).

… the executor route

An already-enabled one-time-use session with no explicit permissions dropped to
signatureMode 1 (plain ERC-1271), where the emissary never runs checkAction. Replay
of a burned id stays blocked (the 1271 advisory read `!$used` reverts the executor's
require), but the policy's action-surface guard — a `consume` may only name the
session's own id, the one thing the executor route enforces on-chain — was inert.

Carry a `oneTimeUse` marker on the resolved Session and OR it into the verifyExecutions
derivation so a one-time-use session always validates in mode 5 (checkAction active),
matching the contract's design (OneTimeUseIdPolicy checkAction + the install-on-every-
action requirement). Confirmed safe for the permit2 route: mode 5 is a superset of mode
1 and verifyExecution is onlyIntentExecutor, so the arbiter path is unaffected. Also
corrects the resolve.ts comment that mis-described the runtime enforcement path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ

Copilot AI 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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

@rhinestone-kevin rhinestone-kevin 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.

Approve — adds one-time-use smart-session wiring; no blockers found.


Kevin score: 6/10 · impact 6 · economy 0

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

Labels

show Same as ship; a human eye would be appreciated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants