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:
{
"type": "approval.requested",
"request_id": "per_0e6e0f207001Qg0xMR9tdla3uP",
"action": "bash",
"resources": ["uname -s"],
"metadata": {}
}| Reply | Effect |
|---|---|
| once | Allow this call. |
| always | Allow this call, and matching calls for the rest of the session. |
| reject | Refuse it. An optional message tells the agent why, so it can adjust. |
Answer from code
This example allows uname and rejects everything else:
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:
// 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 underpending_approvalson everysession.statusitem. - 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.
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.",
})