Planner 통제

Planner 통제

Planner가 있어도 우회 가능하면 AI는 우회한다. Planner 통제는 Planner 없이는 실행이 시작되지 않게 만드는 것이다.


왜 Planner를 별도로 통제하는가

전이 통제는 상태 이동 경로를 고정한다. 빌드 타임 강제는 잘못된 코드를 작성 시점에 차단한다. 그런데도 AI 작업 흐름에서 반복적으로 발생하는 문제가 있다.

AI는 작업을 완료하기 위해 가장 짧은 경로를 선택한다. Planner가 있어도, Executor를 직접 호출할 수 있으면 AI는 Planner를 건너뛴다. 이건 AI의 결함이 아니다. Planner가 선택 가능한 구조이기 때문이다.

saveDocument(doc)
publishDocument(doc)

기능은 동작할 수 있다. 하지만 다음이 보장되지 않는다.

  • 실행 순서가 검증됐는가
  • validation이 통과됐는가
  • side effect가 올바른 순서로 실행됐는가
  • 실패 시 어떻게 처리되는가

Planner 통제는 설명으로 해결하지 않는다. Executor를 숨기고 진입점을 하나로 수렴시켜 직접 실행 경로 자체를 없앤다.


1단계: Executor를 외부에서 숨긴다

Planner 통제의 첫 단계는 간단하다. 실행 함수는 외부에 노출하지 않는다.

const executor = {
  save: async (doc: Document, ctx: ExecutionContext) => { /* ... */ },
  publish: async (doc: Document, ctx: ExecutionContext) => { /* ... */ },
};

export async function dispatch(command: DocumentCommand): Promise<void> {
  const plan = buildPlan(command);
  await runPlan(plan, { source: "planner", planId: plan.id });
}

Atlas에서는 internal이나 비공개 타입으로, Glif에서는 export 없는 모듈과 import restriction으로 이 경계를 만든다. 핵심은 외부 코드가 executor symbol을 참조조차 할 수 없게 만드는 것이다.


2단계: 단일 진입점으로 수렴시킨다

Executor를 숨겨도 진입점이 여러 개면 AI는 가장 편한 public 함수를 선택한다. 그래서 실행 시작점은 하나여야 한다.

type DocumentCommand =
  | { type: "save"; docId: string; content: string }
  | { type: "publish"; docId: string }
  | { type: "archive"; docId: string; reason: string };

export async function dispatch(command: DocumentCommand): Promise<void> {
  const plan = buildPlan(command);
  validatePlan(plan);
  await runPlan(plan, { source: "planner", planId: plan.id });
}

이 구조는 두 가지를 동시에 달성한다.

  • 허용된 작업 목록이 타입과 command로 명시된다
  • 실행 진입점이 dispatch 또는 Planner.ExecuteAsync 하나로 수렴된다

3단계: 런타임에서 Planner 우회를 차단한다

빌드 타임 제약을 우회하는 코드가 있을 수 있다. 런타임 검증은 최후의 방어선이다.

function assertPlannerContext(ctx: ExecutionContext): void {
  if (ctx.source !== "planner") {
    throw new Error(
      "Execution must originate from the planner. Direct executor calls are forbidden."
    );
  }
}

Executor 안쪽에서 이 검증을 수행하면 import restriction을 뚫고 들어온 호출도 마지막에 막을 수 있다. 런타임 강제는 여기서 planner context를 검증하는 방어선이 된다.


4단계: 오류가 합법 경로를 가리키게 만든다

Planner 통제의 실패 메시지는 무엇이 잘못됐는지와 어떻게 돌아가야 하는지를 함께 알려야 한다.

Direct execution is forbidden.
Use dispatch({ type: "save", ... }) or Planner.ExecuteAsync(command).
See:
- harness/atlas/execution-flow-bypass
- harness/glif/ui-action-bypass

방향이 없는 오류 메시지는 또 다른 우회를 낳는다. 다음 시도가 곧바로 합법 경로로 수렴해야 한다.


AI 통제 계층과의 관계

AI 통제 계층은 작업 계약과 acceptance gate로 “무엇이 완료인가"를 정의한다. Planner 통제는 그것의 실행 경로 버전이다. 계약에서 planner 경유를 요구하고, 코드에서 직접 실행 경로를 닫을 때 두 계층이 맞물린다.


환경별 사례

Atlas

Glif


체크리스트

  • Executor가 외부에서 참조 불가능한가?
  • 진입점이 단일 dispatch 또는 planner 호출로 수렴됐는가?
  • Command 타입이 허용된 작업 목록을 명시하는가?
  • 빌드 타임 규칙이 executor 직접 import를 차단하는가?
  • 런타임 컨텍스트 검증이 planner 우회를 차단하는가?
  • 실패 메시지가 합법 경로를 직접 가리키는가?
  • 테스트에서도 dispatch를 통해서만 실행이 가능한가?
  • 환경별 장문 예시가 하네스 leaf에 모여 있는가?