tags: [troubleshooting, workspace-config, ssot, hardcoding, runtime]

# workspace-config 루트기준 SSOT와 하드코딩 분산 문제 오픈

## 상위 원칙

- [0_VALUE Writing Principles](https://github.com/happybell80/0_VALUE/blob/main/02_Governance/writing-principles.md)
- [0_VALUE Infrastructure SSOT Principle](https://github.com/happybell80/0_VALUE/blob/main/02_Governance/infrastructure-ssot-principle.md)
- [0_VALUE Coding Principles](https://github.com/happybell80/0_VALUE/blob/main/02_Governance/coding-principles.md)
- [로빙 문서 작성 원칙](../../book/300_architecture/312_writing-principles.md)
- [로빙 인프라 설정 SSOT 원칙](../../book/300_architecture/314_infrastructure-ssot-principle.md)

## 문제 정의

- `workspace-config`와 모델/런타임 설정은 SSOT라고 선언돼 있지만, 실제 코드·compose·문서에는 절대경로와 하드코딩, 분산 참조가 함께 남아 있습니다.
- 그 결과 값 변경이 `WORKSPACE_ROOT + workspace-config/runtime.env|secrets.env` 한 곳에서 닫히지 않고, 프로젝트별 수정과 해석 혼선이 반복됩니다.
- 이 문제는 로컬만의 문제가 아니라 서버도 같은 목표 구조로 닫혀야 하는 공통 문제입니다.
- 현재 서버와 로컬 모두 `runtime.env -> settings.DEFAULT_LLM_MODEL -> 단일 factory/service -> handler`로 완전히 닫혀 있지 않고, 서비스 로컬 `.env`와 코드 우회 경로가 함께 남아 있습니다.
- 사용자가 "주 모델을 `--`로 바꾸자"라고 했을 때 수정 지점이 즉시 답돼야 하는데, 지금은 그 기준도 충분히 고정돼 있지 않습니다.
- 현재 문서 기준의 목표는 주 모델 선언을 `workspace-config/runtime.env`에 두고, 과도기에는 최소 `rb8001/.env`까지만 수정하거나 API 호출에서 모델명을 명시하면 그 입력값을 따라 실제 호출 모델이 바뀌도록 만드는 것입니다.

## 관련 문서

- [모델 SSOT 하드코딩 분산과 workspace-config 로컬이식 통합 리서치](../research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md)
- [0_VALUE Workspace Config](https://github.com/happybell80/0_VALUE/blob/main/02_Governance/workspace-config/README.md)
- [하드코딩 URL 제거 작업](./250915_hardcoded_url_removal.md)
- [아침 뉴스 슬랙 포맷 SSOT 분산 및 표시 혼선](./260310_아침뉴스슬랙포맷_SSOT_분산_및_표시혼선.md)
- [프롬프트 DB 부분 도입 상태와 하드코딩 프롬프트 잔존](./260311_prompt_db_partial_adoption_and_hardcoded_prompts.md)

## 이 문제를 닫을 때 함께 닫아야 할 문서 묶음

### 0. 닫힌 문서 재오픈 금지 원칙

- `완전 닫힘`, `종결`, `완료`로 표시된 문서는 다시 열지 않습니다.
- 닫힌 문서에서 새 회귀나 새 사실이 발견되면, 기존 문서를 재오픈하지 않고 새 troubleshooting/research 문서에서 후속 이슈를 엽니다.
- 따라서 이 묶음에는 현재 문제를 닫는 데 함께 봐야 하는 `열린 문서` 또는 `아직 닫힘이 확정되지 않은 문서`만 남깁니다.

### 1. 직접 리서치 묶음

- [모델 SSOT 하드코딩 분산과 workspace-config 로컬이식 통합 리서치](../research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md)
- [LLM 모델 비교 분석](../research/LLM_모델_비교_분석.md)
- [로빙 LLM API, Agent API, 모델 선정, 비용 비교 리서치](../research/orchestration_tools/260312_로빙_LLM_API_Agent_API_모델선정_비용비교_리서치.md)
- [23서버 인프라점검 응답실패 원인확정 리서치](../research/260307_23서버_인프라점검_응답실패_원인확정_리서치.md)
- [콜드메일 skill-rag-file 500 및 실패은닉 원인확정 리서치](../research/260310_콜드메일_skill_rag_file_500_및_실패은닉_원인확정_리서치.md)
- [23서버 워크스페이스 SSOT 구조전환 리서치](https://git.ro-being.com/ivada-infra/DOCS/src/branch/main/journey/research/260309_23서버_워크스페이스_SSOT_구조전환_리서치.md)

### 2. 직접 트러블슈팅 묶음

- [아침 뉴스 슬랙 포맷 SSOT 분산 및 표시 혼선](./260310_아침뉴스슬랙포맷_SSOT_분산_및_표시혼선.md)
- [프롬프트 DB 응답생성 폐루프 미연결](./260310_프롬프트DB_응답생성_폐루프_미연결.md)
- [프롬프트 DB 부분 도입 상태와 하드코딩 프롬프트 잔존](./260311_prompt_db_partial_adoption_and_hardcoded_prompts.md)
- [Gemini 모델 최적화: gemini-2.5-flash-lite 전환](./250906_gemini_model_optimization.md)
- [admin gemini single model and ethics visibility](./251021_admin_gemini_single_model_and_ethics_visibility.md)
- [IR deck Gemini fallback 빈응답 처리](./251202_ir_deck_gemini_fallback_빈응답_처리.md)
- [rb8001: 하드코딩 URL/DB 기본값 환경변수화 정리](./251023_admin_rb8001_URL_환경변수화_정리.md)
- [코드와 규칙 정렬 점검](./251023_code_alignment_with_rules.md)
- [gateway SSOT runtime/secrets 분리 적용 및 검증](./260307_gateway_SSOT_runtime_secrets_분리_적용_및_검증.md)

### 3. 닫힘 판정 규칙

- 현재 문제를 닫을 때는 위 문서들을 별개 이슈로 남기지 않고, `주 LLM 모델 SSOT`, `workspace-config 런타임 계약`, `하드코딩 프롬프트/URL/모델 우회 경로`가 실제로 함께 줄어들었는지 확인합니다.
- 특히 `DEFAULT_LLM_MODEL` 원천이 공용 `runtime.env` 1곳으로 올라갔는지, 서비스 로컬 `.env`와 코드 fallback이 제거됐는지, 단일 factory/service 경로만 남았는지를 공통 닫힘 조건으로 봅니다.
- 사용자 모델 변경 요청 기준으로는 최소 `workspace-config/runtime.env`와 `rb8001/.env`까지만 수정하거나, API 입력 모델명만으로 실제 호출 모델이 바뀌어야 합니다.
- 닫힘 이후 회귀가 생기면 이 문서를 재오픈하지 않고 새 문서를 엽니다.

## 이 문서가 여는 리서치

- 이 문제의 원인 수집과 구조 해석은 [모델 SSOT 하드코딩 분산과 workspace-config 로컬이식 통합 리서치](../research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md)로 이어집니다.

## 재현 조건

- 로컬 워크스페이스에서 `workspace-config/runtime.env`, `workspace-config/secrets.env`를 기준으로 설정을 맞추려 할 때
- 서버 워크스페이스에서 `/home/admin/workspace-config/runtime.env`, `/home/admin/workspace-config/secrets.env`를 기준으로 설정을 맞추려 할 때
- 하위 프로젝트의 compose, 코드, 문서가 여전히 절대경로 `/home/admin/...` 또는 서비스별 개별 설정을 전제할 때
- 모델·엔드포인트·모니터·DB 설정 변경을 한 곳에서 끝내려 할 때

## 확인된 사실

### 1. 상위 SSOT는 절대경로가 아니라 루트 기준값 + 상대 구조를 요구한다

- `0_VALUE/02_Governance/infrastructure-ssot-principle.md`는 환경마다 절대경로가 달라도, 루트 기준값 하나만 바꾸면 같은 디렉터리 구조와 같은 설정 해석 규칙을 따라가게 유지하라고 명시합니다.
- `0_VALUE/02_Governance/workspace-config/README.md`는 `workspace-config`의 SSOT가 특정 절대경로가 아니라, 워크스페이스 루트 기준값 아래에서 같은 상대 구조를 해석하는 방식이라고 명시합니다.

### 1-1. 상위 코드 원칙은 하드코딩·광범위 예외·상시 폴백으로 문제를 덮지 말라고 요구한다

- `0_VALUE/02_Governance/coding-principles.md`는 운영 IP, 포트, 시크릿, 공통 URL을 코드에 하드코딩하지 말라고 명시합니다.
- 같은 문서는 catch-all fallback, 광범위 예외 처리, 기본값 주입으로 구조적 결함을 가리지 말라고 명시합니다.
- 따라서 현재 이슈는 단순 경로 정리 문제가 아니라, 상위 코드 원칙과 런타임 SSOT 원칙이 실제 적용 경로에서 어긋난 상태로 해석해야 합니다.

### 2. 현재 로컬에는 공용 런타임 파일 두 개가 존재한다

- 로컬 경로 `/home/happybell/projects/workspace-config/runtime.env`가 존재합니다.
- 로컬 경로 `/home/happybell/projects/workspace-config/secrets.env`가 존재합니다.
- 현재 로컬 기준값은 `WORKSPACE_ROOT=/home/happybell/projects`입니다.

### 3. runtime.env와 secrets.env의 역할 경계는 이미 분리됐다

- `runtime.env`는 루트, 호스트, 포트, 공용 URL, NAS 경로, 모니터 URL, Redis, 로빙/스킬 URL 묶음 같은 비민감 운영값을 담습니다.
- `secrets.env`는 NAS 계정, 외부 NAS 계정, Gitea 계정/토큰, DB URL, JWT 같은 민감값을 담습니다.

### 4. 그런데 하위 프로젝트 적용 경계는 아직 닫히지 않았다

- `robeing` 하위 서비스들의 compose와 코드 일부는 여전히 `/home/admin/workspace-config/...` 같은 서버 기준 절대경로를 전제합니다.
- 일부 서비스는 여전히 서비스별 `.env`, 코드 기본값, 하드코딩 fallback을 같이 들고 있습니다.
- 따라서 현재는 상위 원칙과 로컬 실행 구조가 완전히 일치한다고 볼 수 없습니다.

### 5. 서버도 구조 일부는 같지만, DEFAULT_LLM_MODEL SSOT는 아직 분산돼 있다

- 서버 기준 `rb8001`와 `skill-slack`은 이미 `/home/admin/workspace-config/runtime.env`, `/home/admin/workspace-config/secrets.env`를 `env_file`로 읽는 구조를 사용합니다.
- 하지만 `DEFAULT_LLM_MODEL`의 실제 원천은 아직 공용 `workspace-config/runtime.env`가 아니라 서비스 로컬 `.env`에 남아 있습니다.
- 따라서 서버는 `runtime.env/secrets.env` 2파일 구조를 이미 갖고 있지만, 주 LLM 모델 SSOT만 공용 런타임 계층으로 완전히 승격되지 않은 상태입니다.
- 이 문제는 서버 구조가 틀린 것이 아니라, 마지막 우회 경로가 남아 있는 상태로 해석해야 합니다.

### 5-1. 24서버 확인 사실

- 2026-03-16 기준 24서버(`robeing-i9`, `192.168.0.106`)는 `workspace-config` 구조를 실제로 사용 중입니다.
- `rb8001`, `skill-slack`은 `/home/admin/workspace-config/runtime.env`, `/home/admin/workspace-config/secrets.env`를 `env_file`로 읽습니다.
- 따라서 24서버 관점의 현재 상태는 "구조는 맞아가고 있고, 런타임은 아직 과도기"로 정리합니다.
- 즉 구조 SSOT는 이미 맞지만, 실제 주 모델 원천은 아직 `workspace-config/runtime.env`까지 완전히 올라가지 않았습니다.

### 5-2. 23서버 확인 사실

- 2026-03-16 기준 23서버에서는 이번 pull의 직접 영향은 런타임 변경보다 문서 SSOT 정합성 강화에 가깝습니다.
- `robeing/DOCS` 변경은 23 제어면에서도 같은 `workspace-config` 해석 규칙을 써야 한다는 기준을 더 분명히 해줍니다.
- 반면 `references/openclaw` 변경은 참고용 업스트림 코드 갱신으로, 23서버의 현재 gateway/nginx/auth/Gitea 런타임에 즉시 영향을 주는 변경은 아닙니다.

### 6. 현재 active runtime 경로 인벤토리

| 구분 | 현재 active 경로 | 현재 역할 | 상태 |
|------|------------------|-----------|------|
| 공용 비민감 설정 | `workspace-config/runtime.env` | 목표 SSOT 경로 | 구조 사용 중 |
| 공용 민감 설정 | `workspace-config/secrets.env` | 공용 secrets 경로 | 구조 사용 중 |
| rb8001 compose 주입 | `rb8001/docker-compose.yml`의 `env_file` | 공용 runtime/secrets 주입 | 사용 중 |
| skill-slack compose 주입 | `skill-slack/docker-compose.yml`의 `env_file` | 공용 runtime/secrets 주입 | 사용 중 |
| rb8001 기본 모델 원천 | `rb8001/.env` | 현재 과도기 실제 공급원 | 제거 대상 |
| rb8001 설정 해석 | `rb8001/app/core/config.py` | `settings.DEFAULT_LLM_MODEL` 노출 | 유지 |
| rb8001 기본 호출 진입점 | `rb8001/app/services/llm/llm_service.py` | 기본 handler 생성 | 유지/정리 |
| 직접 우회 1 | `rb8001/app/services/llm/gemini_handler.py` | `GEMINI_MODEL` env 우회 | 제거 대상 |
| 직접 우회 2 | `rb8001/app/router/llm_endpoint.py` | 직접 `GeminiHandler` 생성 | 제거 대상 |
| 직접 우회 3 | `rb8001/app/services/coldmail_llm_classifier.py` | 직접 Gemini SDK 호출 | 제거 대상 |
| 직접 우회 4 | `rb8001/app/services/ir_analyzer.py` | 파일별 fallback 배열 | 제거 대상 |
| 스킬 하드코딩 | `skill-slack/app/services/*` | 모델 문자열 직접 선택 | 제거 또는 정책화 대상 |

## 영향 범위

- 로컬 개발자가 `WORKSPACE_ROOT`와 `workspace-config` 두 파일만 바꾸면 된다고 믿고 작업해도, 실제 하위 프로젝트에서는 추가 수정이 발생할 수 있습니다.
- 서버 운영자도 공용 `workspace-config`만 바꾸면 된다고 믿고 작업해도, 실제로는 서비스 로컬 `.env`와 코드 우회 경로 때문에 추가 수정이 발생할 수 있습니다.
- 모델, 모니터, DB, URL, 포트 같은 공용 운영값이 여러 군데에서 따로 관리되면 회귀와 오판 위험이 커집니다.
- 문제를 `troubleshooting -> research -> plans -> worklog` 흐름으로 닫아야 하는데, 시작점이 불명확해져 같은 논쟁이 반복됩니다.
- 24서버는 이미 구조 일부를 실제로 쓰고 있으므로, 이번 문제는 새 구조 도입보다 런타임 원천 정리에 가깝습니다.
- 23서버는 당장 런타임 반영보다 문서 해석 기준 통일과 참고 코드 영향 분리에 더 직접적인 의미가 있습니다.

## 기대하는 상태

- `WORKSPACE_ROOT`만 다르면 서버와 로컬이 같은 상대 구조 `workspace-config/runtime.env`, `workspace-config/secrets.env`를 동일하게 해석합니다.
- 공용 운영값 변경 시 수정 지점은 `runtime.env`, `secrets.env` 두 파일로 닫힙니다.
- 하위 프로젝트는 서비스별 `.env`나 코드 하드코딩이 아니라, 공용 런타임 파일만 기준으로 읽습니다.
- 특히 주 LLM 모델은 `runtime.env -> settings.DEFAULT_LLM_MODEL -> 단일 LLM factory/service -> handler` 한 줄 경로로만 해석됩니다.
- 서버별 모델 차이는 코드 분기가 아니라 `runtime.env` 값 차이로만 표현됩니다.
- 목표 상태에서는 사용자가 주 모델 변경을 요청하면 `workspace-config/runtime.env` 1곳만 수정합니다.
- 과도기 기준에서도 최소 `workspace-config/runtime.env`와 `rb8001/.env`까지만 수정하거나, API 입력에서 모델명을 명시하면 그 값만으로 실제 호출 모델이 바뀌어야 합니다.

## 미확정 항목

- 각 하위 프로젝트 compose가 로컬에서 어떤 방식으로 루트 기준값을 해석하게 맞출지
- 각 하위 프로젝트 compose가 서버에서 어떤 방식으로 공용 runtime env만 기준으로 읽게 맞출지
- 서비스별 `.env`와 코드 fallback 중 무엇이 아직 실제 런타임 경로에 남아 있는지 전체 인벤토리
- 로컬 기준 공용 파일만 읽도록 바꿀 때, 프로젝트별 예외가 필요한 서비스가 있는지 여부
- 서버 기준 공용 파일만 읽도록 바꿀 때, 운영상 예외가 필요한 서비스가 있는지 여부

## 상태

- 종결 (2026-03-19)
- **종결 근거**:
  - 문서 시점(3/16)에 지적된 우회 경로 4건(gemini_handler GEMINI_MODEL 우회, llm_endpoint 직접 GeminiHandler 생성, coldmail_llm_classifier 직접 SDK 호출, ir_analyzer fallback 배열) 모두 `settings.DEFAULT_LLM_MODEL` 단일 경로로 통합 확인
  - `config.py` 하드코딩 기본값(`"gpt-5-mini"`) 제거, runtime.env 누락 시 에러 노출로 변경 (rb8001 commit 3a2b6bc)
  - `skill-slack` config.py는 이미 기본값 없이 env 의존 (`DEFAULT_LLM_MODEL: str = ""`)
  - 24 서버 rb8001 재배포 후 health 정상, `DEFAULT_LLM_MODEL=gpt-5-mini` (runtime.env 경유) 확인
  - 현재 모델 변경 시 `workspace-config/runtime.env` 1곳 수정으로 닫힘
