Agents APISessions

Approvals

Let an agent work on its own, but decide yourself before it does something risky.

When a permission rule says ask, the agent stops and the session's status becomes waiting. With the default rules that happens before most shell commands. The agent waits for as long as it takes, and a waiting session is never paused for being idle.

The request

The event stream yields an approval.requested item that says what the agent wants to do:

JSON
{
  "type": "approval.requested",
  "request_id": "per_0e6e0f207001Qg0xMR9tdla3uP",
  "action": "bash",
  "resources": ["uname -s"],
  "metadata": {}
}
ReplyEffect
onceAllow this call.
alwaysAllow this call, and matching calls for the rest of the session.
rejectRefuse it. An optional message tells the agent why, so it can adjust.

Answer from code

This example allows uname and rejects everything else:

Approve some commands, reject the rest
const allowed = /^uname\b/

for await (const event of sessions.events(session.id)) {
  if (event.type === "approval.requested" && !("seq" in event)) {
    const ok = event.action === "bash" && (event.resources ?? []).every((command) => allowed.test(command))
    console.log(ok ? "approve:" : "reject:", event.action, (event.resources ?? []).join(" "))
    if (ok) await sessions.approve(session.id, event.request_id, "once")
    else await sessions.approve(session.id, event.request_id, "reject", "Only uname is allowed here.")
  }
  if (event.type === "text.ended") console.log(event.data?.text)
  if (event.type === "run.completed" || event.type === "run.failed") break
}
approve: bash uname -s
reject: bash whoami
- uname -s → Linux
- whoami → failed to execute (the command was rejected)

Ask a person

To put a person in the loop, show them the request and send their answer. This terminal chat asks you about every shell command:

Approve commands from the terminal
// A chat with an agent in your terminal. You approve its shell commands as they come up.
import * as readline from "node:readline/promises"
import { LatentStack } from "@latentcode/sandbox-agents"

const client = new LatentStack({ apiKey: process.env.LS_API_KEY! })
const sessions = client.agents.sessions
const terminal = readline.createInterface({ input: process.stdin, output: process.stdout })

const session = await sessions.create({
  agent: { model: "bedrock/us.anthropic.claude-sonnet-5" },
  input: await terminal.question("task> "),
})

try {
  for await (const event of sessions.events(session.id)) {
    if (event.type === "approval.requested" && !("seq" in event)) {
      const answer = await terminal.question(`run \`${(event.resources ?? []).join(" ")}\`? [y]es / [a]lways / [n]o `)
      const reply = answer === "a" ? "always" : answer === "y" ? "once" : "reject"
      await sessions.approve(session.id, event.request_id, reply)
    }
    if (event.type === "tool.called") console.log(`  · ${event.data?.tool}`)
    if (event.type === "text.ended") console.log(`\n${event.data?.text}\n`)
    if (event.type === "run.completed" || event.type === "run.failed") {
      const next = await terminal.question("task> (empty to quit) ")
      if (!next) break
      await sessions.message(session.id, next)
    }
  }
} finally {
  terminal.close()
  await sessions.delete(session.id)
}
  • The same shape works for a Slack bot or a web UI: forward the request, then answer with the session id and request_id. The request stays pending until someone answers, and it's listed under pending_approvals on every session.status item.
  • Answering a request twice, or one that no longer exists, raises not_found. Treat it as done.

Never wait for a person

For CI jobs and other unattended work, set rules that never ask. Start with a catch-all deny and allow only what the task needs. A denied call is refused and the agent looks for another way, so it never stops to wait.

A read-only reviewer that never asks
const session = await sessions.create({
  agent: {
    model: "bedrock/us.anthropic.claude-sonnet-5",
    instructions: "You review code. Read before you judge, name files and lines, and never modify files.",
    permissions: [
      { action: "*", effect: "deny" },                       // anything not allowed below is refused
      { action: "read", effect: "allow" },
      { action: "glob", effect: "allow" },
      { action: "grep", effect: "allow" },
      { action: "list", effect: "allow" },
      { action: "bash", resource: "git *", effect: "allow" },
      { action: "bash", resource: "git push*", effect: "deny" }, // later rules win
    ],
  },
  environment: { type: "repos", repos: ["acme/demo"] },
  input: "Review the most recent commit (git show HEAD). List up to three concrete risks or improvements.",
})