BashBrainTeammatesPricingDevelopersGet a test key

Let your user say yes

Bash asks before anything that spends money or speaks for someone. The question reaches you, your app shows it to your user, and only their yes moves money.

Choose what waits

Your file lists what Bash asks the person before doing (deploy); every tool of your MCP servers waits for a yes too, unless your server marks it read-only or your file lets it run without asking, and so does every function your code answers, unless your file marks it read_only or run_without_asking (your tools). A system approval enforces what approvals lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask.

What happens Bash asks before any of these, for every user.

approvals.yaml
# What Bash asks your user before doing, in the help desk's vestra.yaml: a system approval for# your own tools, Bash's own ask in a browser or a connected app.name: travel-helpapprovals: [payment, booking, cancellation, send_email]

See what Bash asks

Bash sends interaction.created, and waits. Show your user its summary, which names each input of the call, so one of your own tools that takes the price shows it (its moves_money is null, as only your code knows what your tools cost).

What happens The conversation shows waiting, and nothing is booked or paid.

TypeScript
const run = vestra.user('traveller-42').ask(  'My 6am flight got cancelled. Book me on the next one.',); for await (const event of run) {  if (event.type !== 'interaction.created') continue;  const asked = event.interaction;  // Bash waits: show your user what it wants to do.  if (asked?.type === 'approval')    await app.show(event.user, asked.summary);}
Python
run = vestra.user("traveller-42").ask(    "My 6am flight got cancelled. Book me on the next one.") for event in run:    if event.type != "interaction.created":        continue    asked = event.interaction    # Bash waits: show your user what it wants to do.    if asked and asked.type == "approval":        app.show(event.user, asked.summary)
CLI
# vestra ask shows the question in the terminal itself. From another terminal or a script, list what Bash waits on for one traveller:vestra answer --user traveller-42
HTTP
# What Bash waits on, for one traveller. Then one of them, with the summary to show your user.curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42&status=pending" \  -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/interactions/int_3j9" \  -H "Authorization: Bearer $VESTRA_API_KEY"
HTTP

An agent sees what Bash waits on in the ask tool's answer, and answers with the answer tool.

# What Bash waits on, for one traveller. Then one of them, with the summary to show your user.curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42&status=pending" \  -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/interactions/int_3j9" \  -H "Authorization: Bearer $VESTRA_API_KEY"

Answer with their choice

A yes moves money only with present_user: true: the person who saw it said yes. The yes to a company action (a tool of your MCP servers, or a function of yours that is not read-only or run without asking) needs it too, sent for the person who saw it.

What happens On a yes, Bash books and replies. On a no, nothing is booked or paid.

TypeScript
const run = vestra.user('traveller-42').ask(  'My 6am flight got cancelled. Book me on the next one.',); for await (const event of run) {  if (event.type !== 'interaction.created') continue;  const asked = event.interaction;  if (asked?.type !== 'approval') continue;  // Their choice. present_user: they saw it, so money may move.  const yes = await app.ask(event.user, asked.summary);  await event.answer({ approve: yes, present_user: true });}
Python
run = vestra.user("traveller-42").ask(    "My 6am flight got cancelled. Book me on the next one.") for event in run:    if event.type != "interaction.created":        continue    asked = event.interaction    if asked and asked.type == "approval":        # Your user's choice. present_user: they saw it.        yes = app.ask(event.user, asked.summary)        event.answer(approve=yes, present_user=True)
CLI
# Yes, from the person who saw it. Or no, and why.vestra answer int_3j9 --approve --present-uservestra answer int_7kq --deny --message "Not that flight, it lands too late."
HTTP
# Yes, from the person who saw it: present_user lets money move. Or no, and why.curl "$VESTRA_BASE_URL/v1/interactions/int_3j9/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "approve": true, "present_user": true }' curl "$VESTRA_BASE_URL/v1/interactions/int_7kq/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "approve": false, "message": "Not that flight, it lands too late." }'
MCP
{  "method": "tools/call",  "params": {    "name": "answer",    "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true }  }}

Answer questions and hand-offs

One route answers every kind: answers for a question, done for a hand-off (answer).

What happens Bash carries on with what you said. A body that does not fit is refused as answer_mismatch.

TypeScript
const run = vestra.user('traveller-42').ask(  'Book me on the next flight to Mumbai.',); for await (const event of run) {  if (event.type !== 'interaction.created') continue;  const asked = event.interaction;  // A question: one answer each, in words or one of its options.  if (asked?.type === 'question' && asked.questions) {    const answers = await Promise.all(      asked.questions.map((q) => app.choose(event.user, q)),    );    await event.answer({ answers });  }  // A hand-off: your user does one thing, such as type a code.  if (asked?.type === 'handoff') await event.answer({ done: true });}
Python
run = vestra.user("traveller-42").ask(    "Book me on the next flight to Mumbai.") for event in run:    if event.type != "interaction.created":        continue    asked = event.interaction    # A question: one answer each, in words or one of its options.    if asked and asked.type == "question":        answers = [app.choose(event.user, q) for q in asked.questions]        event.answer(answers=answers)    # A hand-off: your user does one thing, such as type a code.    if asked and asked.type == "handoff":        event.answer(done=True)
CLI
# A question, answered in your user's words; a hand-off, done.vestra answer int_2mx --answer "Window"vestra answer int_9hd --done
HTTP
# A question takes one answer per question; a hand-off takes done, could_not or not_now.curl "$VESTRA_BASE_URL/v1/interactions/int_2mx/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "answers": ["Window"] }' curl "$VESTRA_BASE_URL/v1/interactions/int_9hd/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "done": true }'
MCP
{  "method": "tools/call",  "params": {    "name": "answer",    "arguments": { "interaction": "int_2mx", "answers": ["Window"] }  }}

Or ask inside your app

Bash inside your app shows its approvals to your user itself (embed).

What happens Your webhooks still hear each interaction.created.

The whole thing

Approvals, questions and hand-offs, answered in one loop.

What happens Bash waits at each, and carries on with your user's answer.

TypeScript
import { Vestra } from '@vestra/sdk'; const vestra = new Vestra();const run = vestra.user('traveller-42').ask(  'My 6am flight got cancelled. Book me on the next one.',); for await (const event of run) {  if (event.type !== 'interaction.created') continue;  const asked = event.interaction;   // Answer with their choice  if (asked?.type === 'approval') {    const yes = await app.ask(event.user, asked.summary);    await event.answer({ approve: yes, present_user: true });  }   // Answer questions and hand-offs  if (asked?.type === 'question' && asked.questions) {    const answers = await Promise.all(      asked.questions.map((q) => app.choose(event.user, q)),    );    await event.answer({ answers });  }  if (asked?.type === 'handoff') await event.answer({ done: true });}console.log(await run.text());
Python
from vestra import Vestra vestra = Vestra()run = vestra.user("traveller-42").ask(    "My 6am flight got cancelled. Book me on the next one.") for event in run:    if event.type != "interaction.created":        continue    asked = event.interaction     # Answer with their choice    if asked and asked.type == "approval":        yes = app.ask(event.user, asked.summary)        event.answer(approve=yes, present_user=True)     # Answer questions and hand-offs    if asked and asked.type == "question":        answers = [app.choose(event.user, q) for q in asked.questions]        event.answer(answers=answers)    if asked and asked.type == "handoff":        event.answer(done=True)print(run.text())
CLI
# See what Bash asks: vestra ask asks you right herevestra ask "My 6am flight got cancelled. Book me on the next one." --user traveller-42 # Or answer by id: approvals, questions and hand-offsvestra answer --user traveller-42vestra answer int_3j9 --approve --present-uservestra answer int_2mx --answer "Window"vestra answer int_9hd --done
HTTP
# See what Bash askscurl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42&status=pending" \  -H "Authorization: Bearer $VESTRA_API_KEY" # Answer with their choice, questions and hand-offscurl "$VESTRA_BASE_URL/v1/interactions/int_3j9/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "approve": true, "present_user": true }' curl "$VESTRA_BASE_URL/v1/interactions/int_2mx/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "answers": ["Window"] }' curl "$VESTRA_BASE_URL/v1/interactions/int_9hd/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "done": true }'
HTTP

An agent answers each with the answer tool, as the steps show.

# See what Bash askscurl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42&status=pending" \  -H "Authorization: Bearer $VESTRA_API_KEY" # Answer with their choice, questions and hand-offscurl "$VESTRA_BASE_URL/v1/interactions/int_3j9/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "approve": true, "present_user": true }' curl "$VESTRA_BASE_URL/v1/interactions/int_2mx/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "answers": ["Window"] }' curl "$VESTRA_BASE_URL/v1/interactions/int_9hd/answer" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "done": true }'