mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 14:01:09 +00:00
refactor: generalize permissions example
This commit is contained in:
parent
6e6d94be38
commit
5127b2c4ed
5 changed files with 17 additions and 14 deletions
17
examples/permissions/package.json
Normal file
17
examples/permissions/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@sandbox-agent/example-permissions",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"sandbox-agent": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "latest",
|
||||
"tsx": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
}
|
||||
161
examples/permissions/src/index.ts
Normal file
161
examples/permissions/src/index.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import {
|
||||
SandboxAgent,
|
||||
type PermissionReply,
|
||||
type SessionPermissionRequest,
|
||||
} from "sandbox-agent";
|
||||
|
||||
const agent = (process.env.PERMISSIONS_AGENT?.trim() || "claude").toLowerCase();
|
||||
const requestedPermissionMode = process.env.PERMISSION_MODE?.trim();
|
||||
const autoReply = parsePermissionReply(process.env.PERMISSION_REPLY);
|
||||
const promptText =
|
||||
process.env.PERMISSION_PROMPT?.trim() ||
|
||||
`Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
spawn: {
|
||||
enabled: true,
|
||||
log: "inherit",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await sdk.installAgent(agent);
|
||||
|
||||
const agents = await sdk.listAgents({ config: true });
|
||||
const selectedAgent = agents.agents.find((entry) => entry.id === agent);
|
||||
const configOptions = Array.isArray(selectedAgent?.configOptions)
|
||||
? (selectedAgent.configOptions as Array<{ category?: string; currentValue?: string; options?: unknown[] }>)
|
||||
: [];
|
||||
const modeOption = configOptions.find((option) => option.category === "mode");
|
||||
const availableModes = extractOptionValues(modeOption);
|
||||
const permissionMode = requestedPermissionMode || modeOption?.currentValue || availableModes[0] || "default";
|
||||
|
||||
console.log(`Agent: ${agent}`);
|
||||
console.log(`Permission mode: ${permissionMode}`);
|
||||
if (availableModes.length > 0) {
|
||||
console.log(`Available modes: ${availableModes.join(", ")}`);
|
||||
}
|
||||
console.log(`Working directory: ${process.cwd()}`);
|
||||
console.log(`Prompt: ${promptText}`);
|
||||
if (autoReply) {
|
||||
console.log(`Automatic permission reply: ${autoReply}`);
|
||||
} else {
|
||||
console.log("Interactive permission replies enabled.");
|
||||
}
|
||||
|
||||
const session = await sdk.createSession({
|
||||
agent,
|
||||
permissionMode,
|
||||
sessionInit: {
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
},
|
||||
});
|
||||
|
||||
const rl = autoReply
|
||||
? null
|
||||
: createInterface({
|
||||
input,
|
||||
output,
|
||||
});
|
||||
|
||||
session.onPermissionRequest((request: SessionPermissionRequest) => {
|
||||
void handlePermissionRequest(session, request, autoReply, rl);
|
||||
});
|
||||
|
||||
const response = await session.prompt([{ type: "text", text: promptText }]);
|
||||
console.log(`Prompt finished with stopReason=${response.stopReason}`);
|
||||
|
||||
await rl?.close();
|
||||
} finally {
|
||||
await sdk.dispose();
|
||||
}
|
||||
|
||||
async function handlePermissionRequest(
|
||||
session: {
|
||||
replyPermission(permissionId: string, reply: PermissionReply): Promise<void>;
|
||||
},
|
||||
request: SessionPermissionRequest,
|
||||
auto: PermissionReply | null,
|
||||
rl: ReturnType<typeof createInterface> | null,
|
||||
): Promise<void> {
|
||||
const reply = auto ?? (await promptForReply(request, rl));
|
||||
console.log(`Permission ${reply}: ${request.toolCall.title ?? request.toolCall.toolCallId}`);
|
||||
await session.replyPermission(request.id, reply);
|
||||
}
|
||||
|
||||
async function promptForReply(
|
||||
request: SessionPermissionRequest,
|
||||
rl: ReturnType<typeof createInterface> | null,
|
||||
): Promise<PermissionReply> {
|
||||
if (!rl) {
|
||||
return "reject";
|
||||
}
|
||||
|
||||
const title = request.toolCall.title ?? request.toolCall.toolCallId;
|
||||
const available = request.availableReplies;
|
||||
console.log("");
|
||||
console.log(`Permission request: ${title}`);
|
||||
console.log(`Available replies: ${available.join(", ")}`);
|
||||
const answer = (await rl.question("Reply [once|always|reject]: ")).trim().toLowerCase();
|
||||
const parsed = parsePermissionReply(answer);
|
||||
if (parsed && available.includes(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
console.log("Invalid reply, defaulting to reject.");
|
||||
return "reject";
|
||||
}
|
||||
|
||||
function extractOptionValues(option: { options?: unknown[] } | undefined): string[] {
|
||||
if (!option?.options) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values: string[] = [];
|
||||
for (const entry of option.options) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
continue;
|
||||
}
|
||||
const value = "value" in entry && typeof entry.value === "string" ? entry.value : null;
|
||||
if (value) {
|
||||
values.push(value);
|
||||
continue;
|
||||
}
|
||||
if (!("options" in entry) || !Array.isArray(entry.options)) {
|
||||
continue;
|
||||
}
|
||||
for (const nested of entry.options) {
|
||||
if (!nested || typeof nested !== "object") {
|
||||
continue;
|
||||
}
|
||||
const nestedValue =
|
||||
"value" in nested && typeof nested.value === "string" ? nested.value : null;
|
||||
if (nestedValue) {
|
||||
values.push(nestedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function parsePermissionReply(value: string | undefined): PermissionReply | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (value.trim().toLowerCase()) {
|
||||
case "once":
|
||||
return "once";
|
||||
case "always":
|
||||
return "always";
|
||||
case "reject":
|
||||
case "deny":
|
||||
return "reject";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
14
examples/permissions/tsconfig.json
Normal file
14
examples/permissions/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue