# Computer Use Safety Checks

Some computer actions are risky. The page can ask the model to do a thing the user did not ask for, or the action can send data. OpenAI attaches safety checks to these actions. The run pauses, and a person approves or declines the action.

## How it works

1. The model sends a computer call with `pending_safety_checks`.
2. The `middleware` pauses the run with one interrupt for each call that has checks.
3. A person approves or declines on the client.

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

## Server

Give `tools`, `middleware`, and `interrupt` to `chat()`. Also give `parentRunId` and `resume`, so that the approval can continue the run.

```ts
// 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)
}
```

`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.

## Client

Register `computerSafetyCheck` with `useChat`. Each item 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.

```tsx
// 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'),
    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](../interrupts/generic).

## 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.
