Sep 14, 2026BY SEORY0
publicAI SECURITY
OpenClaw 20 비교 분석으로 본 에이전트 권한 관리 아키텍처
[0] 들어가며
OpenClaw는 메신저와 브라우저에서 AI 에이전트에 작업을 요청하고, 연결된 도구로 이를 수행하게 하는 오픈소스 에이전트 하네스다. Telegram이나 Discord 같은 채널과 웹 UI를 하나의 Gateway에 연결해 사용할 수 있다. [1]이번 글에서 살펴볼 OpenClaw 2.0은 설치 과정을 단순화하고 브라우저 앱을 재구성한 대규모 업데이트다. 여러 사람이 같은 작업에 참여하거나, 대화 맥락을 유지한 채 작업을 넘길 수 있는 공유 세션도 주요 변화 중 하나다. [2]
에이전트가 파일을 수정하고, 명령을 실행하고, 다른 에이전트에게 작업을 넘기기 시작하면 권한 관리도 함께 복잡해진다. 처음 요청한 사용자가 누구인지 확인하는 것만으로는 부족하다. 작업을 넘겨받은 실행이 어떤 권한을 이어받았는지, 응답을 기다리는 사이 승인이 취소되지는 않았는지, credential을 어느 서비스에 사용할 수 있는지까지 판단해야 한다.
OpenClaw 2.0을 보면서 가장 궁금했던 것도 이 부분이었다. 기존 OpenClaw는 에이전트의 행동을 어디에서 제한했고, 2.0에서는 그 제한을 어떤 코드와 상태로 표현하게 되었을까?
먼저 짚어 둘 점이 있다. 공식 문서에서 OpenClaw 2.0은
v2026.8.1을 가리킨다. 이 글 역시 v2.0.0 같은 태그가 아니라 이 릴리스를 기준으로 비교한다.
그림 1. 공식 소개 글에 실린 2.0 브라우저 앱. 화면 출처: [2]
이 글은 다음 두 소스 스냅샷을 비교해, OpenClaw의 권한이 어떻게 구성되어 있고 2.0에서 보안을 강화하기 위해 어떤 기능이 추가되었는지 살펴본다.
| 구분 | 비교 버전 | 고정 커밋 |
|---|---|---|
| 기존 OpenClaw | v2026.7.1 | 2d2ddc43d0dcf71f31283d780f9fe9ff4cc04fe4 [3] |
| OpenClaw 2.0 | v2026.8.1 | ea806575e6450e4d1efdfc72c19f04be982a1b9b [4] |
[1] 먼저, 권한이라는 말을 나누어 보자
OpenClaw의 권한을 읽을 때는 먼저 다음 네 가지 질문을 구분해 두는 편이 좋다.
| 계층 | 판단하는 질문 | 대표 개념 |
|---|---|---|
| 인증·Gateway 인가 | 누가 접속했고 어떤 Gateway 기능을 호출할 수 있는가 | role, operator scope |
| 도구 정책 | 이번 에이전트 실행에 어떤 도구를 제공할 것인가 | tool profile, allow/deny, 상속 정책 |
| 실행 승인 | 이 명령을 현재 조건에서 실행해도 되는가 | exec security, approval |
| 실행 환경·파일 경계 | 도구가 어디에서 실행되고 어떤 파일에 접근하는가 | sandbox, workspace containment |
예를 들어
exec 도구가 도구 목록에 있다고 해서 모든 명령이 승인된 것은 아니다. 반대로 명령 하나를 승인했다고 해서 프로세스에 새로운 OS 격리 경계가 생기는 것도 아니다. 기존 문서에서도 sandbox, tool policy, elevated exec를 서로 다른 통제로 설명하고 있다. [5]모델은 어떤 도구를 어떤 인자로 사용할지 제안할 뿐이다. 그 제안을 실제 동작으로 바꿀지는 호스트의 정책 코드가 결정한다. 프롬프트에 적힌 지시와 프로그램이 집행하는 권한을 구분해서 봐야 하는 이유다.
두 버전을 같은 기준 위에 놓고 보면 변화가 더 잘 보인다.
| 항목 | v2026.7.1에서 확인되는 기반 | v2026.8.1에서 살펴볼 변화 |
|---|---|---|
| 도구 노출 | 여러 정책을 순서대로 적용하는 필터 | 기존 필터를 유지하면서 세션 권한 정책과 결합 |
| 세션 실행 설정 | execSecurity, execAsk 저장 | permissionMode 중심으로 전환, 기존 필드의 쓰기 거절 |
| 실행 수명 | run context, owner token, lifecycle generation | admitted run과 delegated authority lease의 결합 |
| 반복 실행 승인 | 실행 승인·allowlist 체계 | 자동화 작업과 정확한 operation에 묶인 standing grant |
| 시크릿 전달 | 모델 제공자 인증에 sentinel과 egress 치환 사용 | 보호된 저장소의 시크릿에 실행별·목적지별 egress 경로 추가 |
아래에서는 이 표의 각 항목을 실제 코드로 확인한다.
[2] 기존 OpenClaw는 어떻게 제한했는가
2.1. 도구 목록을 여러 번 거른다
기존
buildDefaultToolPolicyPipelineSteps()는 profile, provider profile, global, agent, group, sender 정책을 이 순서대로 구성한다. 실제 도구를 조립하는 코드에서는 그 뒤에 sandbox, owner-only, subagent, inherited 정책이 추가로 이어진다. [6], [7]정책을 적용하는 루프의 핵심 부분은 짧다.
const before = filtered;
filtered = filterToolsByPolicy(before, expanded);
params.onFilter?.({ step, policy: expanded, before, after: filtered });
출처:
tool-policy-pipeline.ts [8]각 단계가 입력으로 받는 것은 최초의 전체 도구 목록이 아니라 직전 단계의 결과다. 따라서 이 필터 루프 안에서는 뒤쪽 정책이 앞 단계에서 제거된 도구를 다시 살려낼 수 없다. 다만 이 사실을 OpenClaw의 모든 도구 조립 경로에 대해 성립하는 것처럼 확대 해석해서는 안 된다. 필터 바깥에서 추가되는 도구와, 그 도구가 자체적으로 수행하는 인가는 따로 살펴봐야 한다.
부모 에이전트의 제한을 자식에게 넘기는 코드도 이미 있었다. 제한적인 allow 정책을 상속해야 하는 경우, 최종 필터 결과의 도구 이름을 보관해 두었다가 자식 세션의
inheritedToolAllow와 inheritedToolDeny로 전달한다. 그러므로 2.0의 변화를 ‘이제야 자식에게 권한이 상속된다’는 식으로 설명하면 이 기존 구현을 놓치게 된다. [9], [10]2.2. Gateway scope와 exec approval은 별도로 판단한다
기존 Gateway의
authorizeGatewayMethod()는 연결의 role과 scope를 읽어 메서드 호출을 허용할지 판단한다. operator.admin, operator.read, operator.write, operator.approvals 같은 scope도 이미 존재했다. 이 계층은 Gateway를 호출하는 주체의 권한을 다루는 것으로, 앞서 본 도구 필터와는 다른 종류의 판단이다. [11], [12]명령 실행에는 또 다른 정책 결합 규칙이 있다. 기존 코드의 두 함수를 보면 어느 방향으로 제한하는지 알 수 있다.
export function minSecurity(a: ExecSecurity, b: ExecSecurity): ExecSecurity {
const order: Record<ExecSecurity, number> = { deny: 0, allowlist: 1, full: 2 };
return order[a] <= order[b] ? a : b;
}
export function maxAsk(a: ExecAsk, b: ExecAsk): ExecAsk {
const order: Record<ExecAsk, number> = { off: 0, "on-miss": 1, always: 2 };
return order[a] >= order[b] ? a : b;
}
출처:
exec-approvals.ts [13]두 값을 결합할 때
security는 더 좁은 실행 범위를, ask는 더 강한 승인 요구를 택한다. 기존 exec-defaults.ts는 host approval 정책을 설정 정책과 결합할 때 이 두 함수를 사용했다. 승인 파일을 반영하는 경로에서는 실행 범위가 넓어지는 방향으로 병합하지 않겠다는 의미다. [14]정리하면 기존 버전에도 이미 여러 겹의 통제가 있었다. 2.0을 이해하려면 그 위에 어떤 상태와 판단 단위가 새로 추가되었는지를 봐야 한다.
[3] 2.0의 세션 권한 모드
3.1. 실행 설정을 세션의 파일 경계와 연결한다
기존 세션은
execSecurity와 execAsk를 저장하고 갱신했다. 2.0의 세션 갱신 코드는 이 두 필드가 요청에 포함되면 오류를 반환하고, 대신 permissionMode를 사용하라고 안내한다. UI에 선택지 하나가 추가된 정도의 변화가 아니라, 정책을 저장하고 해석하는 형태 자체가 바뀐 것이다. [15], [16]새 모드가 exec 정책에 어떻게 대응되는지는 다음 코드에 드러난다.
const EXEC_MODE_BY_PERMISSION_MODE = {
"read-only": "deny",
guarded: "ask",
workspace: "auto",
full: "full",
} as const satisfies Record<PreparedSessionPermissionPolicy["mode"], ExecMode>;
출처:
session-permission-exec-mode.ts [17]파일 도구까지 함께 놓고 보면 다음과 같이 정리된다.
| 세션 모드 | OpenClaw가 관리하는 파일 도구 | 명령 실행의 승인 경로 |
|---|---|---|
read-only | 세션 root 안에서 읽기, 변경 도구 제외 | exec 거부 |
guarded | 세션 root 안에서 읽기·쓰기 | allowlist로 허용되지 않은 실행은 사람이 검토 |
workspace | 세션 root 안에서 읽기·쓰기 | LLM 검토, 필요할 때 사람에게 전달 |
full | 이 모드의 파일 경계 제한 없음 | 이 모드 자체의 승인 요구 없음 |
이 표는 OpenClaw가 직접 관리하는 도구에 대한 정책이다. 외부 harness의 도구나 OS sandbox까지 같은 의미로 바뀐다는 뜻은 아니며, 다른 tool allow/deny 정책과 sandbox 제한은 여전히 독립적으로 적용된다. [18], [19]
실제 도구 구성 단계에서는
readOnly 값을 전달하고, apply_patch의 활성화 조건에도 이를 반영한다. core-coding-tools.ts에서 edit와 write를 생성하는 코드 역시 !options.readOnly 조건 아래에 있다. 모델에게 읽기 전용이라고 설명하는 데서 그치지 않고, 변경 도구를 구성하는 코드 자체에 제한을 심어 두는 방식이다. [20], [21]sessionRoot도 단순한 현재 디렉터리 문자열과는 구분된다. 세션에 기록된 canonical root가 파일 경계가 되고, root가 없으면 선택된 에이전트의 workspace를 사용한다. worktree 안의 하위 디렉터리를 cwd로 잡더라도 경계는 worktree 전체일 수 있다. 따라서 파일 접근을 분석할 때는 상대 경로의 출발점과 허용된 루트를 각각 따로 확인해야 한다. [22]3.2. 한 turn의 설정으로 세션 권한을 넓히지 않는다
resolveSessionPermissionExecPolicy()는 세션 모드로부터 기본 정책을 만든 뒤, 이번 실행의 override를 여기에 결합한다. 핵심 연산은 다음과 같다. const security = minSecurity(
base.security,
minSecurity(override.security, overrides?.security ?? "full"),
);
const ask = maxAsk(base.ask, maxAsk(override.ask, overrides?.ask ?? "off"));
출처:
session-permission-exec-mode.ts [23]앞에서 본
minSecurity와 maxAsk가 여기서는 한 turn의 override가 명시적인 세션 모드를 완화하지 못하게 막는 용도로 쓰인다. 세션이 허용한 범위 안에서 더 좁히는 것은 가능하지만, 이번 요청의 실행 설정만으로 세션보다 넓은 권한을 만들어 내는 것은 이 함수가 허용하지 않는 방향이다.다만 관리자가 명시적으로 선택한
full은 따로 봐야 한다. scope 검사가 적용되는 세션 생성·변경 경로에서 full을 지정하려면 operator.admin이 필요하다. 그리고 명시적인 full은 host approval-file floor, 즉 host 승인 하한에 대한 의도된 예외다. 이 예외는 최종 security가 full로 유지되는 동안만 적용되며, turn override로 security를 좁히면 host 승인 하한도 다시 살아난다. 그래서 ‘항상 가장 제한적인 정책이 이긴다’는 한 문장으로 전체를 요약하면 이 관리자의 선택을 빠뜨리게 된다. [24], [25]업그레이드 역시 같은 관점에서 봐야 한다. 모드를 지정하지 않은 세션은 기존 global·agent 설정을 그대로 따른다. 따라서 2.0으로 올렸다는 사실만으로 모든 세션이
guarded로 바뀌지는 않는다. 기존 execSecurity·execAsk의 마이그레이션도 명시적 full 권한을 자동으로 부여하는 방식으로 설명되어 있지는 않다. [26][4] 실행 ID와 실행 권한을 구분한다
에이전트의 작업은 동기 함수 하나로 끝나지 않는다. 승인 응답을 기다리기도 하고, worker로 넘어가기도 한다. 이때 요청을 처음 받았을 때의 권한이 그대로 유효하다고 가정하면, 실행의 수명과 권한의 수명이 어긋나는 상황이 생길 수 있다.

그림 2. 공식 소개 글의 공동 작업 공간 예시. 화면 출처: [2]
기존 버전에도
runId, owner token, lifecycle generation을 이용해 실행 context를 관리하는 코드가 있었다. 2.0은 이를 admitted-run authority라는 더 명시적인 구조와 연결한다. [27]새로 추가된
OperationalRunInstanceRef는 instanceId와 runId를 가진다. 그런데 이 타입 바로 위의 주석은 이 값들을 identity나 authorization의 근거로 사용하지 말라고 명시한다. 상관관계 추적용 식별자와 실제 권한의 증거를 분리한 것이다. 실제 authority는 AdmittedRunContext를 키로 하는 WeakMap에 연결되고, registry가 실행 instance, claim, lifecycle generation의 유효성을 확인한다. [28], [29]resolveAdmittedRunActiveAssertion()의 본문을 보자. const operationalRunInstance = context.operationalRunInstance;
const authority = getAdmittedRunDelegatedAuthority(context);
if (!authority) {
return undefined;
}
return () => {
if (
signal?.aborted ||
context.operationalRunInstance !== operationalRunInstance ||
getAdmittedRunDelegatedAuthority(context) !== authority
) {
throw new Error("admitted run authority is no longer active");
}
};
출처:
admitted-run-context.ts [30]이 함수는 현재 실행 instance와 authority를 캡처해 둔 검사 함수를 돌려준다. 호출자는 비동기 작업이 끝난 뒤에도 이 함수를 호출해 같은 권한이 아직 살아 있는지 다시 확인할 수 있다. 같은
runId를 들고 있다는 사실만으로 판단을 끝내지 않는 구조다.worker payload를 준비하는 경로는 이 assertion을 얻지 못하면 작업을 거절하고, 얻은 경우에도 곧바로 한 번 실행해 확인한다. worker 실행 쪽에서는 authority 종료와 turn claim 종료를 취소 신호에 연결해 둔다. 연결된 worker가 아직 살아 있는지와, 그 worker가 이번 작업을 계속할 권한이 있는지를 별개의 문제로 관리하는 셈이다. [31], [32]
또 하나 눈에 띄는 점은, 실행 identity의 감사 정보 수집이 꺼져 있어도 authority binding은 그대로 수행된다는 것이다. 로그를 남기는 기능과 실제 인가 상태를 유지하는 기능이 같은 스위치에 묶이지 않도록 분리되어 있다. [33]
[5] 반복 승인은 어떤 작업에 대한 승인인가
자동화 작업이 돌 때마다 같은 명령에 대해 승인을 요구하면 사용성이 떨어진다. 그렇다고 한 번 받은 승인을 이후의 모든 실행에 그대로 적용하면 승인의 범위가 모호해진다.
2.0의 cron standing grant는 이 문제를 작업 단위로 풀어낸다. grant는
agentId, cronJobId, jobConfigRevision, operationBinding을 하나로 묶고, 자신을 발급한 원래 approval을 가리킨다. 별개의 무제한 권한을 새로 발급하는 것이 아니라, 기존 approval에서 파생된 재사용 권한으로 취급하는 것이다. [34]명령 실행의 operation binding을 만드는 코드는 다음과 같다.
export function buildCronExecOperationBinding(params: {
command: string;
cwd: string | null | undefined;
env: Record<string, string> | undefined;
}): string {
return stableStringify({
v: 1,
command: params.command.trim(),
cwd: params.cwd?.trim() || null,
envHash: buildSystemRunApprovalEnvBinding(params.env).envHash,
});
}
출처:
operator-approval-standing-grants.ts [35]여기서 ‘같은 작업’을 판단하는 기준은 trim한 명령 문자열, 작업 디렉터리, 그리고 전달된 환경 override의 binding이다. 실행 파일 이름 하나만 비교하는 방식보다 훨씬 구체적이다. 다만 이 binding이 명령이 참조하는 모든 파일과 외부 서비스의 상태까지 고정해 주는 것은 아니다. 코드가 표현하는 동일성의 범위는 딱 여기까지다.
grant를 사용할 때는 만료·철회 여부, 현재 cron 작업의 존재와 설정 revision, 원래 approval의
allowed·allow-always 상태를 같은 DB write transaction 안에서 확인한다. 한편 expiresAtMs가 null인 경우도 허용되므로, 모든 반복 승인이 일정 시간 뒤 자동으로 만료된다고 말할 수는 없다. [36], [37]이 흐름에서 눈여겨볼 부분은 확인하는 시점이다. Gateway exec 경로는 처음에 grant를 한 번 조회하고, 실행 직전 callback에서 다시 검증하며 소비한다. 이 callback은
runExecProcess()의 beforeSpawn으로 전달된다. 사전 검사와 실제 프로세스 실행 사이에 권한 상태가 바뀌었을 수 있다는 점을 고려한 구조다. [38], [39]grant가 더 이상 유효하지 않으면 이번 실행을 거절하고, 다음 회차에 다시 승인을 요청하도록 결과를 돌려준다. 이미 시작된 프로세스의 부작용을 되돌리는 기능과는 다르다. 이 코드가 다루는 것은 어디까지나 프로세스를 새로 띄우기 직전의 권한이다.
[6] 시크릿을 사용할 권한에도 목적지가 붙는다
에이전트가 credential을 사용할 수 있게 하는 것과, 그 평문을 에이전트에게 직접 건네주는 것은 서로 다른 선택이다.
기존 버전에도 모델 제공자용 인증 정보를 sentinel로 바꿔 두었다가 전송 경계에서 실제 값으로 치환하는 코드가 있었다. 당시 sentinel은 프로세스 안의 맵을 통해 평문과 대응되었다. 따라서 credential 은닉 자체를 2.0에서 처음 도입한 기능으로 설명하는 것은 정확하지 않다. [40], [41]
2.0에서 새로 추가된 것은 보호된 저장소의 시크릿을 Gateway-hosted exec에서 사용할 수 있게 하는 egress proxy 경로다. 이 경로는 실행별로 sentinel과 허용 host를 등록한다. 핵심은
resolveRegisteredSentinel()의 검사 순서다. if (!params.registered.isActive()) {
return undefined;
}
const binding = params.registered.sentinelBindings.get(params.sentinel);
if (!binding) {
return undefined;
}
if (!binding.allowedHosts.has(params.host)) {
throw new SecretEgressSubstitutio;nError("destination-not-allowed", {
host: params.host,
secretName: binding.name,
});
}
return resolveSecretSentinel(params.sentinel);
출처:
egress-proxy/proxy-server.ts [42]먼저 실행 등록이 아직 유효한지 확인하고, 요청된 sentinel이 그 등록에 속하는지 찾는다. 이어서 요청 host가 해당 시크릿의 허용 host인지 검사한다. 실제 값으로 바꾸는 작업은 이 조건을 모두 통과한 마지막 단계에서만 이루어진다. ‘이 시크릿을 사용할 수 있다’는 권한에 사용처와 실행 수명이 함께 붙는 셈이다. [43]
이 경로는 앞에서 살펴본 실행 authority와도 연결된다. Gateway는
onAgentRunAuthorityClosed에서 해당 실행의 egress 등록을 철회한다. 프로세스나 연결이 남아 있다는 이유만으로 credential 사용 권한까지 계속 살려 두지는 않는 설계다. [45]적용 범위는 분명히 해 둘 필요가 있다. 이 proxy는 기본적으로 비활성이며, Gateway-hosted exec에 적용된다. native harness, sandbox, remote node의 명령까지 자동으로 감싸는 통제가 아니다. 또한 목적지 hostname을 허용하는 것과 OS 차원의 네트워크 격리는 서로 다른 속성이다. [44], [46]
저장 시점의 보호는 또 다른 문제다. 이 버전의 문서는 shared state SQLite에 저장되는 시크릿이 at-rest 암호화되지 않으며, 파일·디렉터리 권한으로 보호된다고 명시한다. 모델에 평문을 노출하지 않는 설계가 저장소 암호화까지 보장해 주는 것은 아니다. [47]
[7] 실제 1-day 사례: Skill dispatch의 owner-only 인가 누락
이번 절에서는 내가 직접 찾은 OpenClaw 취약점을 기준으로, 2.0이 이 권한 문제를 어떻게 완화했는지 살펴본다. 이 취약점은 권한 정책 자체는 있었지만, 특정 실행 경로에서는 그 정책이 적용되지 않던 버그였다. 공식 권고문은 첫 안정 수정 버전을 OpenClaw 2.0에 해당하는 2026.8.1로 명시한다. 앞에서 살펴본 도구 정책이 실제 실행 경로까지 어떻게 이어져야 하는지를 보여주는 사례이기도 하다. [48]
| 항목 | 내용 |
|---|---|
| 식별자 | GHSA-7cp7-87pj-p32v — 공개 권고문 기준 CVE 미부여 |
| 공개일 | 2026년 9월 11일 |
| 공식 심각도 | High, CVSS 3.1 8.3 |
| 영향 버전 | openclaw < 2026.8.1 |
| 첫 안정 수정 버전 | 2026.8.1, OpenClaw 2.0 |
| 전제 | 해당 skill 명령을 호출할 수 있도록 허용된 non-owner 발신자 |
| 잠재 영향 | 서버 credential과 소유자 전용 기능을 사용하는 도구에 접근 |
실제 영향은 설치된 skill이 어떤 소유자 전용 도구를 노출했는지에 따라 달라진다. 따라서 이 사례의 범위는 해당 기능과 설정 조합에서 발생한 권한 위반으로 한정해서 봐야 한다. [48]
7.1. 명령을 사용할 수 있는 사람과 소유자는 다르다
이 문제를 이해하려면 두 가지 자격을 나누어 봐야 한다. 발신자가 allowlist에 있어 명령을 사용할 수 있다는 것과, 그 발신자가 소유자 전용 도구까지 사용할 수 있다는 것이다.
영향받는 버전의 skill dispatch는 발신자가 소유자인지 여부를 전달하지 않았고, owner-only 도구 필터도 적용하지 않았다. 그 결과 해당 명령을 사용할 수 있는 non-owner가 소유자에게만 허용된 기능에 도달할 수 있었다. 입구에서 명령 사용을 허용한 판단이, 그 뒤에서 실행되는 도구에 대한 인가까지 대신해 버린 것이다. [48]
여기서 기존 버전의 일반 도구 구성 코드를 다시 떠올릴 필요가 있다. 2.1절에서 확인했듯 일반 에이전트의 도구 조립에는 이미 owner-only 정책 단계가 있었다. 그러므로 ‘2.0에서 소유자 권한을 처음 도입했다’고 설명하면 핵심을 놓치게 된다. 이미 있던 통제를 별도의 도구 실행 경로에도 빠짐없이 적용하도록 만든 것이 이 수정의 요점이다. [49]
7.2. 2.0에서는 발신자 정보를 전달하고 필터를 집행한다
관련 수정은 PR #116532에 기록되어 있다. [50] 병합 커밋은
9018b02f1469b77eb95fbc0b3fd37d4337c16f2c이며, v2026.8.1의 이력에 포함된다. [51] 최종 릴리스 소스에서 확인할 부분은 두 곳이다. 발신자 정보를 전달하는 부분과, 그 정보로 정책을 적용하는 부분이다.먼저 명령 처리부가
command.senderIsOwner를 skill dispatch에 넘긴다. resolveSkillDispatchTools()는 이를 필수 boolean 인자로 받고, 내부 도구를 생성할 때도 같은 값을 전달한다. 이로써 도구를 요청한 발신자가 소유자인지 여부를 다음 계층에서도 판단 근거로 쓸 수 있게 된다. [52], [53], [54]다음은 수정 후 dispatch에서 owner-only 정책을 만드는 코드다.
const ownerOnlyCoreToolPolicy = !params.senderIsOwner
? { deny: [...GATEWAY_OWNER_ONLY_CORE_TOOLS] }
: undefined;
출처:
skills/runtime/tool-dispatch.ts [55]발신자가 non-owner라면 소유자 전용 core 도구 목록으로 deny 정책을 만든다. 이 정책은 선언에 그치지 않고
applyToolPolicyPipeline()의 한 단계로 들어가며, 다른 제한과 함께 걸러진 도구 목록이 반환된다. owner인 경우에는 이 추가 deny가 만들어지지 않을 뿐, 다른 도구 정책은 그대로 적용된다. [56]이 두 변경은 하나로 묶어서 봐야 한다. 소유자 여부를 전달하기만 해서는 실행이 제한되지 않고, 필터가 있어도 올바른 발신자 문맥이 들어오지 않으면 의도한 판단을 내릴 수 없다. 2.0 소스에서는 발신자 정보가 도구 구성까지 이어지고, 그 정보로 만든 정책이 최종 목록에 적용되는 연결을 확인할 수 있다.
7.3. 수정으로 달라진 경계
관련 회귀 테스트는 같은 dispatch에 대해 non-owner와 owner의 경우를 각각 확인한다. non-owner의 결과에서는 소유자 전용 core 도구가 제외되는지, 내부 도구 생성에
senderIsOwner: false와 deny 목록이 전달되는지를 검사한다. owner에 대해서는 반대로 이 제한이 불필요하게 적용되지 않는지 확인한다. 단순히 옵션이 추가되었는지가 아니라, 그 옵션이 최종적으로 반환되는 도구 목록에 어떤 영향을 주는지를 확인하는 테스트다. [57]이 사례에 한정한 버전 비교는 다음과 같다.
| 비교 대상 | 공개 권고문과 구현에서 확인한 상태 |
|---|---|
기존 v2026.7.1 | 공식 영향 범위에 포함. 해당 skill 실행 경로에서 non-owner의 소유자 전용 도구 접근이 가능했음 |
2.0 v2026.8.1 | 첫 안정 수정 릴리스. 발신자의 소유자 여부를 전달하고 owner-only 도구 제한을 적용함 |
이 수정의 의미는 skill 명령을 사용할 자격과 소유자 전용 도구를 사용할 자격을 실행 경로에서도 구분하게 되었다는 데 있다. 다만 이를 두고 하나의 Gateway가 서로 적대적인 사용자들 사이의 완전한 격리를 제공하게 되었다고 말할 수는 없다. OpenClaw의 신뢰된 운영자 모델은 그대로이며, 이 권고문이 다루는 것은 명시된 owner-only 경계가 한 경로에서 빠져 있었다는 문제다. [48]
코드가 보여주는 핵심은 권한 정책이 존재한다는 사실 자체가 아니라, 실제 동작이 일어나는 경로까지 그 정책을 전달하고 집행하는 과정이다.
[8] 마치며
기존 OpenClaw에도 도구 정책, Gateway scope, 실행 승인, 자식 정책 상속은 이미 있었다. OpenClaw 2.0은 그 기반 위에 세션의 파일 경계와 실행 정책을 하나로 묶고, admitted run의 권한 수명을 명시적으로 표현하며, 반복 승인과 시크릿 사용을 더 구체적인 작업과 목적지에 연결했다.
코드를 분석하며 가장 중요하게 본 부분은 권한을 표현하는 단위다. 도구 이름, 사용자 scope, 명령 승인에 더해 세션 root, 실행 instance, 작업 revision, credential의 목적지까지 함께 봐야 한다. 각각은 서로 다른 질문에 답하며, 어느 하나만으로는 전체 권한 모델을 설명할 수 없다.
Skill dispatch 1-day 사례는 권한 필터가 존재하는 것과, 관련된 모든 실행 경로에서 그 필터가 실제로 적용되는 것이 다르다는 점을 보여준다. 발신자의 권한 문맥을 도구 실행 경로까지 전달하고, 그 지점에서도 제한을 집행해야 한다. 2.0의 owner-only 정책 적용은 바로 이 연결을 보완한 구체적인 수정이다.
참고 자료
[2] 공식 2.0 소개
[17] session-permission-exec-mode.ts ·
v2026.8.1, src/agents/session-permission-exec-mode.ts, L10–L15[23] session-permission-exec-mode.ts ·
v2026.8.1, src/agents/session-permission-exec-mode.ts, L44–L59[31] worker admission 검사 ·
v2026.8.1, src/gateway/worker-environments/worker-turn-payload.ts, L106–L120[32] 권한 종료와 실행 취소 연결 ·
v2026.8.1, src/gateway/worker-environments/worker-turn-execution.ts, L158–L175[35] operator-approval-standing-grants.ts ·
v2026.8.1, src/gateway/operator-approval-standing-grants.ts, L92–L103[48] 공식 보안 권고문
[50] PR #116532