Skip to Content
KoDevelopersDaemon오류 분류 및 해결 방법

오류 분류 및 해결 방법

개요

데몬의 실패 모드는 의도적으로 닫힌 union으로 설계되어, SDK 소비자가 exhaustively switch로 처리하고 라우팅 핸들러가 일관된 HTTP 응답을 생성할 수 있습니다. 이 문서는 세 계층에 걸친 모든 타입화된 오류 클래스/kind를 정리합니다:

  1. packages/cli/src/serve/ — HTTP 경계의 바운더리 오류(인증, 워크스페이스 파일시스템, 데몬-호스트 사전 검사).
  2. packages/acp-bridge/ — 데몬-ACP 자식 경계의 브리지/중재자 오류.
  3. packages/sdk-typescript/src/daemon/ — SDK 측 래핑 및 구조화된 오류 필드.

통신 수준 오류 구조는 ../qwen-serve-protocol.md에 문서화되어 있으며, 이 문서는 원인과 해결 지침을 추가합니다.

파일시스템 바운더리 (packages/cli/src/serve/fs/errors.ts)

FsError{ kind, message, status, cause? }를 가집니다. FsErrorKind union (14개 kind, 기본 HTTP 상태):

KindHTTP원인해결 방법
path_outside_workspace400해석된 경로가 바인딩된 워크스페이스를 벗어남.데몬의 workspaceCwd 내부 경로를 사용; /capabilities를 확인.
symlink_escape400대상이 심볼릭 링크.해석된 경로를 직접 지정; 심볼릭 링크는 설계상 거부됨.
path_not_found404ENOENT.파일이 존재하는지 확인; Linux에서 대소문자 구분 경로를 확인.
binary_file422텍스트 라우트에서 바이너리 콘텐츠가 감지됨.원시 바이트는 GET /file/bytes를 사용; 텍스트 라우트는 바이너리를 거부.
file_too_large413큰 텍스트에 유한한 라인 제한이 없거나, 큰 텍스트가 UTF-8이 아니거나, 쓰기가 MAX_WRITE_BYTES(5 MiB)를 초과.큰 UTF-8 텍스트에 유한한 라인 제한을 사용하거나, 바이트 윈도우를 사용하거나, 쓰기를 분할.
hash_mismatch409낙관적 동시성 expectedSha256이 실패했거나, 안정적 읽기 중에 파일이 변경됨.파일을 다시 읽고 새로운 버전/해시로 재시도.
file_already_exists409기존 파일에 mode: 'create'로 요청.mode: 'overwrite'를 사용하거나 새 경로를 선택.
text_not_found422POST /file/edit 검색 문자열이 파일에 없음.검색 문자열을 다시 확인; 공백/인코딩 불일치가 일반적인 원인.
ambiguous_text_match422하나만 매칭되어야 하는데 여러 개가 매칭됨.검색 문자열에 더 많은 주변 컨텍스트를 추가하여 고유하게 만듦.
untrusted_workspace403신뢰할 수 없는 워크스페이스에서 쓰기 시도.워크스페이스를 신뢰됨으로 표시(Config.isTrustedFolder())하거나 createServeApp 직접 임베딩 대신 runQwenServe를 사용.
permission_denied403OS 수준의 EACCES / EPERM.파일시스템 ACL을 조정; 이것은 보안 알림이 아님.
io_error503ENOSPC / EIO / EBUSY / ETXTBSY / ENAMETOOLONG / EMFILE / ENFILE.호스트 수준 운영 수정(디스크 가득, fd 고갈); 보안이 아닌 운영팀에 페이징.
internal_error500errno가 아닌 오류가 바운더리에 도달.데몬 버그로 등록.
parse_error400 / 422요청 본문 파싱 오류(400) 또는 서비스 수준 불변식 위반(422).요청 본문을 검증; SDK 버전을 확인.

io_errorpermission_denied의 구분은 의도적입니다. 모니터링 파이프라인이 errorKind로 라우팅할 수 있도록 하며, ENOSPC를 permission_denied에 포함하면 df -h 문제에 대해 보안 대응팀에 페이징하게 됩니다.

브리지 오류 (packages/acp-bridge/src/bridgeErrors.ts)

브리지/중재자가 throw하는 타입화된 클래스. 대부분은 라우트 핸들러의 switch를 통해 HTTP 상태를 가집니다.

ClassHTTP원인해결 방법
SessionNotFoundError404sessionId가 byId에 없음.세션을 재생성하거나 attach; 세션이 회수되었을 수 있음.
WorkspaceMismatchError400POST /session cwd ≠ 데몬의 boundWorkspace.cwd를 생략(바인딩된 값 사용)하거나 해당 cwd에 바인딩된 데몬으로 라우팅.
SessionLimitExceededError503byId.size >= maxSessions.오래된 세션을 종료; --max-sessions를 증가.
InvalidClientIdError400X-Qwen-Client-Id[A-Za-z0-9._:-]{1,128} 범위를 벗어남.클라이언트 ID를 정리.
InvalidSessionMetadataError400displayName이 256자를 초과하거나 제어 문자를 포함.잘라내기 / 정리.
InvalidSessionScopeError400알 수 없는 sessionScope 값.'single' 또는 'thread'를 사용.
RestoreInProgressError409loadSession / resumeSession의 동시 실행.대기 후 재시도.
WorkspaceInitConflictError409force 없이 기존 파일에 POST /workspace/init 실행.force: true를 전달하거나 다른 경로를 선택.
WorkspaceInitPathEscapeError400초기화 경로가 워크스페이스를 벗어남.workspaceCwd 내부 경로를 사용.
WorkspaceInitSymlinkError400초기화 경로가 심볼릭 링크.해석된 경로를 지정.
WorkspaceInitRaceError409초기화 시 TOCTOU 레이스.재시도.
McpServerNotFoundError404알 수 없는 서버에 대한 재시작./workspace/mcp에서 서버 이름을 확인.
McpServerRestartFailedError502ACP 자식 내부에서 재시작 실패.ACP 자식 로그를 확인; 손상된 MCP 서버를 나타낼 수 있음.
InvalidPermissionOptionError400와이어 투표가 optionId를 통해 CANCEL_VOTE_SENTINEL을 주입 시도.optionId 대신 {outcome: 'cancelled'}로 투표.
PermissionForbiddenError403정책이 투표자를 거부(designated_mismatch / remote_not_allowed).원본 클라이언트 ID(designated)를 사용하거나, 투표자를 사전 등록(consensus)하거나, 루프백에서 투표(로컬 전용). 04-permission-mediation.md 참조.
CancelSentinelCollisionError500에이전트가 '__cancelled__'를 유효한 옵션 레이블로 발행.에이전트 버그 — 옵션 레이블을 센티널이 아닌 다른 값으로 변경.
PermissionPolicyNotImplementedError500요청된 정책이 이 데몬에 내장되어 있지 않음.데몬을 업데이트하거나 policy.permissionStrategy를 변경.
BridgeChannelClosedError503호출 중 ACP 자식 채널이 닫힘.재연결 / 재시도; session_died에서 원인을 확인.
BridgeTimeoutError504브리지 수준의 벽시계 초과.재시도; 근본적인 지연을 조사.
MissingCliEntryError500qwen CLI 진입 파일이 없음(bridgeErrors.ts가 아닌 status.ts에 정의).CLI 설치가 완료되었는지 확인; packages/cli/index.ts가 존재하는지 확인.

부트 시 설정 오류 (packages/cli/src/serve/run-qwen-serve.ts)

Class발생 시점해결 방법
InvalidPolicyConfigErrorvalidatePolicyConfig()가 병합된 설정을 거부: 알 수 없는 policy.permissionStrategy(SERVE_CAPABILITY_REGISTRY.permission_mediation.modes에 대해 검증) 또는 양의 정수가 아닌 policy.consensusQuorum. 부트가 명시적으로 실패.settings.json의 해당 필드를 수정. 이 클래스는 instanceof를 지원; runQwenServe는 이를 사용하여 정책 불일치를 설정 읽기 I/O 실패와 구분하며, 후자는 기본값으로 폴백.

Device Flow 인증 (packages/cli/src/serve/auth/device-flow.ts)

Class발생 시점참고 사항
UpstreamDeviceFlowError폴링 중 업스트림 IdP가 구조화된 오류를 반환.oauthError는 stderr나 감사 힌트에 보간되기 전 sanitizeForStderr로 정리됨(CVE-2021-42574 / Trojan Source 방어; 12-auth-security.md 참조).
DeviceFlowPollTimeoutError제공자가 반환하기 전에 레지스트리 레이스 타이머가 발생.제공자 코드는 이 타입을 throw하면 안 됨. 테스트용으로 export되지만, 레지스트리는 _isRegistryTimeout: boolean 런타임 브랜딩으로 pollTimedOut을 게이트하며 instanceof로 판단하지 않음. new DeviceFlowPollTimeoutError(ms)를 import하여 throw하는 제공자도 _isRegistryTimeout이 기본값 false이므로 일반 제공자 throw 감사 경로를 따름; 오직 내부 팩토리 makeRegistryPollTimeoutError(ms)만 해당 브랜딩을 설정.

데몬-호스트 오류 kind (packages/acp-bridge/src/status.ts)

SERVE_ERROR_KINDS는 진단 셀과 구조화된 데몬 오류에서 사용되는 닫힌 enum입니다:

Kind의미
missing_binary필수 로컬 실행 파일 또는 CLI 진입을 해석할 수 없음.
blocked_egress아웃바운드 네트워크 프루브 실패.
auth_env_error인증 관련 환경 변수, 제공자, 또는 trust-gate 설정이 유효하지 않음.
init_timeout데몬 측 초기화 단계가 벽시계를 초과.
protocol_errorACP / HTTP 프로토콜 불일치.
missing_file필수 로컬 파일이 누락.
parse_error로컬 파일 또는 요청 파싱 오류.
stat_failed로컬 파일시스템 stat 실패.
budget_exhaustedMCP 예산 집행이 검색 또는 서버 항목을 거부.
mcp_budget_would_exceedMCP 재시작 또는 변경이 설정된 예산을 초과.
mcp_server_spawn_failedMCP 서버 spawn 또는 재시작 실패.
invalid_configMCP 또는 데몬 설정이 유효하지 않음.
prompt_deadline_exceeded프롬프트 벽시계 기한이 만료.
writer_idle_timeoutSSE writer가 유휴 타임아웃 전에 성공적인 쓰기를 하지 않음.

이것들은 사전 검사 셀의 errorKind를 통해 노출되어 클라이언트 UI가 구조화된 해결 방법을 렌더링합니다(원시 스택 트레이스가 아님).

인증 오류 구조

상태본문발생 시점
401{ error: 'Unauthorized' }누락 / 잘못된 / 스킴 없는 bearer 토큰. missing header / wrong scheme / wrong token에 대해 동일하게 응답하여 프로빙으로 구별할 수 없음.
401{ error: '...', code: 'token_required' }토큰 없는 루프백 데몬의 mutation-gate strict 라우트. SDK가 “configure —token / —require-auth” 힌트를 렌더링.
403{ error: 'Request denied by CORS policy' }denyBrowserOriginCorsOrigin 헤더가 포함된 요청을 거부.
403{ error: 'Invalid Host header' }hostAllowlistHost 헤더를 거부(DNS rebinding 방어).

전체 인증 모델은 12-auth-security.md를 참조.

권한 결과(와이어 vs 감사 과부하)

PermissionResolution은 두 개의 종료 kind를 가집니다:

  • {kind: 'option', optionId} — 투표가 승리.
  • {kind: 'cancelled', reason: 'timeout' \| 'session_closed' \| 'agent_cancelled'} — 요청이 취소됨. 와이어 구조는 단일({outcome: 'cancelled'}); 감사 로그는 decisionReason.type에서 timeout / session_closed / voter-cancelled / agent-cancelled를 구별. 이 과부하는 동결된 permission.ts 계약을 깨지 않기 위해 의도적으로 유지됨.

SDK 측 오류 래핑

DaemonClient는 HTTP 오류를 거부된 Promise로 반환하며, 파싱된 본문을 거부 값으로 사용합니다. 알 수 없는 세션에 대해 404를 반환하는 메서드는 {error, sessionId}로 거부; SDK는 현재 타입화된 클래스로 래핑하지 않음. 호출자는 instanceof Error.message.includes(...) 매칭에 의존하면 안 되며, 대신 본문의 err.code 또는 err.kind로 switch해야 함.

parseSseStream은 16 MiB 버퍼 오버플로우 시 반복자를 중단(방어적 한도).

워크플로

사용자에게 오류 표시

인증 실패 모드 구별

의존성

  • 모든 오류 클래스는 해당 패키지에서 export되며, SDK 소비자는 동일한 Node 프로세스에서 실행 시 bridgeErrors.ts 타입에 대해 instanceof를 사용할 수 있음. 와이어를 통해서는 body.code / body.kind / body.errorKind로 라우팅.

주의사항 및 알려진 제한

  • **io_errorpermission_denied**는 의도적으로 구분됨. 혼동하지 말 것.
  • PermissionForbiddenError의 이유(designated_mismatch / remote_not_allowed)는 designatedconsensus 정책에 걸쳐 과부하됨; 감사 로그는 정확하게 구별하지만 와이어 형태는 구별하지 않음.
  • CancelSentinelCollisionError는 에이전트 측 버그를 나타냄, 보안 이벤트가 아님 — 브리지 센티널이 실제 옵션과 일치하는 것을 조용히 허용하지 않고 요청을 거부.
  • SDK 측 타입화된 오류는 아직 발전 중. 호출자는 와이어를 통해 JS 클래스 ID에 의존하기보다 본문 필드로 라우팅해야 함.
  • internal_error는 항상 조사해야 함. FsError 생성자가 non-errno 경로용으로 예약된 kind로 호출되었음을 의미(프로그래머 오류); 응답 본문의 cause 필드에 원시 throw가 포함될 수 있음.

참고 문헌

  • packages/cli/src/serve/fs/errors.ts (FsErrorKind, FsErrorStatus)
  • packages/acp-bridge/src/bridgeErrors.ts (모든 타입화된 클래스)
  • packages/acp-bridge/src/status.ts (SERVE_ERROR_KINDS, ServeErrorKind)
  • packages/cli/src/serve/auth.ts (인증 본문)
  • 와이어 참조: ../qwen-serve-protocol.md.
Last updated on