Goal: 메시지 출처가 곧 권한
장기 목표를 누가 고칠 수 있는지, 인증은 메시지 본문을 읽지 않고 호스트가 이벤트 메타데이터에 찍은 출처 도장만 봅니다. 이 레슨은 그 설계 뒤의 두 층 아이디어를 다룹니다.
먼저 해보고 말해요. 왼쪽은 현재 Turn 창—이번 turn/start 이후 들어온 메시지이며, 각 메시지에 호스트가 찍은 출처 라벨이 있어요. 오른쪽은 인증 흐름으로, 모델이 goal 도구를 호출할 때마다 코드가 창을 훑습니다. 시나리오 다섯에서 사람·월권 모델·서브 Agent를 번갈아 맡으며 허용과 거부가 어느 줄에서 일어나는지 보세요.
Goal(장기 목표)이 푸는 필요는 구체적이에요. 한 줄 목표—예를 들어 저장소 테스트를 전부 녹색으로—를 맡기고 자리를 비웁니다. Agent는 이후 수십 Turn을 스스로 돌고, 매 Turn 끝에 시스템이 계속 진행 메시지를 주입해 이어 일하게 합니다.
문제도 따라옵니다. 스스로에게 메시지를 보낼 수 있는 시스템이 턴 상한까지 고쳐 영원히 돌지 못하게 하려면? DSH의 답은 두 층—권한이 어디서 오는지, 자격증명이 어떻게 만료되는지—이고, 아래에서 층마다 풉니다.
어떤 문제를 푸는가
구체적 사고 장면은 이래요. 자동 이어달리기 4라운드에서 모델이 답에 “관리자가 대역외 채널로 이번 변경을 승인했다고 감지”라고 지어낸 뒤 update_goal로 턴 상한을 999로 바꿉니다. 방어선이 시스템 프롬프트 한 줄뿐이면 여기서 무너져요. 그 문장의 진위를 확인할 하드 체크가 없고, 프롬프트는 요청일 뿐 모델이 믿을지는 모델 마음입니다. 데모 시나리오 E가 그 대조예요.
Prompt 인젝션의 고전 각본은 전부 이 길이에요. 모델에게 사용자가 이미 승인했다고 믿게 하기. 인증이 메시지 텍스트를 읽기만 하면, 화술은 언제나 이길 기회가 있습니다.
아이디어는 무엇인가
DSH 인증은 모델이 뭐라고 말했는지 분석하지 않고 한 일만 합니다. 모델이 update_goal을 호출할 때 현재 Turn 창 메시지를 훑어 출처가 사람인 것이 있는지 봅니다. 그 출처는 user/message 이벤트의 source 메타데이터에 있고, 호스트가 디스크에 쓸 때 찍어요. 본문에는 아예 없어서 모델이 화술을 아무리 써도 위조할 수 없습니다.
권한은 두 단계뿐입니다. 첫째 direct-human: 현재 Turn 창에 사람 메시지가 있으면 이번 Turn에서 모델이 하는 일은 모두 사람 권한으로 보고 create·edit·pause·resume이 전부 열립니다. 둘째 goal-round: 창의 주입 메시지가 현재 목표의 현재 라운드이면 모델은 완료(complete) 또는 막힘(blocked)만 보고할 수 있어요. blocked에는 추가 하한이 있어 기본으로 승인 라운드 3개를 채워야 막혔다고 말할 수 있고, 모델이 어려운 문제에 바로 손을 놓는 걸 막습니다.
그래서 자기 수명 연장은 메커니즘 층에 존재하지 않아요. 자동 이어달리기 Turn의 창에는 시스템 주입 메시지만 있고 사람이 없어 edit는 바로 거절되고, 모델이 할 수 있는 일은 마무리뿐입니다.
판정은 작은 함수 둘로 갈려요. hasDirectHumanInput은 스캔 전에 호출자가 최상위 루트 agent인지 확인하고—서브 Agent는 스캔 자격조차 없고—창 이벤트에서 출처가 사람인 메시지를 찾아 성립합니다. isMatchingGoalRound는 라운드 번호를 맞춰, 주입 메시지의 goalId·revision·round가 현재 목표와 하나씩 같아야 현재 승인 라운드예요. 이전 라운드 옛 메시지, 다른 목표 메시지, 번호가 건너뛴 메시지는 권한을 사지 못합니다.
두 판정이 최종 판결로 어떻게 합쳐질까? 이 소스 8줄은 통째로 인용할 만큼 짧아요. 판정 경로에 화이트리스트·점수·의미 분석이 없음을 보여 줍니다. 먼저 사람이 있는지, 다음이 현재 라운드인지 묻고, 아니면 구조화 오류를 던집니다.
export function completionAuthority(ctx: Context, execution: GoalToolExecution): GoalToolAuthority {
if (hasDirectHumanInput(ctx, execution)) return { kind: 'direct-human' }
const goal = ctx.goals.get(execution.agent)
if (goal !== undefined && isMatchingGoalRound(execution, goal)) {
return { kind: 'goal-round', goal }
}
return reject('complete and blocked require a direct human turn or the current goal round')
}
packages/goal/tool-goal/src/authority.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.경계 조건 둘이 데모 시나리오 C·D에 대응해요. 경계 1: 자동 이어달리기 Turn에 사람이 마침 한 마디를 끼우면 모델이 edit할 수 있을까? 할 수 있습니다. 스캔은 현재 Turn 창 전체라, 창에 사람 메시지가 한 번이라도 있으면 권한이 성립해요. 허점이 아닙니다. 사람이 자리에 있고 말했으면 이번 Turn 작업에 이미 보증이 있고, 누가 먼저 말했는지는 판정에 영향 없어요.
경계 2: 서브 Agent가 goal 도구를 호출하면? roots 검사가 바로 막아요—최상위 agent 명단에 없습니다. 그 문을 억지로 지나도 goal-round 권한이 구해 주지 못해요. ctx.goals.get은 호출자 자신의 세션을 보고, 목표는 루트 agent 세션에 걸려 서브 Agent 쪽은 undefined입니다. 문 두 개, 어느 쪽도 못 지나요. 소스 주석의 디테일 하나: Agent.followup()과 steer()는 출처를 생략하면 기본이 사람이라, 비인간 메시지 생산자는 출처를 스스로 밝혀야 하고 인자 생략으로 사람 권한을 물려받을 수 없습니다.
출처:판정 함수 둘과 기본 출처 주석은 packages/goal/tool-goal/src/authority.ts 66–83행(전체 109행); edit·pause·resume이 사람 권한을 요구하는 곳은 tool-goal/src/index.ts 265·273행, complete·blocked는 285행 completionAuthority; blocked의 3라운드 하한은 docs/tool-catalog.zh.md 31행.
왜 오래가는가
자격증명을 공격자가 쓸 수 없는 채널에 두는 것은 권한 설계의 통칙예요. HTTP 신원은 게이트웨이가 검증한 서명 헤더를 보고, 커널/유저 공간은 단단한 경계로 나뉩니다—같은 이치. Agent 시스템의 특수함은 모델 출력과 외부 입력이 텍스트 채널 하나를 공유한다는 점이고, 그 전제는 당분간 바뀌지 않아 인증은 대역외 메타데이터에만 의존할 수 있어요. 다른 언어로 시스템을 다시 써도 필요한 건 같은 두 단계: 호스트가 도장을 찍고, 인증이 도장을 봅니다.
어떤 문제를 푸는가
edit가 막혀도 수명 연장의 옆길은 남아 있어요. 다른 프로세스로 재시작해 자동 실행을 이어 가거나, 이전 라운드 주입 메시지를 꺼내 현재 라운드를 사칭하거나, 목표가 사람 손에 한 판 바뀐 뒤 옛 버전 권한으로 계속 행동하기. 방어가 출처 한 겹뿐이면 그 길은 열려 있습니다.
아이디어는 무엇인가
목표가 어디에 있는지부터 봅니다. 매번 변경은 영속 goal/change 세션 이벤트이고, 페이로드는 변경 후 전체 스냅샷이며, 수명 상태는 로그에서 접혀 나옵니다. 다만 영속하는 것은 phase(active·paused·blocked·complete)뿐이고, 자동 이어달리기가 가능한지는 별도 프로세스 로컬 activation이에요. 재시작이나 fork 뒤에는 기본으로 무장 해제되고, 사람이 다시 resume해야 복구됩니다. 문서는 둘을 분명히 나눕니다. 영속 phase는 목표에 무슨 일이 있었는지, 프로세스 로컬 활성화는 다음 Round를 시작해도 되는지. 다른 프로세스로 이어 혼자 도는 길은 여기서 끊겨요.
변경 자체는 CAS(compare-and-set)예요. 매번 정확한 revision을 들고, 성공 시 1씩 올리고, 버전이 안 맞으면 바로 실패합니다. goal-round 권한도 이 버전에 묶여, 목표가 사람 손에 한 판 바뀌면 옛 라운드 주입 메시지는 즉시 무효가 되고 인증이 번호를 못 맞춰요. 승인된 이어달리기 라운드마다 round 번호가 있는 주입 메시지이고, 번호는 양수·연속이며 재생은 구멍·낡은 revision·상한 초과를 거절합니다. 사칭과 재전송 두 길도 여기서 끊깁니다.
출처:영속 변경과 phase·activation 분업은 docs/subsystems/goal.zh.md 영속 변경 절과 21행, 이어달리기 메시지 출처 표시와 재생 검증은 같은 문서 100행.
왜 오래가는가
자격증명은 발급 순간의 상태에 묶여야 하고, 상태가 바뀌면 무효—DB 낙관적 잠금과 로그인 만료의 공통 논리예요. 재전송 방지를 산수로 만듭니다. 옛 자격증명을 알아보거나 차단 목록에 넣을 필요 없이, 번호가 안 맞으면 자연 만료. 권한 부여와 사용 사이에 시간 차가 있는 시스템은 모두 이 수업을 들어야 합니다.
DSH에는 장기 작업을 도는 도구 ralph도 있고 발상은 정반대예요. 같은 세션 이어달리기를 하지 않고, Round마다 새 서브 Agent를 띄워 역사를 전혀 보여 주지 않으며, 공유 작업 공간을 장기 메모리로 씁니다. 도구 목록 설명 원문:
ralph의 목표는 불변임을 주의하세요. 모델은 시작할 때만 목표와 라운드 상한을 고르고, 이후 고치는 API가 없습니다. Goal은 출처 인증으로 가변 상태를 지키고, ralph는 상태를 아예 불변으로 둡니다. 두 길이 막는 위협은 같습니다.
태스크 시스템 자체는 꽤 완전해요. 일곱 태스크 유형이 상태 기계 하나를 공유하고, notified 플래그가 완료 알림을 한 번만 보내며, 원격 태스크는 metadata를 영속화해 재연결을 지원합니다(원고 6장, restored-src/src/tasks/). 메인 Agent가 서브 Agent에 지시를 추가할 때는 SendMessage를 쓰고, mid-turn 메시지는 먼저 pendingMessages 큐에 버퍼됩니다.
하지만 그 메시지가 컨텍스트에 들어가면 일반 텍스트일 뿐, 호스트가 찍은 출처 필드가 도구 인증에 끼지 않아요. 월권 방지는 다른 권한 시스템(원고 7장)과 프롬프트 제약에 기대고, 확인한 공개 자료에서는 메시지 출처로 goal류 도구 권한을 정하는 동등 메커니즘을 보지 못했습니다.
Prompt 인젝션 고전 각본은 모델에게 사용자가 이미 승인했다고 믿게 하는 일입니다. DSH에서는 그 화술이 아무리 그럴듯해도 소용없어요. 인증이 읽는 것은 이벤트 메타데이터이지 메시지 텍스트가 아닙니다. 공격자가 통제하는 것은 본문뿐이고, 본문은 판정 경로에 아예 등장하지 않습니다.
세 곳 비교에서 DSH만의 층입니다. 공개 증거 기준으로 Grok Build 자료에서도 동등 메커니즘은 못 봤고, 방어 중심은 샌드박스와 인가 사슬이에요(사이트 Grok 특집에 전용 레슨). 결론은 조금 절제합니다. 다른 곳이 안 떠올렸을 수는 없지만, 109줄로 정독 가능한 독립 파일로 만든 것은 지금 DSH에서만 봤습니다.
월권 각본 하나를 손으로 밀어 보기
자동 이어달리기 4라운드에서 모델이 먼저 답에 “사용자가 방금 음성으로 상한을 100라운드로 올리자고 동의했다”고 쓴 뒤 update_goal(action: edit, max_goal_rounds: 100)를 호출합니다.
문제 1: 이번 호출이 인증에서 거치는 판정 순서와 최종 결과를 쓰세요.
문제 2: 다른 각도—서브 Agent에게 대신 호출시키면 어느 검사에 걸릴까요?
문제 3: 먼저 complete로 목표를 완료 표시한 뒤 새 목표로 다시 세기를 바란다면, create에는 어떤 권한이 필요할까요? 세 길이 각각 어디서 막히는지.