A safety check runs before a computer action. It blocks an action the user did not ask for, or an action that sends data.
OpenAI attaches safety checks to these actions. TanStack AI pauses the run, and a person approves or declines the action. This page adds the approval card to your computer use app.
Give tools, middleware, and interrupt to chat(). Also give parentRunId and resume, so that the approval can continue the run.
// app/api/chat/route.ts
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createComputerUse } from '@tanstack/ai-computer-use'
import { playwrightComputer } from '@tanstack/ai-computer-playwright'
const computerUse = createComputerUse({
driver: playwrightComputer({ startUrl: 'https://tanstack.com' }),
// A person has up to one hour to answer before the page closes.
idleTimeout: 60 * 60_000,
})
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
...(params.parentRunId ? { parentRunId: params.parentRunId } : {}),
...(params.resume ? { resume: params.resume } : {}),
tools: computerUse.tools,
middleware: [computerUse.middleware],
interrupts: [computerUse.interrupt],
})
return toServerSentEventsResponse(stream)
}Register computerSafetyCheck with useChat. Then show a card for each paused action. Each interrupt has these fields:
// app/chat.tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import type { GenericInterrupt } from '@tanstack/ai-react'
import { computerSafetyCheck } from '@tanstack/ai-computer-use'
function SafetyCheckCard({
interrupt,
}: {
interrupt: GenericInterrupt<typeof computerSafetyCheck>
}) {
const checks = interrupt.payload?.checks ?? []
return (
<article>
<h2>Approve this computer action?</h2>
<ul>
{checks.map((check) => (
<li key={check.id}>{check.message ?? check.code ?? check.id}</li>
))}
</ul>
<pre>{JSON.stringify(interrupt.payload?.action, null, 2)}</pre>
<button onClick={() => interrupt.resolveInterrupt({ approved: true })}>
Approve
</button>
<button onClick={() => interrupt.resolveInterrupt({ approved: false })}>
Decline
</button>
</article>
)
}
export function Chat() {
const { interrupts } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
interrupts: [computerSafetyCheck],
})
return (
<>
{interrupts.map((interrupt) => {
if (interrupt.kind !== 'generic') return null
if (!('definitionId' in interrupt)) return null
if (interrupt.definitionId !== computerSafetyCheck.id) return null
return <SafetyCheckCard key={interrupt.id} interrupt={interrupt} />
})}
</>
)
}interrupt.cancel() also declines the action. When every interrupt of the batch has an answer, the client sends the answers. For more about generic interrupts, see Generic Interrupts.
Ask the model to do a task that sends data, for example a form submit. When OpenAI adds a safety check, the card shows.
After the answer:
Safety checks are an OpenAI feature. Anthropic models do not send them, so this code does nothing on Anthropic.
computerUse keeps the approval for the thread until the resume request comes. To keep it there: