Skip to content

SDK Permission Control

Version Requirement: This document is for CodeBuddy Agent SDK v0.1.0 and above.

This document explains how to implement permission control in the SDK, including permission modes, canUseTool callbacks, and tool whitelists/blacklists.

Table of Contents

Overview

CodeBuddy Agent SDK provides multiple permission control mechanisms:

MechanismDescriptionUse Case
Permission ModesGlobal permission behavior controlQuick setup of overall policy
canUseTool CallbackRuntime dynamic approvalInteractive permission confirmation
Tool Whitelists/BlacklistsDeclarative tool filteringStatic policy configuration

Permission Modes

Set global permission behavior through permissionMode (TypeScript) or permission_mode (Python).

Available Modes

ModeDescription
defaultDefault mode, all tool operations require confirmation
acceptEditsAuto-approve file edits, other operations still require confirmation
planPlanning mode, only read-only tools allowed
bypassPermissionsSkip all permission checks (use with caution)

Initial Configuration

typescript
import { query } from '@tencent-ai/agent-sdk';

const q = query({
  prompt: 'Help me refactor this code',
  options: {
    model: 'deepseek-v3.1',
    permissionMode: 'acceptEdits'  // Auto-approve edits
  }
});

for await (const message of q) {
  console.log(message);
}
python
import asyncio
from codebuddy_agent_sdk import query, CodeBuddyAgentOptions

async def main():
    options = CodeBuddyAgentOptions(
        model="deepseek-v3.1",
        permission_mode="acceptEdits"  # Auto-approve edits
    )

    async for message in query(prompt="Help me refactor this code", options=options):
        print(message)

asyncio.run(main())

Dynamically Modifying Permission Mode

Using Session/Client API allows you to dynamically modify permission mode at runtime:

typescript
import { unstable_v2_createSession } from '@tencent-ai/agent-sdk';

const session = unstable_v2_createSession({
  model: 'deepseek-v3.1'
});

// Send first message
await session.send('Analyze this project');
for await (const msg of session.stream()) {
  console.log(msg);
}

// Dynamically switch to acceptEdits mode for faster development
// Note: This is an unstable API
python
from codebuddy_agent_sdk import CodeBuddySDKClient, CodeBuddyAgentOptions

async def main():
    options = CodeBuddyAgentOptions(model="deepseek-v3.1")

    async with CodeBuddySDKClient(options=options) as client:
        await client.query("Analyze this project")
        async for msg in client.receive_response():
            print(msg)

        # Dynamically switch permission mode
        await client.set_permission_mode("acceptEdits")

        await client.query("Now help me modify the code")
        async for msg in client.receive_response():
            print(msg)

canUseTool Callback

The canUseTool callback is triggered when a tool requires permission confirmation, allowing you to implement custom permission logic.

Callback Signature

typescript
type CanUseTool = (
  toolName: string,
  input: Record<string, unknown>,
  options: CanUseToolOptions
) => Promise<PermissionResult>;

type CanUseToolOptions = {
  signal: AbortSignal;
  toolUseID: string;
  agentID?: string;
  suggestions?: PermissionUpdate[];
  blockedPath?: string;
  decisionReason?: string;
};

type PermissionResult =
  | { behavior: 'allow'; updatedInput: Record<string, unknown> }
  | { behavior: 'deny'; message: string; interrupt?: boolean };
python
CanUseTool = Callable[
    [str, dict[str, Any], CanUseToolOptions],
    Awaitable[PermissionResult],
]

@dataclass
class CanUseToolOptions:
    tool_use_id: str
    signal: Any | None = None
    agent_id: str | None = None
    suggestions: list[dict[str, Any]] | None = None
    blocked_path: str | None = None
    decision_reason: str | None = None

PermissionResult = PermissionResultAllow | PermissionResultDeny

Complete Example: Interactive Approval

typescript
import { query } from '@tencent-ai/agent-sdk';

const q = query({
  prompt: 'Help me analyze this codebase',
  options: {
    model: 'deepseek-v3.1',
    canUseTool: async (toolName, input, options) => {
      console.log(`\n🔧 Tool request: ${toolName}`);
      console.log(`   Parameters:`, JSON.stringify(input, null, 2));

      // Auto-allow read-only tools
      const readOnlyTools = ['Read', 'Glob', 'Grep'];
      if (readOnlyTools.includes(toolName)) {
        return { behavior: 'allow', updatedInput: input };
      }

      // Deny dangerous commands
      if (toolName === 'Bash') {
        const command = input.command as string;
        if (command.includes('rm -rf') || command.includes('sudo')) {
          return {
            behavior: 'deny',
            message: 'Dangerous command rejected',
            interrupt: true  // Interrupt the entire session
          };
        }
      }

      // Other cases: simulate user confirmation
      const approved = await promptUser(`Allow ${toolName} execution?`);

      if (approved) {
        return { behavior: 'allow', updatedInput: input };
      } else {
        return { behavior: 'deny', message: 'User denied' };
      }
    }
  }
});

for await (const message of q) {
  console.log(message);
}
python
from codebuddy_agent_sdk import (
    query, CodeBuddyAgentOptions,
    CanUseToolOptions, PermissionResultAllow, PermissionResultDeny
)

async def can_use_tool(
    tool_name: str,
    input_data: dict,
    options: CanUseToolOptions
):
    print(f"\n🔧 Tool request: {tool_name}")
    print(f"   Parameters: {input_data}")

    # Auto-allow read-only tools
    read_only_tools = ["Read", "Glob", "Grep"]
    if tool_name in read_only_tools:
        return PermissionResultAllow(updated_input=input_data)

    # Deny dangerous commands
    if tool_name == "Bash":
        command = input_data.get("command", "")
        if "rm -rf" in command or "sudo" in command:
            return PermissionResultDeny(
                message="Dangerous command rejected",
                interrupt=True  # Interrupt the entire session
            )

    # Other cases: simulate user confirmation
    answer = input(f"Allow {tool_name} execution? (y/n): ")

    if answer.lower() == 'y':
        return PermissionResultAllow(updated_input=input_data)
    else:
        return PermissionResultDeny(message="User denied")

async def main():
    options = CodeBuddyAgentOptions(
        model="deepseek-v3.1",
        can_use_tool=can_use_tool
    )

    async for message in query(prompt="Help me analyze this codebase", options=options):
        print(message)

Modifying Tool Input

You can modify tool input parameters in canUseTool:

typescript
canUseTool: async (toolName, input) => {
  if (toolName === 'Bash') {
    // Add safety check before command
    return {
      behavior: 'allow',
      updatedInput: {
        ...input,
        command: `set -e; ${input.command}`
      }
    };
  }
  return { behavior: 'allow', updatedInput: input };
}
python
async def can_use_tool(tool_name, input_data, options):
    if tool_name == "Bash":
        # Add safety check before command
        return PermissionResultAllow(
            updated_input={
                **input_data,
                "command": f"set -e; {input_data.get('command', '')}"
            }
        )
    return PermissionResultAllow(updated_input=input_data)

Handling AskUserQuestion

When AI needs to ask the user a question, it calls the AskUserQuestion tool. You need to handle this tool in canUseTool.

Input Structure

typescript
{
  questions: [
    {
      question: "Which database to use?",
      header: "Database",
      options: [
        { label: "PostgreSQL", description: "Relational database" },
        { label: "MongoDB", description: "Document database" }
      ],
      multiSelect: false
    }
  ]
}

Returning Answers

typescript
canUseTool: async (toolName, input) => {
  if (toolName === 'AskUserQuestion') {
    const questions = input.questions as any[];
    const answers: Record<string, string> = {};

    for (const q of questions) {
      console.log(`Question: ${q.question}`);
      for (let i = 0; i < q.options.length; i++) {
        console.log(`  ${i + 1}. ${q.options[i].label}`);
      }

      // Get user input
      const choice = await getUserChoice();
      answers[q.question] = q.options[choice].label;
    }

    return {
      behavior: 'allow',
      updatedInput: { ...input, answers }
    };
  }

  return { behavior: 'allow', updatedInput: input };
}
python
async def can_use_tool(tool_name, input_data, options):
    if tool_name == "AskUserQuestion":
        questions = input_data.get("questions", [])
        answers = {}

        for q in questions:
            print(f"Question: {q['question']}")
            for i, opt in enumerate(q["options"]):
                print(f"  {i + 1}. {opt['label']}")

            # Get user input
            choice = int(input("Select (1/2/...): ")) - 1
            answers[q["question"]] = q["options"][choice]["label"]

        return PermissionResultAllow(
            updated_input={**input_data, "answers": answers}
        )

    return PermissionResultAllow(updated_input=input_data)

Tool Whitelists/Blacklists

Use allowedTools and disallowedTools to declaratively control tool access.

Configuration Example

typescript
const q = query({
  prompt: 'Analyze project structure',
  options: {
    model: 'deepseek-v3.1',
    // Only allow these tools
    allowedTools: ['Read', 'Glob', 'Grep'],
    // Or disallow these tools
    disallowedTools: ['Bash', 'Write', 'Edit']
  }
});
python
options = CodeBuddyAgentOptions(
    model="deepseek-v3.1",
    # Only allow these tools
    allowed_tools=["Read", "Glob", "Grep"],
    # Or disallow these tools
    disallowed_tools=["Bash", "Write", "Edit"]
)

Common Tool Names

Tool NameFunctionality
ReadRead files
WriteWrite files
EditEdit files
GlobFile pattern matching
GrepContent search
BashExecute shell commands
TaskSub-agent tasks
WebFetchFetch web content
WebSearchWeb search

Best Practices

  1. Use default mode by default: Provides the most complete permission control

  2. Use plan mode for read-only tasks:

    typescript
    permissionMode: 'plan'  // Only allow Read, Glob, Grep
  3. Combine with whitelist for precise control:

    typescript
    allowedTools: ['Read', 'Glob', 'Grep'],
    permissionMode: 'bypassPermissions'  // Allowed tools execute automatically
  4. Use interrupt for dangerous commands:

    typescript
    return {
      behavior: 'deny',
      message: 'Dangerous operation',
      interrupt: true  // Immediately interrupt, don't let AI retry
    };
  5. Avoid bypassPermissions in production: This mode skips all permission checks