승인 및 사용자 입력 처리
승인 및 사용자 입력 처리
allowedTools / disallowedTools 규칙과 canUseTool 콜백을 사용해 에이전트가 사용할 수 있는 도구와 권한 요청이 처리되는 방식을 제어해요.
기본적으로 SDK는 도구 권한을 강제하고 우회하지 않아요. canUseTool 콜백은 allowedTools/disallowedTools 규칙이나 bypassPermissions 같은 명시적 권한 모드로 이미 처리되지 않은 권한 요청을 세밀하게 제어해요.
SDK는 기본적으로 대화형 권한 프롬프트를 제공하지 않아요. 권한 확인된 요청이 SDK에 도달했는데 canUseTool이 제공되지 않으면 요청은 실패해요.
본문
작동 방식
canUseTool 콜백을 제공하면 SDK는 권한 확인된 각 도구 실행 전에 그 콜백을 호출해요. 콜백이 어떻게 할지 결정해요.
- 에이전트가 권한 확인된 도구를 사용하기로 결정.
- SDK가 도구 이름, 요청 입력, 권한 컨텍스트와 함께 사용자의
canUseTool콜백을 호출. - 콜백이
allow또는deny를 반환. - 그에 따라 도구 실행이 진행되거나 차단.
많은 일반적인 도구 권한 확인에서 콜백 입력에는 { action, resource } 같은 필드가 포함돼요. AskUserQuestion, ExitPlanMode 같은 SDK 라우팅 의사 도구는 도구 특화 필드를 포함해요.
canUseTool이 제공되지 않고 권한 확인된 요청이 SDK에 도달하면, SDK는 대화형으로 프롬프트하는 대신 오류를 반환해요.
기본 예시
import { createCortexCodeSession } from "cortex-code-agent-sdk";
const session = await createCortexCodeSession({
cwd: process.cwd(),
canUseTool: async (toolName, input, context) => {
console.log(`Tool requested: ${toolName}`, input);
// Allow read-only tools, deny destructive ones
if (["Read", "Glob", "Grep"].includes(toolName)) {
return { behavior: "allow" };
}
return { behavior: "deny", message: "Only read-only tools are allowed" };
},
});
await session.send("What files are in this directory?");
for await (const event of session.stream()) {
if (event.type === "assistant") {
for (const b of event.content) {
if (b.type === "text") process.stdout.write(b.text);
}
}
if (event.type === "result") break;
}
await session.close();
from typing import Any
from cortex_code_agent_sdk import (
CortexCodeAgentOptions,
CortexCodeSDKClient,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext,
) -> PermissionResultAllow | PermissionResultDeny:
print(f"Tool requested: {tool_name}", tool_input)
# Allow read-only tools, deny destructive ones
if tool_name in ("Read", "Glob", "Grep"):
return PermissionResultAllow()
return PermissionResultDeny(message="Only read-only tools are allowed")
async with CortexCodeSDKClient(CortexCodeAgentOptions(
can_use_tool=can_use_tool,
)) as client:
await client.query("What files are in this directory?")
async for msg in client.receive_response():
...
응답 패턴
도구 호출 허용
{ behavior: "allow" }를 반환하면 도구가 원래 입력으로 실행되게 해요.
canUseTool: async (toolName, input, context) => {
return { behavior: "allow" };
}
from typing import Any
from cortex_code_agent_sdk import PermissionResultAllow, ToolPermissionContext
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext,
) -> PermissionResultAllow:
return PermissionResultAllow()
도구 호출 거부
선택적 메시지와 함께 { behavior: "deny" }를 반환해요. 에이전트는 거부 메시지를 보고 접근 방식을 조정할 수 있어요.
canUseTool: async (toolName, input, context) => {
if (toolName === "Bash") {
return { behavior: "deny", message: "Bash commands are not allowed in this session" };
}
return { behavior: "allow" };
}
from typing import Any
from cortex_code_agent_sdk import (
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext,
) -> PermissionResultAllow | PermissionResultDeny:
if tool_name == "Bash":
return PermissionResultDeny(message="Bash commands are not allowed in this session")
return PermissionResultAllow()
AskUserQuestion 도구
Cortex Code에는 에이전트가 사용자에게 명확성이 필요할 때 사용하는 기본 제공 AskUserQuestion 도구가 있어요. 에이전트가 AskUserQuestion을 호출하면 SDK가 이를 toolName이 "AskUserQuestion"인 사용자의 canUseTool 콜백으로 라우팅해요. 입력에는 에이전트의 질문이 구조화된 객관식 옵션으로 포함돼요.
질문 처리
canUseTool 콜백에서 toolName === "AskUserQuestion"을 확인하세요. 입력에는 각 질문에 다음이 있는 questions 배열이 포함돼요.
| 필드 | 설명 |
|---|---|
question |
표시할 전체 질문 텍스트 |
header |
질문의 짧은 라벨 |
options |
각각 label, description, freeForm(bool), isCancel(bool)이 있는 선택지 배열 |
multiSelect |
true이면 사용자가 여러 옵션을 선택할 수 있어요 |
원래 질문과 answers 맵을 포함한 updatedInput과 함께 allow를 반환해요. 각 키는 질문 텍스트이고 각 값은 선택된 옵션의 라벨이에요. 다중 선택 질문의 경우 라벨을 ", "로 연결해요.
const session = await createCortexCodeSession({
cwd: process.cwd(),
canUseTool: async (toolName, input, context) => {
if (toolName === "AskUserQuestion") {
const answers: Record<string, string> = {};
for (const q of input.questions) {
// Present q.question and q.options to the user, collect their choice
const selected = await promptUser(q.question, q.options);
answers[q.question] = selected;
}
return {
behavior: "allow",
updatedInput: { questions: input.questions, answers },
};
}
return { behavior: "allow" };
},
});
from typing import Any
from cortex_code_agent_sdk import (
PermissionResultAllow,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext,
) -> PermissionResultAllow:
if tool_name == "AskUserQuestion":
answers: dict[str, str] = {}
for q in tool_input.get("questions", []):
# Present q["question"] and q["options"] to the user, collect their choice
selected = await prompt_user(q["question"], q["options"])
answers[q["question"]] = selected
return PermissionResultAllow(
updated_input={"questions": tool_input["questions"], "answers": answers}
)
return PermissionResultAllow()
질문을 거부하려면(상호작용 취소) deny를 반환해요.
return { behavior: "deny", message: "User cancelled" };
return PermissionResultDeny(message="User cancelled")
팁:
AskUserQuestion은canUseTool을 통해 라우팅돼요. 콜백이 없으면 상호작용을 완료할 수 없고 요청은 조용히 진행되는 대신 오류가 나요.
ExitPlanMode 도구
세션이 플랜 모드에 있으면 Cortex Code는 플랜이 승인될 때까지 계획만 해요. 승인 요청은 toolName/tool_name이 "ExitPlanMode"로 설정된 사용자의 canUseTool 콜백으로 라우팅돼요.
입력에는 다음이 포함돼요.
| 필드 | 설명 |
|---|---|
plan |
에이전트가 실행하려는 제안된 계획 텍스트 |
question |
에이전트의 선택적 추가 승인 프롬프트 |
플랜 승인 또는 거부
allow를 반환하면 플랜을 승인해요. 선택적으로 updatedInput.message를 포함해 플랜 모드를 벗어나기 전에 리뷰 컨텍스트를 에이전트에 전달할 수 있어요.
deny를 반환하면 플랜을 거부해요. 에이전트가 특정 피드백으로 플랜을 수정하길 원할 때 message를 사용하세요. ExitPlanMode를 거부하면 현재 턴이 플랜 모드로 유지되어 에이전트가 플랜을 업데이트하고 다시 물어볼 수 있어요.
ExitPlanMode 승인은 현재 턴의 플랜 모드를 종료해요. 이후 턴은 세션의 정상적인 비플랜 권한 동작으로 돌아가므로 나중의 도구 호출은 플랜 모드 밖에서와 같은 방식으로 평가돼요.
권장 상호작용 패턴
플랜 모드를 리뷰 루프로 사용하세요.
permissionMode: "plan"(TypeScript) 또는permission_mode="plan"(Python)으로 세션 시작.- 에이전트가
AskUserQuestion을 통해 누락된 정보를 모으도록 함. - 에이전트가
ExitPlanMode를 호출하면 제안된plan텍스트를 검사. - 플랜에 변경이 필요하면 정확한 리뷰 메시지와 함께
deny반환. - 플랜이 수락 가능하면
allow반환. 선택적으로 실행을 위한 리뷰어 지침과 함께updatedInput.message/updated_input["message"]를 포함할 수 있음. - 승인 후에는 이후 턴이 세션의 정상적인 비플랜 권한 동작으로 돌아감.
const session = await createCortexCodeSession({
cwd: process.cwd(),
permissionMode: "plan",
canUseTool: async (toolName, input, context) => {
if (toolName === "ExitPlanMode") {
const plan = String(input.plan ?? "");
if (!plan.includes("test")) {
return {
behavior: "deny",
message: "Add a test or verification step before leaving plan mode.",
};
}
return {
behavior: "allow",
updatedInput: {
message: "Approved. Run the verification step before the final answer.",
},
};
}
return { behavior: "allow" };
},
});
from typing import Any
from cortex_code_agent_sdk import (
CortexCodeAgentOptions,
CortexCodeSDKClient,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext,
) -> PermissionResultAllow | PermissionResultDeny:
if tool_name == "ExitPlanMode":
plan = str(tool_input.get("plan", ""))
if "test" not in plan:
return PermissionResultDeny(
message="Add a test or verification step before leaving plan mode."
)
return PermissionResultAllow(
updated_input={
"message": "Approved. Run the verification step before the final answer."
}
)
return PermissionResultAllow()
client = CortexCodeSDKClient(CortexCodeAgentOptions(
cwd=".",
permission_mode="plan",
can_use_tool=can_use_tool,
))
리뷰 루프 예시
가장 단순한 리뷰 루프는 약한 플랜을 특정 피드백과 함께 한 번 거부한 뒤 수정된 플랜을 승인하는 거예요.
let rejectedOnce = false;
const session = await createCortexCodeSession({
cwd: process.cwd(),
permissionMode: "plan",
canUseTool: async (toolName, input) => {
if (toolName === "ExitPlanMode") {
const plan = String(input.plan ?? "");
if (!rejectedOnce) {
rejectedOnce = true;
return {
behavior: "deny",
message: "Add a verification step and say which file you will edit.",
};
}
return {
behavior: "allow",
updatedInput: {
message: `Approved plan: ${plan}`,
},
};
}
return { behavior: "allow" };
},
});
from typing import Any
from cortex_code_agent_sdk import (
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
state = {"rejected_once": False}
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext,
) -> PermissionResultAllow | PermissionResultDeny:
if tool_name == "ExitPlanMode":
plan = str(tool_input.get("plan", ""))
if not state["rejected_once"]:
state["rejected_once"] = True
return PermissionResultDeny(
message="Add a verification step and say which file you will edit."
)
return PermissionResultAllow(
updated_input={"message": f"Approved plan: {plan}"}
)
return PermissionResultAllow()
규칙 기반 권한
일반적인 권한 패턴의 경우 콜백 로직을 작성하는 대신 규칙 기반 구성을 사용할 수 있어요. allowedTools 및 disallowedTools 옵션은 자동으로 승인되거나 차단되는 도구 목록을 정의하게 해줘요.
const session = await createCortexCodeSession({
cwd: process.cwd(),
allowedTools: ["Read", "Glob", "Grep", "Bash(npm test:*)"],
disallowedTools: ["Write"],
});
client = CortexCodeSDKClient(CortexCodeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "Bash(npm test:*)"],
disallowed_tools=["Write"],
))
allowedTools 항목에는 패턴이 포함될 수 있어요. 예를 들어 Bash(npm test:*)는 그 패턴과 일치하는 모든 Bash 명령을 자동 승인해요. disallowedTools 항목은 특정 도구를 완전히 차단해요.
이 규칙들은 canUseTool 콜백 전에 CLI가 평가해요. 도구가 허용 또는 금지 규칙과 일치하면 그 도구에 대해 콜백이 호출되지 않아요.
권한 모드와 결합
canUseTool 콜백은 권한 모드와 함께 작동해요. 둘 다 설정되면 CLI의 내장 권한 모드 필터가 먼저 실행되고 콜백이 승인이 필요한 나머지 도구 호출을 처리해요.
사용 가능한 권한 모드는 다음과 같아요.
| 모드 | 설명 |
|---|---|
default |
표준 권한 확인 사용. SDK 세션에서 allowedTools, disallowedTools 또는 canUseTool로 권한 확인된 도구 제어. |
autoAcceptPlans |
플랜 요청과 플랜 종료 확인을 자동 승인. 일반 도구 권한은 우회하지 않음. |
plan |
에이전트가 변경을 계획하지만 승인 없이는 실행하지 않음. canUseTool과 함께 플랜 모드 승인은 ExitPlanMode를 통해 라우팅됨. 그 요청을 거부하면 계획이 활성 상태로 유지되고, 승인하면 플랜 모드를 종료하며 이후 턴은 정상 권한을 재개함. |
bypassPermissions |
모든 권한 확인 건너뜀. 안전 플래그로 allowDangerouslySkipPermissions: true(TypeScript) 또는 allow_dangerously_skip_permissions=True(Python) 필요. |
세션 중 권한 모드 변경
세션 시작 후 권한 모드를 변경할 수 있어요. TypeScript는 Query와 CortexCodeSession 모두에 setPermissionMode()를 노출하고, Python은 CortexCodeSDKClient에 set_permission_mode()를 노출해요.
업데이트된 모드는 제어 요청이 처리된 후 이후 턴에 적용돼요. 이미 실행 중인 도구 호출을 소급해서 변경하지는 않아요.
const session = await createCortexCodeSession({
cwd: process.cwd(),
});
await session.setPermissionMode("plan");
await session.send("Review the repo and propose a plan before editing.");
await session.setPermissionMode("default");
await session.send("Now implement the approved change.");
from cortex_code_agent_sdk import CortexCodeAgentOptions, CortexCodeSDKClient
async with CortexCodeSDKClient(CortexCodeAgentOptions(cwd=".")) as client:
await client.set_permission_mode("plan")
await client.query("Review the repo and propose a plan before editing.")
await client.set_permission_mode("default")
await client.query("Now implement the approved change.")
훅
훅은 다양한 수명 주기 단계에서 도구 실행을 가로챌 수 있게 해줘요. canUseTool이 도구가 실행되는지만 제어하는 것과 달리 훅은 도구 결과를 검사하고, 컨텍스트를 주입하고, 에이전트 흐름을 제어할 수 있어요.
import { createCortexCodeSession } from "cortex-code-agent-sdk";
const session = await createCortexCodeSession({
cwd: process.cwd(),
hooks: {
PreToolUse: [
{
matcher: "Bash",
hooks: [
async (input, toolUseId, context) => {
console.log(`About to run Bash: ${input.tool_input?.command}`);
return {}; // Allow execution to proceed
},
],
},
],
PostToolUse: [
{
hooks: [
async (input, toolUseId, context) => {
console.log(`Tool ${input.tool_name} completed`);
return {};
},
],
},
],
},
});
from cortex_code_agent_sdk import CortexCodeSDKClient, CortexCodeAgentOptions, HookMatcher
async def log_bash(input_data, tool_use_id, context):
print(f"About to run Bash: {input_data.get('tool_input', {}).get('command')}")
return {}
async def log_completion(input_data, tool_use_id, context):
print(f"Tool {input_data.get('tool_name')} completed")
return {}
async with CortexCodeSDKClient(CortexCodeAgentOptions(
hooks={
"PreToolUse": [HookMatcher(matcher="Bash", hooks=[log_bash])],
"PostToolUse": [HookMatcher(hooks=[log_completion])],
},
)) as client:
...
훅 이벤트
| 이벤트 | 발생 시점 |
|---|---|
PreToolUse |
도구 실행 전. 실행을 차단하거나 입력을 수정할 수 있음. |
PostToolUse |
도구가 성공적으로 완료된 후. |
UserPromptSubmit |
사용자 프롬프트가 제출될 때. |
Stop |
에이전트가 중지될 때. |
SubagentStop |
하위 에이전트가 중지될 때. |
Notification |
에이전트가 알림을 발생시킬 때. |
PermissionRequest |
도구 권한 요청이 발생할 때. |
PreCompact |
컨텍스트 압축 전. |
훅 항목의 matcher 필드는 선택 사항이에요. 제공되면 이벤트의 매치 값으로 필터링해요: PreToolUse, PostToolUse, PermissionRequest는 도구 이름, Notification은 알림 유형, PreCompact는 트리거. 생략되면 해당 이벤트의 모든 값에 대해 훅이 발생해요.
전체 훅 입력 및 출력 유형 정의는 TypeScript SDK 참조와 Python SDK 참조를 참고하세요.
접근 방식 선택
| 접근 방식 | 사용 시기 |
|---|---|
안전 플래그가 있는 permissionMode: "bypassPermissions" |
도구 호출을 검토할 필요가 없는 샌드박스 환경, CI 파이프라인 또는 완전히 신뢰하는 시나리오. allowDangerouslySkipPermissions: true(TypeScript) 또는 allow_dangerously_skip_permissions=True(Python) 필요. |
allowedTools / disallowedTools |
권한 정책을 허용 또는 차단된 도구의 정적 목록으로 표현할 수 있을 때 |
canUseTool 콜백 |
실행 전에 도구 호출을 감사, 필터링 또는 수정해야 하는 프로덕션 시스템 |
| 권한 모드만(콜백 없음) | autoAcceptPlans로 충분하고 사용자 정의 콜백 처리가 필요 없을 때 |
| 훅 | 허용/거부 결정 너머 도구 결과를 관찰하거나 반응하고, 컨텍스트를 주입하거나, 에이전트 흐름을 제어해야 할 때 |
참고: SDK는 기본적으로 권한을 우회하거나 대화형으로 프롬프트하지 않아요. 권한 확인된 요청이 SDK에 도달했는데
canUseTool이 제공되지 않으면 요청은 실패해요. 권한을 우회하려면 적절한 안전 플래그와 함께permissionMode: "bypassPermissions"를 명시적으로 설정해야 해요.