Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- Providers
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Computer Use Safety Checks
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.
1. Add the server route#
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)
}2. Show the approval card#
Register computerSafetyCheck with useChat. Then show a card for each paused action. Each interrupt has these fields:
- payload.checks: the safety checks. Each check has an id, and can have a code and a message.
- payload.action: the action that waits. For a batch, this is the list of actions.
// 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.
3. Try it#
Ask the model to do a task that sends data, for example a form submit. When OpenAI adds a safety check, the card shows.
- Click Approve, and the action runs.
- Click Decline, and the page stays as it was.
How it works#
- The model sends a computer call with pending_safety_checks.
- The middleware pauses the run with one interrupt for each call that has checks.
- A person approves or declines on the client.
After the answer:
- If the person approves all calls, the calls run. The checks go back to OpenAI as acknowledged.
- If the person declines one call, no call of that turn runs.
- A call with checks that nobody approved does not run. The tool refuses it.
Safety checks are an OpenAI feature. Anthropic models do not send them, so this code does nothing on Anthropic.
Advanced#
Keep the approval until the resume request#
computerUse keeps the approval for the thread until the resume request comes. To keep it there:
- Create computerUse one time, at module scope. Every request uses the same one.
- Give params.threadId to chat(). The resume request has the same thread id as the request that paused.
- Set idleTimeout above the time that a person takes to answer. If the time ends first, the page closes, and the approved action runs on a new page.
- Send the resume request to the same server process. The approvals live in the memory of that process.