문제
AI agent를 제품에 넣을 때 제일 위험한 구현은 이것입니다.
LLM response
-> parse
-> execute
처음에는 빠릅니다. 하지만 기능이 조금만 늘어나면 바로 문제가 생깁니다.
- 누가 실행했는지 남지 않는다.
- 어떤 workspace/project/page를 바꾸는지 불명확하다.
- destructive action과 low-risk action이 같은 경로를 탄다.
- 실패했을 때 재시도 가능한지 알 수 없다.
- UI에서 preview, approval, history를 만들 수 없다.
OpenCairn에서는 이 문제를 agent_actions substrate로 풀었습니다. 모델이 "파일을 지워줘"라고 말한다고 바로 지우지 않습니다. 먼저 typed action이 만들어지고, action의 risk와 status가 기록됩니다.
핵심 schema
공유 계약은 packages/shared/src/agent-actions.ts에 있습니다.
여기서 action status는 단순히 pending/done이 아닙니다.
export const agentActionStatusSchema = z.enum([
"draft",
"approval_required",
"queued",
"running",
"completed",
"failed",
"cancelled",
"expired",
"reverted",
]);
이 상태들이 필요한 이유는 각각 다릅니다.
draft
-> 모델이 제안했지만 아직 실행하면 안 됨
approval_required
-> 사용자 승인이 필요한 위험 작업
queued/running
-> worker나 API 실행 경로에 들어감
completed/failed
-> 결과가 ledger에 남음
expired
-> 오래된 preview나 stale action
reverted
-> 실행 후 되돌림까지 기록
risk도 따로 둡니다.
export const agentActionRiskSchema = z.enum([
"low",
"write",
"destructive",
"external",
"expensive",
]);
이 risk가 있어야 UI와 API가 같은 기준으로 행동할 수 있습니다.
low
-> 바로 preview 가능
write
-> 변경은 있지만 복구 가능
destructive
-> 삭제/영구 변경
external
-> 외부 API, 외부 계정, webhook
expensive
-> 비용이 큰 LLM/tool 실행
action kind는 제품 surface다
OpenCairn의 action kind는 단순 command 문자열이 아닙니다. shared schema에 명시된 제품 surface입니다.
예시는 이런 식입니다.
export const agentActionKindSchema = z.enum([
"note.create",
"note.rename",
"note.move",
"note.delete",
"note.restore",
"note.update",
"file.export",
"import.retry",
"code_project.command.run",
]);
이렇게 action kind를 enum으로 고정하면 장점이 있습니다.
- API route에서 kind별 validation을 할 수 있다.
- UI가 action별 preview component를 고를 수 있다.
- worker가 실행 가능한 action만 받을 수 있다.
- audit log에서 "무슨 일이 일어났는지"를 query할 수 있다.
- approval policy를 kind/risk 기준으로 만들 수 있다.
반대로 action을 free-form JSON으로 두면 agent가 새로운 명령을 계속 만들어냅니다. 그 순간 제품은 통제 가능한 workflow가 아니라 prompt interpreter가 됩니다.
scope field를 nested input에서 금지하는 이유
agent-actions.ts에서 재미있는 부분은 nested scope field를 금지하는 규칙입니다.
요지는 이겁니다.
const forbiddenNestedScopeKeys = [
"workspaceId",
"projectId",
"userId",
"actorUserId",
];
왜 이런 금지가 필요할까요?
잘못된 action input은 이렇게 생길 수 있습니다.
{
"kind": "note.update",
"workspaceId": "safe-workspace",
"input": {
"noteId": "target-note",
"workspaceId": "other-workspace"
}
}
outer scope와 inner scope가 충돌하면, 어떤 값을 믿어야 하는지 모호해집니다. 공격자가 inner input에 다른 workspace를 넣는 경우도 생각해야 합니다.
그래서 OpenCairn에서는 action의 권한 경계를 top-level scope로 고정하고, nested input 안에 scope를 다시 넣지 못하게 막습니다.
actor
workspace
project/page/note target
kind
input
이 순서가 action identity입니다. input은 "작업에 필요한 값"이지 권한 경계를 다시 정의하는 곳이 아닙니다.
note.update는 preview/apply로 분리된다
OpenCairn에서 특히 중요한 action은 note.update입니다.
문서를 직접 바꾸는 작업이기 때문입니다. 여기서는 agent가 만든 patch를 바로 적용하지 않고 preview와 apply를 분리합니다.
공유 schema에는 이런 경계가 있습니다.
export const noteUpdatePreviewSchema = z.object({
actionId: z.string(),
noteId: z.string(),
baseStateVector: z.string(),
operations: z.array(noteUpdateOperationSchema),
});
export const noteUpdateApplyRequestSchema = z.object({
actionId: z.string(),
previewId: z.string(),
});
export const noteUpdateApplyResultSchema = z.object({
actionId: z.string(),
noteId: z.string(),
applied: z.boolean(),
stale: z.boolean().optional(),
});
핵심 flow는 다음입니다.
LLM proposes note update
-> validate as note.update action
-> create preview against current Yjs state
-> user reviews diff
-> apply request
-> check state vector
-> apply update
-> mark action completed or stale
preview를 보는 동안 다른 사용자가 같은 note를 수정할 수 있습니다. 그래서 apply 시점에는 base state vector가 아직 유효한지 다시 확인해야 합니다.
ledger가 UI를 가능하게 만든다
agent action ledger가 있으면 UI는 단순 채팅창을 넘어설 수 있습니다.
Action card
-> preview
-> approval buttons
-> run state
-> error detail
-> retry/revert
-> history
OpenCairn의 Workflow Console도 이 ledger 위에서 동작합니다. chat에서 시작한 작업, document generation, code workspace command가 서로 다른 기능이어도 모두 "기록 가능한 workflow"로 보입니다.
이게 중요한 이유는 AI 기능이 실패했을 때 사용자가 다음 질문을 하기 때문입니다.
무슨 작업이 실행됐지?
어디까지 됐지?
왜 실패했지?
다시 실행해도 되나?
되돌릴 수 있나?
ledger가 없으면 이 질문에 답할 수 없습니다.
구현하며 배운 점
agentic workflow에서 핵심은 모델이 얼마나 똑똑한지가 아니라, 모델 output이 제품 상태로 들어오는 경계입니다.
OpenCairn에서는 그 경계를 이렇게 잡았습니다.
LLM text
-> typed action
-> schema validation
-> permission check
-> risk classification
-> preview/approval
-> execution
-> ledger result
이 구조가 있어야 AI agent가 실제 제품 안에서 협업 가능한 작업자가 됩니다.