chat(): sign the state of a suspended run so a resume without persistence cannot change it
メンテナーはふだん 1 日以内に返信
@AlemTuzlak がすでに取り組んでいます。
2026年10月13日 から。
評価
この issue はまだ評価されていません。
説明
Without persistence, chat() trusts the client for the state of a suspended run. Sign that state on the server when the run suspends, and verify it when the run resumes.
Problem
A run that ends with outcome: interrupt waits for the client to send answers. With no persistence middleware, the server keeps nothing between the two requests. The resume reads this state back from the client:
- The pending tool calls and their arguments.
chat()rebuilds them from the message history in the request. - The subagent router plan on
SUBAGENT_STARTEDmetadata: agent names, order, and (in #1722) each agent's input.
Nothing in this state is signed. The interrupt binding under tanstack:interruptBinding holds SHA-256 hashes of the tool schemas (defaultInterruptHash in packages/ai/src/interrupt-serialization.ts). The hash has no key. It finds a schema that changed between the two requests. It does not find a client that changed the state.
Effects:
- A client can change the arguments of a pending tool call after the user saw the approval prompt.
- A client can change which agents of the chat a routed resume runs.
- A router is server code, so an app can put a trusted value in a routed input, such as a tenant id. A client can replace it. A schema check does not help, because the new value has the same shape.
With persistence, the server checks a resume against its stored record. This issue does not apply there.
Scope
All interrupts that resume through the client, not only subagents:
- Tool approvals (
needsApproval). - Client tools.
- Generic interrupts from
onInterruptBoundary. - Interrupts raised in a subagent, and the router plan.
All four leave the run through the same suspend point and come back through the same resume point. One signature at that point covers them.
Proposal
- Add one server secret to
chat(), for examplesigningSecret. - When a run suspends, build one canonical JSON value that holds:
- the thread id and the run id
- each pending interrupt: id, tool name, arguments, schema hashes
- the router plan and its inputs, if the turn has one
- an expiry time
- Compute an HMAC-SHA256 of that value and send it with the interrupts.
- On resume, build the same value from the request, compute the HMAC again, and compare. If the values differ, or the expiry time is past, fail the resume with
InterruptResumeValidationErrorand codestale.
Parts that exist:
canonicalInterruptJsongives a stable serialization.- Web Crypto has HMAC in each runtime the library supports, so no new dependency is necessary.
Open questions
- Where the signature travels. An AG-UI resume entry has an interrupt id, a status, and a payload. The signature must be on a field that each client sends back. Use an AG-UI field for this. Do not add a new wire format.
- No secret set. Option: signing is opt-in at first, and a resume with no signature works as it does now, with a warning in development. A later breaking release then requires a secret or persistence.
- Generic interrupts. Confirm that their resume path goes through the same point as tool approvals and client tools.
- Key rotation. A new secret makes each suspended run stale. Decide if
chat()accepts more than one secret for verification.
Limits
- The server still trusts the earlier message history. The signature covers the suspended state only.
- A client can send the same resume two times. The expiry time limits this. To stop it, the server must keep state.
- The client can still read the arguments and the inputs. That needs encryption, which is not part of this issue.
Related
- #1722 adds routed inputs to the plan. Until this issue is done, the safe behavior is to keep routed inputs off the wire, so a turn with routed inputs cannot resume.
- A second way to get the same result is to keep the plan on the server through
routedSubagentPersistence. That needs persistence. Signing does not.
- 主要言語
- TypeScript
- スター
- 3.2k
- フォーク
- 361
- 平均マージ
- 1日 8時間
- マージ済み PR(30日)
- 252
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/ai のほかの issue
-
難易度 4/5 3〜5日 初心者へのやさしさ 32/100
メンテナーはふだん 1 日以内に返信
-
Responses replay puts assistant text between function_call and function_call_output対応中かも @tombeckenham が今日担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
move to modelschemas対応中かも @jherr が今日担当しました。 オープンhas-pr waiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
Text adapters fail chat()'s adapter constraint under exactOptionalPropertyTypes (midConversationChannels)対応中かも @tombeckenham が 1 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
@tanstack/ai-anthropic: a stream that fails after message_start loses the call's input usage対応中かも @AlemTuzlak が 2 日前に担当しました。 オープンwaiting-on: maintainer
TanStack/ai#1735 · コメント 2 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
QuantumNous/new-api#7839 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
bug p2 triaged
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
getpaseo/paseo#7071 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
test: make the #3408 Claude OAuth token fixture obviously fake so secret scanners stop flagging itオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
code-yeongyu/senpi#3436 ·
メンテナーはふだん 1 日以内に返信
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
難易度 1/5 1時間未満 初心者へのやさしさ 78/100
lingdojo/kana-dojo#32505 · コメント 1 件 · リアクション 5 件 ·
メンテナーはふだん 1 日以内に返信