본문으로 이동

도움말:S3 연구 메모리/문제 해결: 두 판 사이의 차이

S3 연구 메모리
S3 연구 메모리 저장소 문서 동기화
S3 연구 메모리 저장소 문서 동기화
 
(같은 사용자의 중간 판 하나는 보이지 않습니다)
7번째 줄: 7번째 줄:
docker compose config --quiet
docker compose config --quiet
docker compose ps -a
docker compose ps -a
docker compose logs --tail=200 db mediawiki bootstrap mcp-http caddy
docker compose logs --tail=200 db mediawiki-install mediawiki bootstrap \
  mediawiki-jobs mcp-http caddy
</nowiki></pre>
</nowiki></pre>


21번째 줄: 22번째 줄:


파일 이름이 <code><nowiki>compose.yaml</nowiki></code> 옆의 <code><nowiki>.env</nowiki></code>인지 확인하고, 필수 <code><nowiki>change-me</nowiki></code> 비밀번호를 모두 바꿉니다. 토큰을 출력하지 않고 공용 MCP 설정을 마칩니다.
파일 이름이 <code><nowiki>compose.yaml</nowiki></code> 옆의 <code><nowiki>.env</nowiki></code>인지 확인하고, 필수 <code><nowiki>change-me</nowiki></code> 비밀번호를 모두 바꿉니다. 토큰을 출력하지 않고 공용 MCP 설정을 마칩니다.
<code><nowiki>S3RM_WIKI_BOT_PASSWORD</nowiki></code>가 빠졌다면 <code><nowiki>openssl rand -hex 16</nowiki></code>으로 정확히 32자를 만듭니다. <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>에는 기본 계정만 쓰며 <code><nowiki>@mcp-http</nowiki></code>를 직접 붙이지 않습니다.


<pre><nowiki>
<pre><nowiki>
223번째 줄: 226번째 줄:


주요 원인은 비어 있거나 잘못된 필수 비밀번호, 디스크 부족, 볼륨 소유권 문제, <code><nowiki>db_data</nowiki></code> 초기화 뒤 <code><nowiki>.env</nowiki></code>의 DB 비밀번호만 바꾼 경우입니다. 환경 변수만 바꿔도 기존 MariaDB 계정은 바뀌지 않습니다. 이전에 맞던 <code><nowiki>.env</nowiki></code>를 복구하거나 계획된 DB 계정 교체를 수행하세요. 오류를 없애려고 <code><nowiki>db_data</nowiki></code>를 지우지 마세요.
주요 원인은 비어 있거나 잘못된 필수 비밀번호, 디스크 부족, 볼륨 소유권 문제, <code><nowiki>db_data</nowiki></code> 초기화 뒤 <code><nowiki>.env</nowiki></code>의 DB 비밀번호만 바꾼 경우입니다. 환경 변수만 바꿔도 기존 MariaDB 계정은 바뀌지 않습니다. 이전에 맞던 <code><nowiki>.env</nowiki></code>를 복구하거나 계획된 DB 계정 교체를 수행하세요. 오류를 없애려고 <code><nowiki>db_data</nowiki></code>를 지우지 마세요.
binary log 설정 오류가 보이면 <code><nowiki>db_binlogs</nowiki></code> 볼륨과 현재 master 상태를 읽기 전용으로 확인합니다. 출력에는 비밀번호가 없지만 DB 이름·파일 번호는 운영 정보로 취급합니다.
<pre><nowiki>
docker volume ls | grep db_binlogs
docker compose exec -T db sh -c \
  'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -h127.0.0.1 \
  --batch --skip-column-names -e "SHOW MASTER STATUS"'
</nowiki></pre>
결과가 비어 있거나 <code><nowiki>mariadb-bin.000001</nowiki></code> 형식이 아니면 새 백업을 만들지 마세요. Compose 명령과 볼륨 mount를 먼저 고치고 DB를 계획된 작업 시간에 다시 만듭니다. 기존 <code><nowiki>db_binlogs</nowiki></code>를 임의로 지우면 연속 PITR 구간을 잃습니다.


=== MediaWiki가 unhealthy 상태임 ===
=== MediaWiki가 unhealthy 상태임 ===
239번째 줄: 253번째 줄:
=== 브라우저에 설치 세션 만료가 표시됨 ===
=== 브라우저에 설치 세션 만료가 표시됨 ===


이 메시지는 MediaWiki의 대화형 <code><nowiki>/mw-config/</nowiki></code> 설치 화면에서 나옵니다. 이 배포는 해당 화면을 사용하지 않습니다. <code><nowiki>mediawiki</nowiki></code> entrypoint가 Apache를 시작하기 전에 CLI로 설치하고, 생성된 설정은 <code><nowiki>mediawiki_config</nowiki></code> 볼륨에 저장합니다. 세션 만료는 지원하지 않는 경로를 연 증상이지 원인이 아닙니다.
이 메시지는 MediaWiki의 대화형 <code><nowiki>/mw-config/</nowiki></code> 설치 화면에서 나옵니다. 이 배포는 해당 화면을 사용하지 않습니다. <code><nowiki>mediawiki-install</nowiki></code> one-shot이 Apache와 분리된 secret 경계에서 CLI로 설치하고, 생성된 설정은 <code><nowiki>mediawiki_config</nowiki></code> 볼륨에 저장합니다. <code><nowiki>mediawiki</nowiki></code> entrypoint는 설정이 없으면 설치를 시도하지 않고 종료합니다. 세션 만료는 지원하지 않는 경로를 연 증상이지 원인이 아닙니다.


&lt;!-- Contract marker: Do not increase <code><nowiki>session.gc_maxlifetime</nowiki></code> --&gt; <code><nowiki>session.gc_maxlifetime</nowiki></code>를 늘리거나, 브라우저 설치를 다시 시작하거나, <code><nowiki>LocalSettings.php</nowiki></code>를 내려받거나, 볼륨을 지우지 마세요. 세션 수명만 늘려도 CLI 설정은 고쳐지지 않습니다.
&lt;!-- Contract marker: Do not increase <code><nowiki>session.gc_maxlifetime</nowiki></code> --&gt; <code><nowiki>session.gc_maxlifetime</nowiki></code>를 늘리거나, 브라우저 설치를 다시 시작하거나, <code><nowiki>LocalSettings.php</nowiki></code>를 내려받거나, 볼륨을 지우지 마세요. 세션 수명만 늘려도 CLI 설정은 고쳐지지 않습니다.
247번째 줄: 261번째 줄:
<pre><nowiki>
<pre><nowiki>
docker compose ps -a
docker compose ps -a
docker compose logs --tail=300 mediawiki bootstrap
docker compose logs --tail=300 mediawiki-install mediawiki bootstrap
</nowiki></pre>
</nowiki></pre>


268번째 줄: 282번째 줄:
<pre><nowiki>
<pre><nowiki>
docker compose up -d --build --force-recreate mediawiki bootstrap
docker compose up -d --build --force-recreate mediawiki bootstrap
docker compose logs -f mediawiki bootstrap
docker compose logs -f mediawiki-install mediawiki bootstrap
</nowiki></pre>
</nowiki></pre>


운영하던 배포에서 <code><nowiki>LocalSettings.php</nowiki></code>를 잃었다면 여기서 진단을 멈춥니다. 기존 DB에 다시 설치하거나 볼륨을 지우지 마세요. [[Help:S3 연구 메모리/백업과 복원|<code><nowiki>operations.md</nowiki></code>]]에 따라 같은 시점의 DB, 설정, 업로드 백업을 복원합니다.
운영하던 배포에서 <code><nowiki>LocalSettings.php</nowiki></code>를 잃었다면 여기서 진단을 멈춥니다. 기존 DB에 다시 설치하거나 볼륨을 지우지 마세요. [[Help:S3 연구 메모리/백업과 복원|<code><nowiki>operations.md</nowiki></code>]]에 따라 같은 시점의 DB, 설정, 업로드 백업을 복원합니다.
=== <code><nowiki>mediawiki-install</nowiki></code>이 0이 아닌 코드로 끝남 ===
새 볼륨의 첫 설치에서만 이 one-shot이 실제 설치를 합니다. 기존 <code><nowiki>LocalSettings.php</nowiki></code>가 있으면 코드 0으로 아무것도 바꾸지 않는 것이 정상입니다.
<pre><nowiki>
docker compose logs --tail=200 mediawiki-install
</nowiki></pre>
secret 파일 없음·빈 값·여러 줄 오류면 <code><nowiki>.env</nowiki></code>의 <code><nowiki>MW_ADMIN_PASSWORD</nowiki></code>와 Compose secret 선언을 확인합니다. DB 연결 오류면 먼저 <code><nowiki>db</nowiki></code> health를 고칩니다. 설치 실패 뒤 브라우저 설치로 우회하거나 부분 생성된 설정을 직접 편집하지 마세요. 새 시험 볼륨이라 데이터가 없다는 사실을 확인한 경우에만 실패 원인을 고친 뒤 같은 one-shot을 다시 실행합니다. 운영 볼륨이면 복구 절차로 전환합니다.


=== <code><nowiki>bootstrap</nowiki></code>이 <code><nowiki>Exited (0)</nowiki></code>임 ===
=== <code><nowiki>bootstrap</nowiki></code>이 <code><nowiki>Exited (0)</nowiki></code>임 ===


정상입니다. <code><nowiki>bootstrap</nowiki></code>은 daemon이 아니라 일회성 동기화 작업입니다. 평소에는 <code><nowiki>db</nowiki></code>, <code><nowiki>mediawiki</nowiki></code>, <code><nowiki>mcp-http</nowiki></code>가 healthy이고 <code><nowiki>bootstrap</nowiki></code>은 코드 0으로 종료됩니다.
정상입니다. <code><nowiki>bootstrap</nowiki></code>은 daemon이 아니라 일회성 동기화 작업입니다. 평소에는 <code><nowiki>db</nowiki></code>, <code><nowiki>mediawiki</nowiki></code>, <code><nowiki>mediawiki-jobs</nowiki></code>, <code><nowiki>mcp-http</nowiki></code>가 healthy이고 <code><nowiki>mediawiki-install</nowiki></code>·<code><nowiki>bootstrap</nowiki></code>은 코드 0으로 종료됩니다.


=== <code><nowiki>bootstrap</nowiki></code>이 0이 아닌 코드로 끝남 ===
=== <code><nowiki>bootstrap</nowiki></code>이 0이 아닌 코드로 끝남 ===
289번째 줄: 313번째 줄:
* '''edit failed/permission denied''': 관리자 권한이 없어졌거나 관리 페이지가 예상과 다르게 보호되어 있습니다.
* '''edit failed/permission denied''': 관리자 권한이 없어졌거나 관리 페이지가 예상과 다르게 보호되어 있습니다.
* '''account provisioning failed''': <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>을 확인합니다. DB 테이블을 직접 고치지 말고 MediaWiki 절차로 전용 계정을 교체합니다.
* '''account provisioning failed''': <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>을 확인합니다. DB 테이블을 직접 고치지 말고 MediaWiki 절차로 전용 계정을 교체합니다.
* '''BotPassword secret/validation failed''': <code><nowiki>S3RM_WIKI_BOT_PASSWORD</nowiki></code>가 <code><nowiki>openssl rand -hex 16</nowiki></code>으로 만든 정확히 32자인지, app ID가 안전한 ASCII 1~32자인지 확인합니다. 로그에 secret을 붙이지 마세요.
* '''BotPassword login/effective rights failed''': <code><nowiki>bootstrap</nowiki></code>이 grant를 저장한 뒤 <code><nowiki>&lt;기본 계정&gt;@&lt;app-id&gt;</nowiki></code> 실제 로그인 또는 <code><nowiki>agent</nowiki></code> 유효 권한 확인에 실패했습니다. primary 비밀번호를 <code><nowiki>mcp-http</nowiki></code>에 넣어 우회하지 말고 아래 전환 절차를 따릅니다.
원인을 고친 뒤 다시 실행합니다.
원인을 고친 뒤 다시 실행합니다.


308번째 줄: 334번째 줄:


현재 이미지가 출력한 문법만 사용합니다. 복구 작업을 기록하고, 추적하지 않는 <code><nowiki>.env</nowiki></code>를 갱신한 뒤 <code><nowiki>bootstrap</nowiki></code>을 다시 실행합니다. 새 비밀번호를 셸 기록에 직접 넣지 마세요.
현재 이미지가 출력한 문법만 사용합니다. 복구 작업을 기록하고, 추적하지 않는 <code><nowiki>.env</nowiki></code>를 갱신한 뒤 <code><nowiki>bootstrap</nowiki></code>을 다시 실행합니다. 새 비밀번호를 셸 기록에 직접 넣지 마세요.
=== 기존 설치를 BotPassword로 전환하거나 BotPassword를 교체함 ===
기존 <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>·<code><nowiki>S3RM_WIKI_PASSWORD</nowiki></code>는 기본 계정을 유지하기 위한 one-shot 값입니다. <code><nowiki>.env</nowiki></code>에 다음 두 값을 추가합니다. 사용자 이름에는 <code><nowiki>@app-id</nowiki></code>를 붙이지 않습니다.
<pre><nowiki>
S3RM_WIKI_BOT_APP_ID=mcp-http
S3RM_WIKI_BOT_PASSWORD=<openssl-rand-hex-16-output>
</nowiki></pre>
primary 비밀번호가 장기 서비스에 남는 시간을 만들지 않도록 다음 순서를 사용합니다.
<pre><nowiki>
docker compose stop mcp-http
docker compose run --rm bootstrap
docker compose up -d --build --force-recreate mcp-http
curl --fail --silent --show-error http://127.0.0.1:8765/healthz \
  | jq --exit-status \
      '.canonical_store == "ok" and .job_runner == "ok"'
</nowiki></pre>
<code><nowiki>bootstrap</nowiki></code> 로그에는 <code><nowiki>S3ResearchAgent@mcp-http</nowiki></code>와 같은 로그인 이름만 나와야 하며 비밀번호는 나오지 않아야 합니다. health가 <code><nowiki>canonical_store=ok</nowiki></code>이면 장기 MCP가 BotPassword로 실제 위키 로그인을 완료한 것입니다. 실패하면 <code><nowiki>mcp-http</nowiki></code>를 멈춘 채 이전 <code><nowiki>.env</nowiki></code>의 안전한 사본으로 되돌리거나 <code><nowiki>bootstrap</nowiki></code> 로그를 확인합니다.
app ID를 바꾸면 새 app ID가 추가되고 이전 app ID는 자동 삭제되지 않습니다. 새 서비스 확인 후 관리 화면 <code><nowiki>Special:BotPasswords</nowiki></code>에서 이전 app ID만 폐기합니다. 기본 계정 자체나 <code><nowiki>agent</nowiki></code> 그룹, DB row를 직접 지우지 마세요.


=== 양식이 없거나 Lesson이 template 원문으로 표시됨 ===
=== 양식이 없거나 Lesson이 template 원문으로 표시됨 ===
321번째 줄: 371번째 줄:


관리 양식이나 template 페이지를 직접 고치지 마세요. <code><nowiki>bootstrap</nowiki></code>이 이미지의 원본으로 맞춥니다.
관리 양식이나 template 페이지를 직접 고치지 마세요. <code><nowiki>bootstrap</nowiki></code>이 이미지의 원본으로 맞춥니다.
=== <code><nowiki>mediawiki-jobs</nowiki></code>가 unhealthy 상태임 ===
runner는 한 process에서 batch당 기본 25건·20초만 처리합니다. 마지막 성공 batch가 기본 120초보다 오래되면 unhealthy가 되며, 반복 실패는 15초부터 최대 5분까지 지수 backoff합니다.
<pre><nowiki>
docker compose ps mediawiki-jobs
docker compose logs --tail=200 mediawiki-jobs
docker compose exec mediawiki-jobs cat /run/s3rm-jobs/status.env
docker compose exec mediawiki php maintenance/run.php showJobs --group
</nowiki></pre>
<code><nowiki>runner_generation</nowiki></code>, <code><nowiki>consecutive_failures</nowiki></code>, <code><nowiki>queue_depth</nowiki></code>, <code><nowiki>updated_epoch</nowiki></code>를 보고 DB·확장 기능·디스크 문제를 먼저 해결합니다. 상태 파일에는 secret이나 Lesson 본문이 없어야 합니다. <code><nowiki>S3RM_JOBS_HEALTH_STALE_SECONDS</nowiki></code>는 <code><nowiki>S3RM_JOBS_MAX_SECONDS + S3RM_JOBS_IDLE_SECONDS + 30</nowiki></code> 이상이어야 합니다. 잘못된 <code><nowiki>S3RM_JOBS_*</nowiki></code> 값은 이 관계와 각 허용 범위 안으로 고치고 컨테이너를 다시 만듭니다.
<pre><nowiki>
docker compose up -d --force-recreate mediawiki-job-state-init mediawiki-jobs
</nowiki></pre>
health만 통과시키기 위해 stale 상한을 크게 늘리거나 <code><nowiki>--nothrottle</nowiki></code>, 무제한 job 수, 다중 runner를 사용하지 마세요. backlog가 정상 부하로 계속 늘 때만 처리 시간과 건수를 작게 올리고 CPU·DB latency를 함께 관찰합니다.


=== Lesson ID 또는 양식 검증이 실패함 ===
=== Lesson ID 또는 양식 검증이 실패함 ===
334번째 줄: 403번째 줄:
=== 새 Lesson이 검색되지 않음 ===
=== 새 Lesson이 검색되지 않음 ===


페이지를 직접 열어 <code><nowiki>Lesson:</nowiki></code> 네임스페이스인지 먼저 확인합니다. <code><nowiki>/index.php/Special:Search</nowiki></code>에서 제목이나 본문의 구별되는 정확한 단어로 검색합니다. 먼저 대기 중인 MediaWiki·Semantic MediaWiki 작업을 실행합니다. 색인이 손상되었거나 DB 복원 뒤 누락된 경우에만 검색 색인을 다시 만듭니다.
페이지를 직접 열어 <code><nowiki>Lesson:</nowiki></code> 네임스페이스인지 먼저 확인합니다. <code><nowiki>/index.php/Special:Search</nowiki></code>에서 제목이나 본문의 구별되는 정확한 단어로 검색합니다. 먼저 bounded job runner와 대기 queue를 확인합니다. 색인이 손상되었거나 DB 복원 뒤 누락된 경우에만 검색 색인을 다시 만듭니다.


<pre><nowiki>
<pre><nowiki>
docker compose exec mediawiki \
docker compose ps mediawiki-jobs
  php maintenance/run.php runJobs --maxjobs 100
docker compose exec mediawiki-jobs cat /run/s3rm-jobs/status.env
docker compose exec mediawiki php maintenance/run.php showJobs --group
docker compose exec mediawiki \
docker compose exec mediawiki \
   php maintenance/run.php rebuildtextindex --quiet
   php maintenance/run.php rebuildtextindex --quiet
</nowiki></pre>
</nowiki></pre>
<code><nowiki>mediawiki-jobs</nowiki></code>가 unhealthy이면 [[#mediawiki-jobs가 unhealthy 상태임|<code><nowiki>mediawiki-jobs가-unhealthy-상태임</nowiki></code>]]을 먼저 해결합니다. queue를 빨리 비우려고 무제한 <code><nowiki>runJobs</nowiki></code>나 여러 process를 동시에 실행하지 마세요.


기본 <code><nowiki>S3RM_EMBEDDING_PROVIDER=disabled</nowiki></code>에서는 띄어쓰기와 단어 기반 검색이므로 비슷한 뜻의 다른 단어까지 찾는다고 보장하지 않습니다. 도메인을 넘는 연결은 관계나 <code><nowiki>find_analogies</nowiki></code>를 사용합니다. 의미 검색을 켰더라도 클라이언트가 <code><nowiki>retrieval_mode=hybrid_v1</nowiki></code>을 명시해야 합니다. 서비스 시작 시 단 하나의 백그라운드 작업이 전체 파생 인덱스를 프리웜합니다. hybrid 요청은 구축을 기다리지 않으며, 아직 snapshot이 없으면 <code><nowiki>semantic_status=building</nowiki></code>과 함께 즉시 기존 검색으로 대체됩니다. MCP Lesson 저장의 dirty hint는 즉시 백그라운드 대조를 시작합니다. 설정한 간격이 지나면 다음 hybrid 요청이 브라우저 편집까지 포함한 대조를 시작하며 기존 snapshot을 계속 사용합니다. 최종 결과는 MediaWiki 최신 revision으로 다시 확인하므로 오래된 의미 후보가 새 본문으로 반환되지는 않습니다.
기본 <code><nowiki>S3RM_EMBEDDING_PROVIDER=disabled</nowiki></code>에서는 띄어쓰기와 단어 기반 검색이므로 비슷한 뜻의 다른 단어까지 찾는다고 보장하지 않습니다. 도메인을 넘는 연결은 관계나 <code><nowiki>find_analogies</nowiki></code>를 사용합니다. 의미 검색을 켰더라도 클라이언트가 <code><nowiki>retrieval_mode=hybrid_v1</nowiki></code>을 명시해야 합니다. 서비스 시작 시 단 하나의 백그라운드 작업이 전체 파생 인덱스를 프리웜합니다. hybrid 요청은 구축을 기다리지 않으며, 아직 snapshot이 없으면 <code><nowiki>semantic_status=building</nowiki></code>과 함께 즉시 기존 검색으로 대체됩니다. MCP Lesson 저장의 dirty hint는 즉시 백그라운드 대조를 시작합니다. 설정한 간격이 지나면 다음 hybrid 요청이 브라우저 편집까지 포함한 대조를 시작하며 기존 snapshot을 계속 사용합니다. 최종 결과는 MediaWiki 최신 revision으로 다시 확인하므로 오래된 의미 후보가 새 본문으로 반환되지는 않습니다.
380번째 줄: 452번째 줄:
<pre><nowiki>
<pre><nowiki>
docker compose ps -a caddy mcp-http mediawiki bootstrap db
docker compose ps -a caddy mcp-http mediawiki bootstrap db
curl --fail --silent --show-error http://127.0.0.1:8765/healthz
curl --fail --silent --show-error http://127.0.0.1:8765/livez
curl --fail --silent --show-error http://127.0.0.1:8765/readyz
docker compose logs --tail=200 mcp-http caddy
docker compose logs --tail=200 mcp-http caddy
</nowiki></pre>
</nowiki></pre>


로컬 health 요청은 프로세스 실행만 확인합니다. 다른 PC에서 [[Help:S3 연구 메모리/설치와 검증#클라이언트를 연결하기 전 공용 MCP 확인|<code><nowiki>setup.md</nowiki></code>]]의 토큰 없는 초기화를 정확한 <code><nowiki>https://s3wiki.yonsei.ac.kr/mcp</nowiki></code> 주소로 실행합니다. <code><nowiki>Authorization</nowiki></code> 없이 초기화되어야 하며 도구 목록과 다섯 조회 도구도 공개되어야 합니다.
<code><nowiki>/livez</nowiki></code>는 MCP 프로세스 실행만 확인합니다. <code><nowiki>/healthz</nowiki></code>와 <code><nowiki>/readyz</nowiki></code>는 BotPassword로 canonical MediaWiki store까지 확인하며 연결이나 로그인이 실패하면 503을 반환합니다. 다른 PC에서 [[Help:S3 연구 메모리/설치와 검증#클라이언트를 연결하기 전 공용 MCP 확인|<code><nowiki>setup.md</nowiki></code>]]의 토큰 없는 초기화를 정확한 <code><nowiki>https://s3wiki.yonsei.ac.kr/mcp</nowiki></code> 주소로 실행합니다. <code><nowiki>Authorization</nowiki></code> 없이 초기화되어야 하며 도구 목록과 다섯 조회 도구도 공개되어야 합니다.


다음 값을 확인합니다.
다음 값을 확인합니다.
397번째 줄: 470번째 줄:
먼저 <code><nowiki>Authorization</nowiki></code> 없이 공개 조회를 시험합니다. 로컬 health만 성공하고 원격 초기화가 실패하면 Caddy 로그, TLS, DNS, 80/443을 확인합니다. 클라이언트를 localhost에 연결하거나 별도 MCP 프로세스를 만들지 마세요.
먼저 <code><nowiki>Authorization</nowiki></code> 없이 공개 조회를 시험합니다. 로컬 health만 성공하고 원격 초기화가 실패하면 Caddy 로그, TLS, DNS, 80/443을 확인합니다. 클라이언트를 localhost에 연결하거나 별도 MCP 프로세스를 만들지 마세요.


클라이언트 주소는 정확히 공용 URL이어야 합니다. <code><nowiki>token</nowiki></code> 모드에서 변경 도구를 쓸 때만 공용 Bearer 토큰을 불러옵니다. <code><nowiki>open</nowiki></code> 모드에서는 조회와 변경 도구 개에 인증이 필요하지 않습니다. 클라이언트 사용자에게 Docker 명령, MediaWiki API URL, 위키 자격 증명을 전달하지 마세요. 플랫폼별 확인은 [[Help:S3 연구 메모리/MCP 클라이언트|<code><nowiki>mcp-clients.md</nowiki></code>]]를 봅니다.
클라이언트 주소는 정확히 공용 URL이어야 합니다. <code><nowiki>token</nowiki></code> 모드에서 변경 도구를 쓸 때만 공용 Bearer 토큰을 불러옵니다. <code><nowiki>open</nowiki></code> 모드에서는 조회와 변경 도구 다섯 개에 인증이 필요하지 않습니다. 클라이언트 사용자에게 Docker 명령, MediaWiki API URL, 위키 자격 증명을 전달하지 마세요. 플랫폼별 확인은 [[Help:S3 연구 메모리/MCP 클라이언트|<code><nowiki>mcp-clients.md</nowiki></code>]]를 봅니다.


=== MCP <code><nowiki>/healthz</nowiki></code>가 실패함 ===
=== MCP <code><nowiki>/healthz</nowiki></code>가 실패함 ===
419번째 줄: 492번째 줄:
</nowiki></pre>
</nowiki></pre>


<code><nowiki>S3RM_GITHUB_ALLOWED_USERS</nowiki></code>와 <code><nowiki>S3RM_GITHUB_ALLOWED_ORGS</nowiki></code>는 GitHub 로그인 이름을 쉼표로 구분합니다. private 조직 membership을 확인하려면 사용자가 GitHub 승인 화면에서 <code><nowiki>read:org</nowiki></code>를 허용해야 합니다. <code><nowiki>mcp-http</nowiki></code>를 다시 만들면 메모리에 있던 OAuth client와 token이 사라지므로 ChatGPT 앱 인증을 다시 진행합니다. 일반 설정은 URL <code><nowiki>https://s3wiki.yonsei.ac.kr/mcp</nowiki></code>, bind <code><nowiki>127.0.0.1</nowiki></code>, allow flag <code><nowiki>false</nowiki></code>입니다. 같은 endpoint의 변경 도구에는 <code><nowiki>mcp:write</nowiki></code> scope가 필요합니다.
<code><nowiki>S3RM_GITHUB_ALLOWED_USERS</nowiki></code>와 <code><nowiki>S3RM_GITHUB_ALLOWED_ORGS</nowiki></code>는 GitHub 로그인 이름을 쉼표로 구분합니다. private 조직 membership을 확인하려면 사용자가 GitHub 승인 화면에서 <code><nowiki>read:org</nowiki></code>를 허용해야 합니다. <code><nowiki>mcp-http</nowiki></code>를 다시 만들면 메모리에 있던 OAuth client와 token이 사라지므로 ChatGPT 앱 인증을 다시 진행합니다. 일반 설정은 URL <code><nowiki>https://s3wiki.yonsei.ac.kr/mcp</nowiki></code>, bind <code><nowiki>127.0.0.1</nowiki></code>, allow flag <code><nowiki>false</nowiki></code>입니다. 같은 endpoint의 다섯 변경 도구에는 <code><nowiki>mcp:write</nowiki></code> scope가 필요합니다.


health는 성공하지만 공용 <code><nowiki>/mcp</nowiki></code>가 실패하면 프로세스는 살아 있습니다. 직접 IP 경로와 아래 HTTP 상태 항목을 확인합니다. <code><nowiki>/healthz</nowiki></code>는 변경 자격 증명이나 위키 로그인을 확인하지 않습니다.
<code><nowiki>/livez</nowiki></code>는 성공하지만 <code><nowiki>/healthz</nowiki></code>·<code><nowiki>/readyz</nowiki></code>가 실패하면 프로세스는 살아 있으나 위키 연결·BotPassword 로그인 또는 bounded job runner가 준비되지 않은 상태입니다. 응답의 <code><nowiki>canonical_store</nowiki></code>와 <code><nowiki>job_runner</nowiki></code>를 확인하고, <code><nowiki>job_runner=unavailable</nowiki></code>이면 [[#mediawiki-jobs가 unhealthy 상태임|<code><nowiki>mediawiki-jobs</nowiki></code>가 unhealthy 상태임]]을 따릅니다. 세 경로가 성공하는데 공용 <code><nowiki>/mcp</nowiki></code>만 실패하면 직접 IP 경로와 아래 HTTP 상태 항목을 확인합니다.


=== MCP 변경 도구에서 인증 오류가 남 ===
=== MCP 변경 도구에서 인증 오류가 남 ===
433번째 줄: 506번째 줄:
아래 토큰 진단은 <code><nowiki>token</nowiki></code> 모드에만 적용합니다.
아래 토큰 진단은 <code><nowiki>token</nowiki></code> 모드에만 적용합니다.


이 오류가 있어도 익명 초기화, 도구 목록, 조회는 동작해야 합니다. 변경 도구의 인증 오류는 Bearer 헤더가 없거나 형식이 틀리거나 <code><nowiki>S3RM_MCP_TOKEN_SHA256</nowiki></code>과 맞지 않는다는 뜻입니다. 비교할 때 어느 값도 출력하지 마세요. GitHub 모드에서는 <code><nowiki>mcp:write</nowiki></code> scope가 필요합니다. <code><nowiki>open</nowiki></code> 모드라면 도구에 별도 인증이 없습니다. <code><nowiki>revise_lesson</nowiki></code> 요청이 위키 저장 경계에서 거부되면 서버 내부의 <code><nowiki>S3RM_WIKI_REVISION_HMAC_KEY</nowiki></code>를 <code><nowiki>mediawiki</nowiki></code>와 <code><nowiki>mcp-http</nowiki></code>가 같은 값으로 읽는지 확인하되 값을 출력하지 않습니다.
이 오류가 있어도 익명 초기화, 도구 목록, 조회는 동작해야 합니다. 다섯 변경 도구의 인증 오류는 Bearer 헤더가 없거나 형식이 틀리거나 <code><nowiki>S3RM_MCP_TOKEN_SHA256</nowiki></code>과 맞지 않는다는 뜻입니다. 비교할 때 어느 값도 출력하지 마세요. GitHub 모드에서는 <code><nowiki>mcp:write</nowiki></code> scope가 필요합니다. <code><nowiki>open</nowiki></code> 모드라면 다섯 도구에 별도 인증이 없습니다. <code><nowiki>revise_lesson</nowiki></code> 요청이 위키 저장 경계에서 거부되면 서버 내부의 <code><nowiki>S3RM_WIKI_REVISION_HMAC_KEY</nowiki></code>를 <code><nowiki>mediawiki</nowiki></code>와 <code><nowiki>mcp-http</nowiki></code>가 같은 값으로 읽는지 확인하되 값을 출력하지 않습니다.


서버에서 기존 HTTPS 토큰과 해시를 바꾸거나 출력하지 않고 확인합니다.
서버에서 기존 HTTPS 토큰과 해시를 바꾸거나 출력하지 않고 확인합니다.
458번째 줄: 531번째 줄:
=== 여러 변경 작업이 rate limit에서 멈춤 ===
=== 여러 변경 작업이 rate limit에서 멈춤 ===


모든 MCP 클라이언트는 같은 <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>으로 저장하므로 계정별 편집 한도를 함께 사용합니다. 기본 <code><nowiki>agent</nowiki></code> 한도는 600회/분이며, 일반 로그인 계정의 90회/분과 편집 이외 작업의 제한은 그대로입니다. 현재 컨테이너에 들어간 값은 비밀값을 출력하지 않고 확인할 수 있습니다.
모든 MCP 클라이언트는 같은 <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>으로 저장하므로 계정별 편집 한도를 함께 사용합니다. 기본 <code><nowiki>agent</nowiki></code> 한도는 6000회/분이며, 일반 로그인 계정의 90회/분과 편집 이외 작업의 제한은 그대로입니다. 현재 컨테이너에 들어간 값은 비밀값을 출력하지 않고 확인할 수 있습니다.


<pre><nowiki>
<pre><nowiki>
465번째 줄: 538번째 줄:
</nowiki></pre>
</nowiki></pre>


결과가 <code><nowiki>600, 60</nowiki></code>이 아니면 <code><nowiki>.env</nowiki></code>의 <code><nowiki>S3RM_AGENT_EDITS_PER_MINUTE</nowiki></code>를 확인하고 MediaWiki 컨테이너를 다시 만듭니다. <code><nowiki>restart</nowiki></code>만 하면 기존 컨테이너 환경이 유지됩니다.
결과가 <code><nowiki>6000, 60</nowiki></code>이 아니면 <code><nowiki>.env</nowiki></code>의 <code><nowiki>S3RM_AGENT_EDITS_PER_MINUTE</nowiki></code>를 확인하고 MediaWiki 컨테이너를 다시 만듭니다. <code><nowiki>restart</nowiki></code>만 하면 기존 컨테이너 환경이 유지됩니다.


<pre><nowiki>
<pre><nowiki>
471번째 줄: 544번째 줄:
</nowiki></pre>
</nowiki></pre>


저장소의 MediaWiki 설정 파일을 갱신한 첫 배포에는 <code><nowiki>--build</nowiki></code>도 붙입니다. 한도를 조정할 때 일반 <code><nowiki>user</nowiki></code> 값을 바꾸거나 <code><nowiki>agent</nowiki></code>에 <code><nowiki>noratelimit</nowiki></code> 권한을 주지 마세요. 한도가 충분한데도 느리면 쓰기 동시성을 먼저 4~8개로 제한하고 MediaWiki, MariaDB, Semantic MediaWiki 작업 큐의 부하를 확인합니다.
저장소의 MediaWiki 설정 파일을 갱신한 첫 배포에는 <code><nowiki>--build</nowiki></code>도 붙입니다. 한도를 조정할 때 일반 <code><nowiki>user</nowiki></code> 값을 바꾸거나 <code><nowiki>agent</nowiki></code>에 <code><nowiki>noratelimit</nowiki></code> 권한을 주지 마세요.
 
오류 code가 <code><nowiki>write_rate_limited</nowiki></code> 또는 <code><nowiki>server_busy</nowiki></code>이면 MediaWiki보다 앞의 MCP admission입니다. <code><nowiki>/healthz</nowiki></code>의 비밀값 없는 현재 active·waiting·principal 수와 설정된 상한을 확인합니다.
 
<pre><nowiki>
curl --fail --silent --show-error http://127.0.0.1:8765/healthz \
  | jq '.write_admission'
docker compose exec mcp-http sh -c \
  'env | sort | grep "^S3RM_MCP_WRITE_"'
</nowiki></pre>
 
기본은 동시 8개, queue 64개·2초, principal별 지속 600회/분·burst 120입니다. <code><nowiki>open</nowiki></code> 모드는 client IP별, <code><nowiki>github</nowiki></code>는 subject별, <code><nowiki>token</nowiki></code>은 공유 token 전체가 한 bucket입니다. <code><nowiki>retry_after_seconds</nowiki></code>를 따르고, queue가 계속 차면 MariaDB latency와 job queue를 먼저 확인합니다. 필요한 경우 <code><nowiki>S3RM_MCP_WRITE_MAX_CONCURRENCY</nowiki></code>나 queue 값을 작게 조정한 뒤 <code><nowiki>mcp-http</nowiki></code>를 <code><nowiki>--force-recreate</nowiki></code>합니다. rate와 burst를 높이면 오작동·오용 범위도 늘며, MediaWiki 6000회/분 공유 상한을 넘을 수는 없습니다.
 
공개 요청의 IP별 bucket이 하나로 합쳐진다면 <code><nowiki>configure-public-domain.sh</nowiki></code>를 다시 실행해 <code><nowiki>.env</nowiki></code>의 서버 전용 <code><nowiki>S3RM_MCP_PROXY_SECRET</nowiki></code>을 준비하고 Caddy와 <code><nowiki>mcp-http</nowiki></code>를 함께 다시 만듭니다. 값 자체를 출력하거나 caller 헤더를 허용 목록처럼 등록하지 마세요.


=== MCP가 HTTP 421을 반환함 ===
=== MCP가 HTTP 421을 반환함 ===
481번째 줄: 567번째 줄:
=== MCP HTTP는 열리지만 도구가 위키 인증에 실패함 ===
=== MCP HTTP는 열리지만 도구가 위키 인증에 실패함 ===


초기화는 성공하지만 조회와 변경 도구에서 위키 인증 오류가 나오면 <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code> 또는 <code><nowiki>S3RM_WIKI_PASSWORD</nowiki></code>가 <code><nowiki>bootstrap</nowiki></code>이 만든 계정과 맞지 않을 수 있습니다. 전용 계정을 맞추고 공용 어댑터를 다시 만듭니다.
초기화는 성공하지만 조회와 변경 도구에서 위키 인증 오류가 나오면 기본 <code><nowiki>S3RM_WIKI_USERNAME</nowiki></code>, <code><nowiki>S3RM_WIKI_BOT_APP_ID</nowiki></code> 또는 32자 <code><nowiki>S3RM_WIKI_BOT_PASSWORD</nowiki></code>가 <code><nowiki>bootstrap</nowiki></code>이 만든 BotPassword와 맞지 않을 수 있습니다. primary <code><nowiki>S3RM_WIKI_PASSWORD</nowiki></code>는 <code><nowiki>mcp-http</nowiki></code>에 전달되지 않는 것이 정상입니다. 전용 credential을 one-shot에서 맞추고 공용 어댑터를 다시 만듭니다.


<pre><nowiki>
<pre><nowiki>
docker compose up -d --force-recreate bootstrap
docker compose stop mcp-http
docker compose run --rm bootstrap
docker compose logs --tail=100 bootstrap
docker compose logs --tail=100 bootstrap
docker compose up -d --force-recreate mcp-http
docker compose up -d --build --force-recreate mcp-http
curl --fail --silent --show-error http://127.0.0.1:8765/healthz \
  | jq --exit-status \
      '.canonical_store == "ok" and .job_runner == "ok"'
</nowiki></pre>
</nowiki></pre>


551번째 줄: 641번째 줄:
* <code><nowiki>citation</nowiki></code>은 추적 가능한 3~500자의 사람이 읽을 수 있는 참고문헌입니다.
* <code><nowiki>citation</nowiki></code>은 추적 가능한 3~500자의 사람이 읽을 수 있는 참고문헌입니다.
* <code><nowiki>kind</nowiki></code>는 <code><nowiki>paper</nowiki></code>, <code><nowiki>code</nowiki></code>, <code><nowiki>dataset</nowiki></code>, <code><nowiki>log</nowiki></code>, <code><nowiki>benchmark</nowiki></code>, <code><nowiki>other</nowiki></code> 중 하나입니다.
* <code><nowiki>kind</nowiki></code>는 <code><nowiki>paper</nowiki></code>, <code><nowiki>code</nowiki></code>, <code><nowiki>dataset</nowiki></code>, <code><nowiki>log</nowiki></code>, <code><nowiki>benchmark</nowiki></code>, <code><nowiki>other</nowiki></code> 중 하나입니다.
* <code><nowiki>verification_basis</nowiki></code>는 <code><nowiki>full_text</nowiki></code>, <code><nowiki>official_abstract</nowiki></code>, <code><nowiki>partial_source</nowiki></code>, <code><nowiki>metadata_only</nowiki></code>, <code><nowiki>not_recorded</nowiki></code> 중 하나입니다.
* <code><nowiki>verification_basis</nowiki></code>는 <code><nowiki>full_text</nowiki></code>, <code><nowiki>official_abstract</nowiki></code>, <code><nowiki>partial_source</nowiki></code>, <code><nowiki>metadata_only</nowiki></code> 중 하나를 명시합니다. <code><nowiki>not_recorded</nowiki></code>는 이전 자료를 읽는 호환값이며 새 evidence에는 쓸 수 없습니다.
* 선택 사항인 evidence ID는 해당 Lesson에서 고유하고 영문·숫자·<code><nowiki>._:-</nowiki></code>만 사용합니다.
* 선택 사항인 evidence ID는 해당 Lesson에서 고유하고 영문·숫자·<code><nowiki>._:-</nowiki></code>만 사용합니다.
* 선택 URL은 HTTP(S)이며 사용자 이름과 비밀번호가 없습니다.
* 선택 URL은 HTTP(S)이며 사용자 이름과 비밀번호가 없습니다.
* note는 2000자 이하입니다.
* note는 2000자 이하입니다.
확인 범위는 신뢰도가 아니라 실제 읽은 자료 범위입니다. 서지정보만 확인했다면 <code><nowiki>metadata_only</nowiki></code>로 표시하고 기술 내용을 채우지 않습니다. 검증을 없애거나 관리자 자격 증명을 넣지 마세요. 근거 위치를 고치고 다시 시도합니다.
확인 범위는 신뢰도가 아니라 실제 읽은 자료 범위입니다. 서지정보만 확인했다면 <code><nowiki>metadata_only</nowiki></code>로 표시하고 기술 내용을 채우지 않습니다. 검증을 없애거나 관리자 자격 증명을 넣지 마세요. 브라우저 양식은 <code><nowiki>not_recorded</nowiki></code>를 기본으로 선택하지 않으며 새 근거에 쓸 수 없습니다. high confidence Lesson을 새로 만들 때는 <code><nowiki>full_text</nowiki></code> 또는 <code><nowiki>official_abstract</nowiki></code>, <code><nowiki>supports</nowiki></code>인 초기 검증이 observation, interpretation, reusable_lesson 전부를 <code><nowiki>claim_fields</nowiki></code>로 연결해야 합니다.
 
=== 근거 검증 기록이 거부됨 ===
 
<code><nowiki>get_lesson</nowiki></code>에서 현재 revision, evidence <code><nowiki>content_digest</nowiki></code>, 기존 검증 기록을 다시 읽습니다. <code><nowiki>record_evidence_verification</nowiki></code>은 다음 조건을 모두 요구합니다.
 
* <code><nowiki>expected_revision_id</nowiki></code>가 현재 Lesson revision과 같습니다.
* <code><nowiki>expected_evidence_digest</nowiki></code>가 대상 evidence의 현재 <code><nowiki>content_digest</nowiki></code>와 같습니다.
* <code><nowiki>verification_basis</nowiki></code>가 이번에 실제로 검사한 범위이고 <code><nowiki>not_recorded</nowiki></code>가 아닙니다.
* <code><nowiki>source_identity</nowiki></code>, <code><nowiki>source_locator</nowiki></code>, <code><nowiki>coverage</nowiki></code>에 실제 식별자·위치·범위가 있으며 문장부호만 입력하지 않았습니다.
* <code><nowiki>source_sha256</nowiki></code>은 실제로 검사한 64자 소문자 SHA-256입니다.
* <code><nowiki>outcome</nowiki></code>은 <code><nowiki>supports</nowiki></code>, <code><nowiki>contradicts</nowiki></code>, <code><nowiki>inconclusive</nowiki></code> 중 하나이고 <code><nowiki>claim_fields</nowiki></code>는 중복 없이 실제로 확인한 주장만 가리킵니다.
원본 파일을 검사했다면 그 파일 바이트를 해시합니다. 저장·정규화한 텍스트를 검사했다면 그 정확한 텍스트의 UTF-8 바이트를 해시하고, 변환 방법과 보존한 산출물을 <code><nowiki>source_identity</nowiki></code>와 <code><nowiki>coverage</nowiki></code>에 명시합니다. 이전 기록은 수정·삭제할 수 없으므로 판정을 바꾸려면 새 기록을 추가합니다. MCP 호출에서는 서비스가 <code><nowiki>verified_by</nowiki></code>와 시각을 만듭니다. 브라우저·raw 저장은 MediaWiki revision actor·timestamp가 권위 출처이며, 하위 actor가 현재 저장 계정과 정확히 같고 시각이 저장 시각 전후 5분 안이며 monotonic인지 훅이 확인합니다. 양식을 5분 넘게 열어 두었다면 작성 내용을 안전하게 복사한 뒤 새로고침하거나, 편집 가능한 시각 필드를 현재 UTC로 갱신한 뒤 저장합니다. 숨겨진 시각은 직접 우회하지 말고 양식을 새로고침합니다.
 
revision이나 evidence digest가 바뀌었다면 자동 병합하지 말고 현재 내용으로 검증을 다시 계산합니다. 같은 Lesson에서 같은 <code><nowiki>operation_id</nowiki></code>를 일반 쓰기, revision, 검증 기록의 다른 종류나 digest에 쓰면 <code><nowiki>idempotency_conflict</nowiki></code>입니다. 정확히 같은 종류·요청 digest를 재시도할 때만 같은 ID를 사용합니다.


=== Lesson revision이 거부됨 ===
=== Lesson revision이 거부됨 ===
566번째 줄: 670번째 줄:
* <code><nowiki>idempotency_conflict</nowiki></code>: 같은 <code><nowiki>operation_id</nowiki></code>를 다른 요청에 재사용했습니다. 성공 여부가 불분명한 재시도는 원래 요청을 그대로 보내고, 새 작업에는 새 ID를 사용합니다.
* <code><nowiki>idempotency_conflict</nowiki></code>: 같은 <code><nowiki>operation_id</nowiki></code>를 다른 요청에 재사용했습니다. 성공 여부가 불분명한 재시도는 원래 요청을 그대로 보내고, 새 작업에는 새 ID를 사용합니다.
* <code><nowiki>write_outcome_unknown</nowiki></code>: 원래 <code><nowiki>operation_id</nowiki></code>와 요청을 그대로 재시도합니다. 적용 여부를 확인하기 전에 새 ID로 같은 수정을 보내지 않습니다.
* <code><nowiki>write_outcome_unknown</nowiki></code>: 원래 <code><nowiki>operation_id</nowiki></code>와 요청을 그대로 재시도합니다. 적용 여부를 확인하기 전에 새 ID로 같은 수정을 보내지 않습니다.
operation replay와 충돌 검사는 해당 Lesson의 최근 50개 revision을 확인합니다. <code><nowiki>write_outcome_unknown</nowiki></code>은 즉시 같은 요청으로 재시도하고, 오래 보관할 작업 ID는 다시 쓰지 않습니다.
operation replay와 충돌 검사는 해당 Lesson의 최신 revision부터 최대 10000개를 확인합니다.
 
observation, interpretation, reusable_lesson을 바꾸고 저장 후 결과 confidence가 <code><nowiki>medium</nowiki></code> 또는 <code><nowiki>high</nowiki></code>인 revision은 사용한 basis evidence의 최신 검증 기록이 <code><nowiki>full_text</nowiki></code> 또는 <code><nowiki>official_abstract</nowiki></code>, <code><nowiki>supports</nowiki></code>이고 바꾸는 필드를 <code><nowiki>claim_fields</nowiki></code>로 연결해야 합니다. confidence를 <code><nowiki>high</nowiki></code>로 하면 세 필드 모두를 뒷받침해야 합니다. <code><nowiki>contradicts</nowiki></code>와 <code><nowiki>inconclusive</nowiki></code>는 이 조건을 충족하지 않습니다. 결과 confidence가 <code><nowiki>low</nowiki></code>인 교정은 <code><nowiki>medium</nowiki></code>/<code><nowiki>high</nowiki></code>에서 낮추거나 <code><nowiki>low</nowiki></code>를 유지하는 경우 모두 <code><nowiki>not_recorded</nowiki></code>가 아닌 basis evidence로 할 수 있습니다. <code><nowiki>write_outcome_unknown</nowiki></code>은 즉시 같은 요청으로 재시도하고, 오래 보관할 작업 ID는 다시 쓰지 않습니다.


같은 주장이나 같은 논문의 보강은 같은 Lesson ID의 revision으로 남깁니다. 출처만 추가할 때는 evidence, 독립된 새 주장이 이전 주장을 실제로 대신할 때만 새 Lesson과 <code><nowiki>supersedes</nowiki></code>를 사용합니다.
같은 주장이나 같은 논문의 보강은 같은 Lesson ID의 revision으로 남깁니다. 출처만 추가할 때는 evidence, 독립된 새 주장이 이전 주장을 실제로 대신할 때만 새 Lesson과 <code><nowiki>supersedes</nowiki></code>를 사용합니다.
579번째 줄: 685번째 줄:
</nowiki></pre>
</nowiki></pre>


종료 코드 <code><nowiki>2</nowiki></code>는 감사를 완료했지만 확인할 Lesson이 있다는 뜻입니다. 로그의 <code><nowiki>lesson_id</nowiki></code>, <code><nowiki>revision_id</nowiki></code>, <code><nowiki>codes</nowiki></code>로 현재 Lesson과 근거를 확인합니다. 감사 작업은 Lesson을 자동으로 바꾸지 않습니다.
종료 코드 <code><nowiki>2</nowiki></code>는 감사를 완료했지만 확인할 Lesson이 있다는 뜻입니다. 로그의 <code><nowiki>lesson_id</nowiki></code>, <code><nowiki>revision_id</nowiki></code>, <code><nowiki>codes</nowiki></code>로 현재 Lesson과 근거를 확인합니다. 감사 작업은 Lesson을 자동으로 바꾸지 않습니다. <code><nowiki>verification_timestamp_order_invalid</nowiki></code>는 같거나 거꾸로 된 검증 시각 때문에 append 순서의 최신 판정을 안전하게 정할 수 없다는 뜻입니다. 감사는 저장 구조·digest·검증 수령 기록의 일관성만 확인하며 외부 자료의 사실 진위를 판정하지 않습니다.


종료 코드 <code><nowiki>1</nowiki></code>은 감사를 완료하지 못했다는 뜻입니다. 중앙 MCP와 HTTPS 상태를 먼저 확인합니다. 감사 중 Lesson이 바뀌었다면 현재 revision으로 다시 실행합니다. 목록이 <code><nowiki>scan_limit</nowiki></code>에서 잘렸다면 누락된 Lesson이 있으므로 성공으로 처리하지 마세요. 전체 Lesson 수에 맞춰 <code><nowiki>S3RM_SEARCH_SCAN_LIMIT</nowiki></code>과 서비스의 <code><nowiki>--max-lessons</nowiki></code>를 맞추되 각 값은 최대 1000을 넘기지 않습니다. 1000개가 넘으면 현재 작업으로 전수 감사했다고 표시하지 말고, 한도가 있는 분할 조회를 먼저 설계합니다. <code><nowiki>mcp-http</nowiki></code>를 다시 만든 뒤 수동 감사를 확인합니다. [[Help:S3 연구 메모리/백업과 복원#Lesson 내용 정기 감사|<code><nowiki>operations.md</nowiki></code>]]를 참고하세요.
종료 코드 <code><nowiki>1</nowiki></code>은 감사를 완료하지 못했다는 뜻입니다. 중앙 MCP와 HTTPS 상태를 먼저 확인합니다. 감사 중 Lesson이 바뀌었다면 현재 revision으로 다시 실행합니다. 목록이 <code><nowiki>scan_limit</nowiki></code>에서 잘렸다면 누락된 Lesson이 있으므로 성공으로 처리하지 마세요. 전체 Lesson 수에 맞춰 <code><nowiki>S3RM_SEARCH_SCAN_LIMIT</nowiki></code>과 서비스의 <code><nowiki>--max-lessons</nowiki></code>를 맞추되 각 값은 최대 1000을 넘기지 않습니다. 1000개가 넘으면 현재 작업으로 전수 감사했다고 표시하지 말고, 한도가 있는 분할 조회를 먼저 설계합니다. <code><nowiki>mcp-http</nowiki></code>를 다시 만든 뒤 수동 감사를 확인합니다. [[Help:S3 연구 메모리/백업과 복원#Lesson 내용 정기 감사|<code><nowiki>operations.md</nowiki></code>]]를 참고하세요.
586번째 줄: 692번째 줄:


완료되지 않은 출력은 진단을 위해 보존하되 복구 지점으로 표시하지 않습니다. 디스크 여유 공간, 대상 권한, 서비스 상태, 마지막 오류를 확인합니다. 이전 정상 백업을 지우지 마세요. 원인을 고친 뒤 새 시각의 디렉터리로 다시 실행합니다.
완료되지 않은 출력은 진단을 위해 보존하되 복구 지점으로 표시하지 않습니다. 디스크 여유 공간, 대상 권한, 서비스 상태, 마지막 오류를 확인합니다. 이전 정상 백업을 지우지 마세요. 원인을 고친 뒤 새 시각의 디렉터리로 다시 실행합니다.
<code><nowiki>SHOW MASTER STATUS</nowiki></code>, embedded coordinate 또는 <code><nowiki>database-binlogs.tar.gz</nowiki></code> 오류는 PITR 구간이 없거나 dump와 좌표가 다르다는 뜻입니다. binary log 검증을 빼거나 manifest 좌표를 손으로 고치지 마세요. <code><nowiki>db</nowiki></code>의 binlog 설정·볼륨·보존 기간을 고친 뒤 앱 쓰기를 멈춘 새 백업을 만듭니다. <code><nowiki>repository_untracked=present_not_archived</nowiki></code>는 untracked 파일이 있었다는 상태만 기록하며 그 이름·내용이나 <code><nowiki>.env</nowiki></code>를 백업하지 않은 것이 정상입니다.


=== 복원 스크립트가 백업을 거부함 ===
=== 복원 스크립트가 백업을 거부함 ===


checksum, manifest, archive, version 검증 실패는 현재 기준 데이터를 보호합니다. <code><nowiki>--yes</nowiki></code>를 반복해서 붙이거나, manifest를 고치거나, 다른 시각의 파일을 섞거나, SQL만 직접 가져오지 마세요. 복구 세트의 작업 사본에서 출처나 전송 손상을 조사하고, 가장 최근의 온전하고 호환되는 백업을 사용합니다. [[Help:S3 연구 메모리/백업과 복원|<code><nowiki>operations.md</nowiki></code>]]를 참고하세요.
checksum, manifest, archive, version 검증 실패는 현재 기준 데이터를 보호합니다. <code><nowiki>--yes</nowiki></code>를 반복해서 붙이거나, manifest를 고치거나, 다른 시각의 파일을 섞거나, SQL만 직접 가져오지 마세요. 복구 세트의 작업 사본에서 출처나 전송 손상을 조사하고, 가장 최근의 온전하고 호환되는 백업을 사용합니다. [[Help:S3 연구 메모리/백업과 복원|<code><nowiki>operations.md</nowiki></code>]]를 참고하세요.
기준 restore는 dump import를 binary log에 다시 쓰지 않고, DB를 멈춘 뒤 기존의 더 새로운 <code><nowiki>db_binlogs</nowiki></code> timeline을 비워 새 timeline을 시작합니다. 이 단계에서 중단되면 클라이언트를 계속 멈추고 restore 로그를 보존하세요. backup의 archived binlog는 자동 replay되지 않으므로 목표 시각 PITR이 필요하면 격리 환경에서 별도 검증합니다.
격리 검증이 project 이름, 운영 <code><nowiki>.env</nowiki></code>, loopback bind, 보조 checksum 또는 PITR 좌표를 거부했다면 guard를 끄지 마세요. mode <code><nowiki>0600</nowiki></code>의 별도 복원 env와 <code><nowiki>s3rm-restore-...</nowiki></code> 이름, 사용하지 않는 loopback 포트를 새로 정합니다. 실패한 격리 볼륨은 진단을 위해 남으므로 다음 시도 전에 출력된 정확한 <code><nowiki>down --volumes</nowiki></code> 명령으로 그 project만 정리합니다. 운영 project나 이름이 비슷한 volume을 지우지 마세요.
=== 컨테이너가 OOM 종료되거나 PID 상한에 닿음 ===
health와 함께 실제 종료 원인, 사용량, 재시작 수를 확인합니다.
<pre><nowiki>
docker stats --no-stream
docker inspect --format \
  '{{.Name}} oom={{.State.OOMKilled}} exit={{.State.ExitCode}} restarts={{.RestartCount}}' \
  $(docker compose ps -q)
docker compose logs --tail=200 SERVICE
</nowiki></pre>
무한 재시작, 큰 요청, job backlog, DB query와 임베딩 batch를 먼저 확인합니다. 단순히 <code><nowiki>mem_limit</nowiki></code>·<code><nowiki>pids_limit</nowiki></code>를 없애면 호스트 전체 장애 범위가 커집니다. 정상 peak와 호스트 여유를 측정한 뒤 해당 서비스 상한만 조정하고 재발 여부를 봅니다. Docker json-file 로그는 파일당 10MB·5개로 회전하므로 오래된 진단이 필요하면 사전에 접근이 제한된 외부 로그 보관을 설정합니다.


=== 디스크 사용량이 갑자기 늘어남 ===
=== 디스크 사용량이 갑자기 늘어남 ===


DB, upload, build cache, log, 보관 백업을 나눠 확인합니다. 이름이 있는 볼륨을 지우지 마세요. 업로드 근거 기준과 백업 보관 정책을 확인합니다. 다시 만들 수 있는 Docker build cache·image만 서버 담당자의 확인 뒤 정리합니다. 큰 원본 파일은 정해진 object store로 옮기고 Lesson에는 오래 유지되는 인용을 남깁니다.
DB, <code><nowiki>db_binlogs</nowiki></code>, upload, build cache, log, 보관 백업을 나눠 확인합니다. 이름이 있는 볼륨을 지우지 마세요. 업로드 근거 기준과 백업 보관 정책을 확인합니다. 다시 만들 수 있는 Docker build cache·image만 서버 담당자의 확인 뒤 정리합니다. 큰 원본 파일은 정해진 object store로 옮기고 Lesson에는 오래 유지되는 인용을 남깁니다.


그래도 문제가 남으면 버전, 정확한 명령, 종료 코드, 민감한 값을 지운 로그 끝부분, <code><nowiki>docker compose ps -a</nowiki></code>, 비파괴 재시작 뒤에도 재현되는지를 기록합니다.
그래도 문제가 남으면 버전, 정확한 명령, 종료 코드, 민감한 값을 지운 로그 끝부분, <code><nowiki>docker compose ps -a</nowiki></code>, 비파괴 재시작 뒤에도 재현되는지를 기록합니다.

2026년 7월 18일 (토) 19:09 기준 최신판

이 페이지는 저장소 문서에서 자동으로 동기화됩니다. 위키에서 직접 편집하지 마세요.

실패한 상태를 보존하고, 출력 길이를 제한한 진단부터 실행합니다. 원인을 찾는 동안 볼륨을 지우거나, DB 테이블을 직접 고치거나, LocalSettings.php를 다시 만들거나, 백업의 유일한 사본으로 복원을 반복하지 마세요.

docker compose config --quiet
docker compose ps -a
docker compose logs --tail=200 db mediawiki-install mediawiki bootstrap \
  mediawiki-jobs mcp-http caddy

출력을 전달할 때는 비밀번호, session cookie, Bearer 토큰, 전체 환경 dump를 지웁니다. Lesson 내용은 공개 대상이지만 운영 자격 증명은 공개하지 않습니다.

Compose에서 필수 변수가 없다고 나옴

예상 메시지:

required variable MARIADB_PASSWORD is missing a value

파일 이름이 compose.yaml 옆의 .env인지 확인하고, 필수 change-me 비밀번호를 모두 바꿉니다. 토큰을 출력하지 않고 공용 MCP 설정을 마칩니다.

S3RM_WIKI_BOT_PASSWORD가 빠졌다면 openssl rand -hex 16으로 정확히 32자를 만듭니다. S3RM_WIKI_USERNAME에는 기본 계정만 쓰며 @mcp-http를 직접 붙이지 않습니다.

test -f .env
grep -n 'change-me' .env
./scripts/configure-public-domain.sh s3wiki.yonsei.ac.kr \
  --expected-ip 165.132.118.220
docker compose config --quiet

저장소 밖에서 Compose를 실행한다면 절대 프로젝트 경로를 적습니다.

docker compose --project-directory /ABS/PATH/TO/s3wiki config --quiet

해석된 설정에는 비밀번호와 토큰 해시가 있으므로 일반 확인에는 config --quiet를 사용합니다.

호스트 포트 8765를 이미 사용 중임

8765는 도메인 배포의 loopback 진단 포트입니다. Caddy는 Compose 네트워크에서 mcp-http:8000에 연결하므로 호스트 진단 포트를 바꿔도 클라이언트 주소는 그대로입니다.

S3RM_MCP_PUBLIC_URL=https://s3wiki.yonsei.ac.kr/mcp
S3RM_MCP_BIND_ADDRESS=127.0.0.1
S3RM_MCP_HTTP_PORT=18765
S3RM_MCP_ALLOW_INSECURE_HTTP=false

공용 MCP를 다시 만들고 127.0.0.1:18765/healthz로 프로세스를 확인합니다.

docker compose up -d --force-recreate mcp-http

클라이언트 주소는 계속 https://s3wiki.yonsei.ac.kr/mcp입니다. 새 진단 포트를 외부에 열거나 로컬 대체 MCP를 시작하지 마세요.

호스트 포트 8080을 이미 사용 중임

사용하지 않는 포트를 고르고 외부 URL은 그대로 둡니다.

S3RM_HTTP_PORT=8090
MW_SERVER_URL=https://s3wiki.yonsei.ac.kr

MediaWiki를 다시 만듭니다.

docker compose up -d --force-recreate mediawiki bootstrap

이 포트는 직접 진단용입니다. 브라우저 링크와 리디렉션은 계속 https://s3wiki.yonsei.ac.kr/를 사용해야 합니다.

호스트 포트 80 또는 443을 이미 사용 중임

공개 배포에서는 Caddy가 두 포트를 모두 사용해야 합니다. 관련 없는 서비스를 바로 멈추지 말고 리스너부터 확인합니다.

ss -ltnp '( sport = :80 or sport = :443 )'
docker compose ps caddy
docker compose logs --tail=200 caddy

충돌하는 서비스를 옮기거나 설정을 바꾼 뒤 Caddy를 다시 만듭니다. 공개 포트를 다른 번호로 우회하면 일반 HTTPS URL과 인증서 검증이 실패합니다.

DNS는 맞지만 루트 주소가 연결 재설정 또는 거부로 열리지 않음

브라우저에서 루트 주소가 ERR_CONNECTION_RESET 또는 Connection refused로 끝나고 DNS가 165.132.118.220을 가리킨다면 실제 리스너부터 확인합니다.

getent ahostsv4 s3wiki.yonsei.ac.kr
ss -ltn '( sport = :80 or sport = :443 or sport = :8080 or sport = :8765 )'
docker compose ps -a

80/443은 없고 8080/8765만 보이면 MediaWiki와 MCP의 진단 포트만 열린 상태입니다. Caddy가 든 Compose domain 프로필이 시작되지 않은 것이므로 방화벽이나 커널 설정을 먼저 바꾸지 마세요.

기본 도메인 설정에서는 8080을 127.0.0.1에만 게시하므로 다른 컴퓨터에서 접속할 수 없습니다. 서버 담당자가 직접 HTTP 화면도 열기로 했다면 다음 두 값을 함께 설정하고 MediaWiki를 다시 만듭니다.

S3RM_WIKI_BIND_ADDRESS=0.0.0.0
S3RM_WIKI_DIRECT_URL=http://165.132.118.220:8080
docker compose up -d --no-deps --force-recreate mediawiki

이후 서버에서 직접 주소가 자기 주소로 이동하는지 확인합니다.

curl --silent --show-error --head http://165.132.118.220:8080/

Location이 같은 IP의 8080을 가리키면 도메인과 직접 주소가 분리된 것입니다. 이 설정은 Docker 리스너만 엽니다. 외부 여러 지역에서 계속 timeout이면 학교 경계에서 TCP 8080이 차단된 상태이므로 보안정책에 8080을 별도로 추가해야 합니다. 직접 HTTP 주소를 MCP에 등록하지 마세요. MCP는 계속 HTTPS 도메인 하나만 사용합니다.

Docker socket 접근이 permission denied로 실패하고 sudo 암호가 필요한 서버에서는 대화형 셸에서 다음 한 줄을 실행합니다.

cd /home/mrcha033/Projects/s3wiki; sudo -v; make domain-up COMPOSE='sudo docker compose'

sudo -v는 자격을 먼저 확인합니다. COMPOSE에만 sudo를 붙이면 저장소 파일과 .env는 현재 사용자 소유로 남습니다. sudo make domain-up을 실행하거나 /var/run/docker.sock을 mode 666으로 바꾸지 마세요. 이 증상 때문에 sysctl을 바꾸지 않습니다.

실행 뒤 서비스, Caddy 로그, 공개 포트, HTTPS 응답을 차례로 확인합니다.

sudo docker compose --profile domain ps -a
sudo docker compose --profile domain logs --tail=200 caddy
ss -ltn '( sport = :80 or sport = :443 )'
curl --fail --silent --show-error --head https://s3wiki.yonsei.ac.kr/
curl --fail --silent --show-error \
  'https://s3wiki.yonsei.ac.kr/api.php?action=query&meta=siteinfo&format=json' \
  >/dev/null

caddy가 실행 중이고 80/443이 리슨하며 두 curl이 성공해야 외부 브라우저에서 확인합니다. 실패하면 Caddy 로그의 인증서 또는 포트 오류를 먼저 해결합니다.

HTTPS 인증서가 발급되지 않음

DNS가 이 서버를 가리키고 두 공개 포트가 서버까지 들어오는지 확인합니다.

getent ahostsv4 s3wiki.yonsei.ac.kr
docker compose ps caddy
docker compose logs --tail=300 caddy

예상 IPv4는 165.132.118.220입니다. DNS가 맞다면 캠퍼스·호스트 방화벽과 포트 소유 프로세스를 확인합니다. caddy_data를 지워도 DNS나 라우팅은 고쳐지지 않으며 불필요한 인증서 요청만 늘어날 수 있습니다. 외부 연결을 고친 뒤 Caddy만 다시 만들고 다른 네트워크에서 확인합니다.

docker compose up -d --force-recreate caddy
curl --fail --silent --show-error --head https://s3wiki.yonsei.ac.kr/

Caddy가 80/443을 리슨하고 서버 안에서는 HTTP 308이 나오는데 로그에 다음 문구가 있다면 애플리케이션 문제가 아닙니다.

Timeout during connect (likely firewall problem)

Let’s Encrypt의 http-01tls-alpn-01이 모두 이 오류라면 외부에서 서버의 TCP 80과 443에 들어오지 못하는 상태입니다. 연세대학교 정보통신처의 보안정책서비스 안내에 따라 보안정책 신청을 합니다. 네트워크 PORT 신청은 벽면 네트워크 포트 설치 절차이므로 이 문제와 다릅니다. 신청 화면에는 다음 값을 적습니다. 소속, 성명, 연락처, 이메일은 신청자가 직접 입력합니다. 개인 연락처나 계정 정보는 이 저장소에 남기지 않습니다.

신청 항목 입력값
소속 신청자 소속 직접 입력
성명 신청자 성명 직접 입력
연락처 신청자 연락처 직접 입력
이메일 신청자 연세 이메일 직접 입력
시작일 2026-07-16
종료일 2029-07-15
출발지 IP 공개 웹서비스 80/443 예외 적용 요청. 출발지는 공개 웹서비스이므로 특정할 수 없습니다.
목적지 IP 165.132.118.220
프로토콜 TCP만 선택
사용포트 80, 443
요청사유 및 특이사항 아래 문안 붙여넣기

정책 기간은 최대 3년이므로 위 종료일은 시작일로부터 3년 이내입니다. 출발지 IP 칸이 숫자 주소만 받는다면 임의의 IP를 넣지 말고 보안정책 담당자 02-2123-6419에 공개 웹서비스 예외 입력 방법을 확인합니다.

요청사유 및 특이사항에는 다음 내용을 붙여넣습니다.

S3 연구실에서 함께 사용하는 위키(s3wiki.yonsei.ac.kr)를 운영하려고 합니다.
DNS는 165.132.118.220으로 등록되어 있고 서버도 80/443 포트에서 대기 중이지만,
현재 외부 연결이 차단되어 HTTPS 인증서 검증이 실패합니다.

HTTPS 접속과 인증서 자동 발급·갱신을 위해 목적지
165.132.118.220의 TCP 80, 443에 대한 외부 인바운드 허용을 요청드립니다.
출발지는 공개 웹서비스이므로 특정할 수 없습니다. 80은 인증서 검증과 HTTPS 전환,
443은 위키와 MCP 접속에 사용합니다. 그 밖의 포트 개방은 요청하지 않습니다.

공식 안내에는 필요한 웹서비스의 80/443은 출발지 IP 특정 조건의 예외라고 적혀 있습니다. 보안정책 문의는 정보통신처 02-2123-6419, 인터넷 문의는 02-2123-3411입니다.

포트가 열리면 Caddy가 인증서 발급을 자동으로 다시 시도합니다. Caddy 로그의 certificate obtained successfully와 서버의 make verify-domain을 확인한 뒤 학교 밖 네트워크의 브라우저를 다시 여세요. 인증서 볼륨을 지우거나 자체 서명 인증서로 바꾸지 마세요.

docker compose --profile domain logs --follow caddy
make verify-domain

/etc/group에는 이미 사용자가 docker 그룹에 있는데 현재 셸만 Docker 권한을 읽지 못한다면 로그아웃한 뒤 다시 로그인합니다. 바로 새 셸에서 작업하려면 newgrp docker를 실행한 뒤 위 명령을 사용합니다.

MariaDB가 unhealthy 상태임

관련 상태와 로그 끝부분만 확인합니다.

docker compose ps db
docker compose logs --tail=200 db

주요 원인은 비어 있거나 잘못된 필수 비밀번호, 디스크 부족, 볼륨 소유권 문제, db_data 초기화 뒤 .env의 DB 비밀번호만 바꾼 경우입니다. 환경 변수만 바꿔도 기존 MariaDB 계정은 바뀌지 않습니다. 이전에 맞던 .env를 복구하거나 계획된 DB 계정 교체를 수행하세요. 오류를 없애려고 db_data를 지우지 마세요.

binary log 설정 오류가 보이면 db_binlogs 볼륨과 현재 master 상태를 읽기 전용으로 확인합니다. 출력에는 비밀번호가 없지만 DB 이름·파일 번호는 운영 정보로 취급합니다.

docker volume ls | grep db_binlogs
docker compose exec -T db sh -c \
  'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -h127.0.0.1 \
   --batch --skip-column-names -e "SHOW MASTER STATUS"'

결과가 비어 있거나 mariadb-bin.000001 형식이 아니면 새 백업을 만들지 마세요. Compose 명령과 볼륨 mount를 먼저 고치고 DB를 계획된 작업 시간에 다시 만듭니다. 기존 db_binlogs를 임의로 지우면 연속 PITR 구간을 잃습니다.

MediaWiki가 unhealthy 상태임

첫 빌드와 마이그레이션에는 몇 분이 걸릴 수 있습니다. 의존 서비스와 로컬 상태 경로를 확인합니다.

docker compose ps -a
docker compose logs --tail=300 mediawiki
curl --fail --silent --show-error \
  'https://s3wiki.yonsei.ac.kr/api.php?action=query&meta=siteinfo&format=json'

DB 인증 불일치, 호환되지 않는 고정 확장 기능, Composer 빌드 실패, 디스크 부족, 잘못된 MW_SERVER_URL을 확인합니다. 로그의 원인을 고치고 서비스를 다시 만드세요. 생성된 설정 볼륨을 직접 편집하지 않습니다.

브라우저에 설치 세션 만료가 표시됨

이 메시지는 MediaWiki의 대화형 /mw-config/ 설치 화면에서 나옵니다. 이 배포는 해당 화면을 사용하지 않습니다. mediawiki-install one-shot이 Apache와 분리된 secret 경계에서 CLI로 설치하고, 생성된 설정은 mediawiki_config 볼륨에 저장합니다. mediawiki entrypoint는 설정이 없으면 설치를 시도하지 않고 종료합니다. 세션 만료는 지원하지 않는 경로를 연 증상이지 원인이 아닙니다.

<!-- Contract marker: Do not increase session.gc_maxlifetime --> session.gc_maxlifetime를 늘리거나, 브라우저 설치를 다시 시작하거나, LocalSettings.php를 내려받거나, 볼륨을 지우지 마세요. 세션 수명만 늘려도 CLI 설정은 고쳐지지 않습니다.

설치 탭을 닫고 상태와 로그를 확인합니다.

docker compose ps -a
docker compose logs --tail=300 mediawiki-install mediawiki bootstrap

mediawiki가 실행 중이면 설정 내용을 출력하지 않고 영구 설정과 이미지의 symlink를 확인합니다.

docker compose exec mediawiki \
  sh -c 'test -s /var/www/data/LocalSettings.php &&
    test "$(readlink -f /var/www/html/LocalSettings.php)" = /var/www/data/LocalSettings.php &&
    stat -c "%a %U %G %n" /var/www/data /var/www/data/LocalSettings.php \
      /opt/s3wiki/LocalSettings.s3.php /var/www/html/LocalSettings.php'

이미지는 시작할 때마다 다음 권한을 맞춥니다. 설정 디렉터리는 750 root:www-data, 생성된 LocalSettings.php640 root:www-data, web root symlink는 www-data 소유입니다. 비밀 정보가 없는 이미지 설정 파일은 Apache가 읽을 수 있습니다. upstream web root가 sticky이고 Linux의 보호된 symlink 동작이 있으므로 symlink 소유권도 필요합니다. API healthcheck는 설치 화면의 200 OK HTML이 아니라 MediaWiki JSON을 확인합니다.

위 검사가 성공하면 mediawiki가 healthy, bootstrap이 코드 0이 될 때까지 기다린 뒤 /mw-config/가 아니라 MW_SERVER_URL을 엽니다.

기준 데이터가 한 번도 없었던 새 시험 스택이라면 이미지와 서비스를 다시 만들어 CLI 설치를 실행합니다.

docker compose up -d --build --force-recreate mediawiki bootstrap
docker compose logs -f mediawiki-install mediawiki bootstrap

운영하던 배포에서 LocalSettings.php를 잃었다면 여기서 진단을 멈춥니다. 기존 DB에 다시 설치하거나 볼륨을 지우지 마세요. operations.md에 따라 같은 시점의 DB, 설정, 업로드 백업을 복원합니다.

mediawiki-install이 0이 아닌 코드로 끝남

새 볼륨의 첫 설치에서만 이 one-shot이 실제 설치를 합니다. 기존 LocalSettings.php가 있으면 코드 0으로 아무것도 바꾸지 않는 것이 정상입니다.

docker compose logs --tail=200 mediawiki-install

secret 파일 없음·빈 값·여러 줄 오류면 .envMW_ADMIN_PASSWORD와 Compose secret 선언을 확인합니다. DB 연결 오류면 먼저 db health를 고칩니다. 설치 실패 뒤 브라우저 설치로 우회하거나 부분 생성된 설정을 직접 편집하지 마세요. 새 시험 볼륨이라 데이터가 없다는 사실을 확인한 경우에만 실패 원인을 고친 뒤 같은 one-shot을 다시 실행합니다. 운영 볼륨이면 복구 절차로 전환합니다.

bootstrapExited (0)

정상입니다. bootstrap은 daemon이 아니라 일회성 동기화 작업입니다. 평소에는 db, mediawiki, mediawiki-jobs, mcp-http가 healthy이고 mediawiki-install·bootstrap은 코드 0으로 종료됩니다.

bootstrap이 0이 아닌 코드로 끝남

docker compose logs --tail=300 bootstrap

주요 원인은 다음과 같습니다.

  • administrator login failed: .envMW_ADMIN_PASSWORD가 설치된 위키와 다릅니다. 이 값은 최초 설치 때만 관리자 계정을 만듭니다. .env만 바꿔도 위키 비밀번호는 바뀌지 않습니다.
  • manifest source is missing: 이미지 또는 저장소가 불완전합니다. 온전한 checkout에서 다시 빌드합니다.
  • edit failed/permission denied: 관리자 권한이 없어졌거나 관리 페이지가 예상과 다르게 보호되어 있습니다.
  • account provisioning failed: S3RM_WIKI_USERNAME을 확인합니다. DB 테이블을 직접 고치지 말고 MediaWiki 절차로 전용 계정을 교체합니다.
  • BotPassword secret/validation failed: S3RM_WIKI_BOT_PASSWORDopenssl rand -hex 16으로 만든 정확히 32자인지, app ID가 안전한 ASCII 1~32자인지 확인합니다. 로그에 secret을 붙이지 마세요.
  • BotPassword login/effective rights failed: bootstrap이 grant를 저장한 뒤 <기본 계정>@<app-id> 실제 로그인 또는 agent 유효 권한 확인에 실패했습니다. primary 비밀번호를 mcp-http에 넣어 우회하지 말고 아래 전환 절차를 따릅니다.

원인을 고친 뒤 다시 실행합니다.

docker compose up -d --build --force-recreate bootstrap
docker compose logs --tail=200 bootstrap

bootstrap은 관리 스키마와 양식을 맞추지만 일반 Lesson revision을 덮어쓰지 않습니다. 동기화 경고가 반복되면 우회하지 말고 원인을 확인하세요.

관리자 비밀번호가 .env와 맞지 않음

첫 설치 뒤 MW_ADMIN_PASSWORD만 바꿔도 MediaWiki에 저장된 비밀번호는 바뀌지 않습니다. 기존 비밀번호를 알고 있다면 .env 값을 다시 맞춥니다. 알 수 없다면 권한이 있는 서버 담당자가 고정된 MediaWiki 버전의 명령 도움말을 확인한 뒤 재설정합니다.

docker compose exec mediawiki \
  php maintenance/run.php resetPassword --help

현재 이미지가 출력한 문법만 사용합니다. 복구 작업을 기록하고, 추적하지 않는 .env를 갱신한 뒤 bootstrap을 다시 실행합니다. 새 비밀번호를 셸 기록에 직접 넣지 마세요.

기존 설치를 BotPassword로 전환하거나 BotPassword를 교체함

기존 S3RM_WIKI_USERNAME·S3RM_WIKI_PASSWORD는 기본 계정을 유지하기 위한 one-shot 값입니다. .env에 다음 두 값을 추가합니다. 사용자 이름에는 @app-id를 붙이지 않습니다.

S3RM_WIKI_BOT_APP_ID=mcp-http
S3RM_WIKI_BOT_PASSWORD=<openssl-rand-hex-16-output>

primary 비밀번호가 장기 서비스에 남는 시간을 만들지 않도록 다음 순서를 사용합니다.

docker compose stop mcp-http
docker compose run --rm bootstrap
docker compose up -d --build --force-recreate mcp-http
curl --fail --silent --show-error http://127.0.0.1:8765/healthz \
  | jq --exit-status \
      '.canonical_store == "ok" and .job_runner == "ok"'

bootstrap 로그에는 S3ResearchAgent@mcp-http와 같은 로그인 이름만 나와야 하며 비밀번호는 나오지 않아야 합니다. health가 canonical_store=ok이면 장기 MCP가 BotPassword로 실제 위키 로그인을 완료한 것입니다. 실패하면 mcp-http를 멈춘 채 이전 .env의 안전한 사본으로 되돌리거나 bootstrap 로그를 확인합니다.

app ID를 바꾸면 새 app ID가 추가되고 이전 app ID는 자동 삭제되지 않습니다. 새 서비스 확인 후 관리 화면 Special:BotPasswords에서 이전 app ID만 폐기합니다. 기본 계정 자체나 agent 그룹, DB row를 직접 지우지 마세요.

양식이 없거나 Lesson이 template 원문으로 표시됨

  1. bootstrap이 코드 0으로 끝났는지 확인합니다.
  2. /index.php/Special:Version에서 Semantic MediaWiki, Page Forms, S3 Research Memory가 로드되었는지 확인합니다.
  3. Form:Lesson과 기준 template이 있는지 확인합니다.
  4. bootstrap을 다시 실행하고 로그를 봅니다.
docker compose up -d --force-recreate bootstrap
docker compose logs --tail=200 bootstrap

관리 양식이나 template 페이지를 직접 고치지 마세요. bootstrap이 이미지의 원본으로 맞춥니다.

mediawiki-jobs가 unhealthy 상태임

runner는 한 process에서 batch당 기본 25건·20초만 처리합니다. 마지막 성공 batch가 기본 120초보다 오래되면 unhealthy가 되며, 반복 실패는 15초부터 최대 5분까지 지수 backoff합니다.

docker compose ps mediawiki-jobs
docker compose logs --tail=200 mediawiki-jobs
docker compose exec mediawiki-jobs cat /run/s3rm-jobs/status.env
docker compose exec mediawiki php maintenance/run.php showJobs --group

runner_generation, consecutive_failures, queue_depth, updated_epoch를 보고 DB·확장 기능·디스크 문제를 먼저 해결합니다. 상태 파일에는 secret이나 Lesson 본문이 없어야 합니다. S3RM_JOBS_HEALTH_STALE_SECONDSS3RM_JOBS_MAX_SECONDS + S3RM_JOBS_IDLE_SECONDS + 30 이상이어야 합니다. 잘못된 S3RM_JOBS_* 값은 이 관계와 각 허용 범위 안으로 고치고 컨테이너를 다시 만듭니다.

docker compose up -d --force-recreate mediawiki-job-state-init mediawiki-jobs

health만 통과시키기 위해 stale 상한을 크게 늘리거나 --nothrottle, 무제한 job 수, 다중 runner를 사용하지 마세요. backlog가 정상 부하로 계속 늘 때만 처리 시간과 건수를 작게 올리고 CPU·DB latency를 함께 관찰합니다.

Lesson ID 또는 양식 검증이 실패함

기준 ID 형식은 다음과 같습니다.

Lesson:<lowercase_slug>

slug는 소문자, 숫자, _, -만 사용할 수 있습니다. 첫 글자는 문자나 숫자이고 최대 128자입니다. 화면 제목인 title은 별도 필드이며 일반적인 대문자를 쓸 수 있습니다. 필수 연구 필드는 비울 수 없습니다. confidence는 low, medium, high 중 하나입니다.

새 Lesson이 검색되지 않음

페이지를 직접 열어 Lesson: 네임스페이스인지 먼저 확인합니다. /index.php/Special:Search에서 제목이나 본문의 구별되는 정확한 단어로 검색합니다. 먼저 bounded job runner와 대기 queue를 확인합니다. 색인이 손상되었거나 DB 복원 뒤 누락된 경우에만 검색 색인을 다시 만듭니다.

docker compose ps mediawiki-jobs
docker compose exec mediawiki-jobs cat /run/s3rm-jobs/status.env
docker compose exec mediawiki php maintenance/run.php showJobs --group
docker compose exec mediawiki \
  php maintenance/run.php rebuildtextindex --quiet

mediawiki-jobs가 unhealthy이면 mediawiki-jobs가-unhealthy-상태임을 먼저 해결합니다. queue를 빨리 비우려고 무제한 runJobs나 여러 process를 동시에 실행하지 마세요.

기본 S3RM_EMBEDDING_PROVIDER=disabled에서는 띄어쓰기와 단어 기반 검색이므로 비슷한 뜻의 다른 단어까지 찾는다고 보장하지 않습니다. 도메인을 넘는 연결은 관계나 find_analogies를 사용합니다. 의미 검색을 켰더라도 클라이언트가 retrieval_mode=hybrid_v1을 명시해야 합니다. 서비스 시작 시 단 하나의 백그라운드 작업이 전체 파생 인덱스를 프리웜합니다. hybrid 요청은 구축을 기다리지 않으며, 아직 snapshot이 없으면 semantic_status=building과 함께 즉시 기존 검색으로 대체됩니다. MCP Lesson 저장의 dirty hint는 즉시 백그라운드 대조를 시작합니다. 설정한 간격이 지나면 다음 hybrid 요청이 브라우저 편집까지 포함한 대조를 시작하며 기존 snapshot을 계속 사용합니다. 최종 결과는 MediaWiki 최신 revision으로 다시 확인하므로 오래된 의미 후보가 새 본문으로 반환되지는 않습니다.

MCP 검색만 실패한다면 어댑터와 namespace ID를 확인하고 Action API 요청에 srwhat=text가 포함되는지 확인합니다. 조회에는 Bearer 토큰이 필요하지 않습니다.

의미 검색이 시작되지 않거나 기존 검색으로 대체됨

mcp-http 시작 로그에 임베딩 설정 오류가 있으면 .env에서 다음 항목을 먼저 확인합니다.

S3RM_EMBEDDING_PROVIDER=openai-compatible
S3RM_EMBEDDING_API_URL=https://embedding.example/v1/embeddings
S3RM_EMBEDDING_MODEL=multilingual-embedding-model
S3RM_EMBEDDING_DIMENSIONS=768

provider 값은 disabled 또는 openai-compatible만 받습니다. 사용 시 URL, 모델, 차원이 모두 필요합니다. URL에는 사용자 이름, 비밀번호, query와 fragment를 넣을 수 없습니다. 비-loopback HTTP는 S3RM_EMBEDDING_ALLOW_INSECURE_HTTP=true를 명시하지 않으면 거부합니다.

설정을 고친 뒤 서비스만 다시 만듭니다.

docker compose config --quiet
docker compose up -d --build --force-recreate mcp-http
docker compose logs --tail=200 mcp-http

재시작 직후 semantic_status=building으로 기존 검색이 반환되는 것은 정상입니다. 잠시 뒤 새 첫 페이지 요청에서 ready인지 확인합니다. 사용할 기존 snapshot이 있는 동안 백그라운드 갱신 중이면 stale, 갱신에 실패하면 degraded로 hybrid 결과를 계속 제공할 수 있습니다.

unavailable이 계속되거나 hybrid_v1 요청이 반복해서 기존 검색으로 대체되면 제공자 timeout, 대기열 포화, 응답 차원 불일치 또는 0·비정상 벡터일 수 있습니다. 제공자의 모델 차원이 S3RM_EMBEDDING_DIMENSIONS와 같은지 확인합니다. 제한을 무조건 키우기 전에 endpoint 상태와 모델 설정을 확인하세요. 의미 제공자 장애만으로 /healthz를 실패시키지는 않으며 전체 텍스트와 관계 검색은 계속 제공합니다.

진단을 공유할 때 S3RM_EMBEDDING_API_KEY, Authorization 헤더, 원문 query, Lesson 본문과 전체 환경 dump를 지웁니다. API key를 명령행 인자나 임시 Lesson에 넣어 연결을 시험하지 마세요.

MCP 클라이언트가 끊겼거나 도구가 보이지 않음

모든 클라이언트는 같은 공용 Streamable HTTP URL을 사용해야 합니다. 서버에서 자격 증명을 출력하지 않고 전체 상태와 프로세스 경로를 확인합니다.

docker compose ps -a caddy mcp-http mediawiki bootstrap db
curl --fail --silent --show-error http://127.0.0.1:8765/livez
curl --fail --silent --show-error http://127.0.0.1:8765/readyz
docker compose logs --tail=200 mcp-http caddy

/livez는 MCP 프로세스 실행만 확인합니다. /healthz/readyz는 BotPassword로 canonical MediaWiki store까지 확인하며 연결이나 로그인이 실패하면 503을 반환합니다. 다른 PC에서 setup.md의 토큰 없는 초기화를 정확한 https://s3wiki.yonsei.ac.kr/mcp 주소로 실행합니다. Authorization 없이 초기화되어야 하며 도구 목록과 다섯 조회 도구도 공개되어야 합니다.

다음 값을 확인합니다.

S3RM_MCP_PUBLIC_URL=https://s3wiki.yonsei.ac.kr/mcp
S3RM_MCP_WRITE_AUTH_MODE=token
S3RM_MCP_BIND_ADDRESS=127.0.0.1
S3RM_MCP_ALLOW_INSECURE_HTTP=false

먼저 Authorization 없이 공개 조회를 시험합니다. 로컬 health만 성공하고 원격 초기화가 실패하면 Caddy 로그, TLS, DNS, 80/443을 확인합니다. 클라이언트를 localhost에 연결하거나 별도 MCP 프로세스를 만들지 마세요.

클라이언트 주소는 정확히 공용 URL이어야 합니다. token 모드에서 변경 도구를 쓸 때만 공용 Bearer 토큰을 불러옵니다. open 모드에서는 조회와 변경 도구 다섯 개에 인증이 필요하지 않습니다. 클라이언트 사용자에게 Docker 명령, MediaWiki API URL, 위키 자격 증명을 전달하지 마세요. 플랫폼별 확인은 mcp-clients.md를 봅니다.

MCP /healthz가 실패함

서버에서 Connection refused가 나오면 mcp-http가 설정된 bind·port에 리슨하지 않는 상태입니다. timeout은 호스트 방화벽이나 멈춘 프로세스일 수 있습니다.

docker compose ps mcp-http
docker compose logs --tail=200 mcp-http
docker compose up -d --force-recreate mcp-http

주요 시작 오류는 잘못된 쓰기 인증 모드, token 모드에서 잘못되거나 빠진 64자리 공용 토큰 해시, 43~256자 URL-safe revision HMAC 키 누락, github 모드에서 빠진 client ID·secret·allowlist, /mcp로 끝나지 않는 공개 URL, 명시적인 허용 없이 쓴 비-loopback HTTP, 이미 사용 중인 bind port입니다. .env를 고치고 docker compose config --quiet를 실행한 뒤 mcp-http만 다시 만듭니다.

GitHub OAuth 연결이 실패함

GitHub OAuth App callback URL이 다음 값과 정확히 같은지 확인합니다.

https://s3wiki.yonsei.ac.kr/oauth/github/callback

S3RM_GITHUB_ALLOWED_USERSS3RM_GITHUB_ALLOWED_ORGS는 GitHub 로그인 이름을 쉼표로 구분합니다. private 조직 membership을 확인하려면 사용자가 GitHub 승인 화면에서 read:org를 허용해야 합니다. mcp-http를 다시 만들면 메모리에 있던 OAuth client와 token이 사라지므로 ChatGPT 앱 인증을 다시 진행합니다. 일반 설정은 URL https://s3wiki.yonsei.ac.kr/mcp, bind 127.0.0.1, allow flag false입니다. 같은 endpoint의 다섯 변경 도구에는 mcp:write scope가 필요합니다.

/livez는 성공하지만 /healthz·/readyz가 실패하면 프로세스는 살아 있으나 위키 연결·BotPassword 로그인 또는 bounded job runner가 준비되지 않은 상태입니다. 응답의 canonical_storejob_runner를 확인하고, job_runner=unavailable이면 mediawiki-jobs가 unhealthy 상태임을 따릅니다. 세 경로가 성공하는데 공용 /mcp만 실패하면 직접 IP 경로와 아래 HTTP 상태 항목을 확인합니다.

MCP 변경 도구에서 인증 오류가 남

먼저 .envS3RM_MCP_WRITE_AUTH_MODE를 확인합니다. open인데 인증 오류가 남는다면 설정 변경 뒤 mcp-http를 다시 만들지 않은 상태일 수 있습니다.

docker compose up -d --build --force-recreate mcp-http

아래 토큰 진단은 token 모드에만 적용합니다.

이 오류가 있어도 익명 초기화, 도구 목록, 조회는 동작해야 합니다. 다섯 변경 도구의 인증 오류는 Bearer 헤더가 없거나 형식이 틀리거나 S3RM_MCP_TOKEN_SHA256과 맞지 않는다는 뜻입니다. 비교할 때 어느 값도 출력하지 마세요. GitHub 모드에서는 mcp:write scope가 필요합니다. open 모드라면 다섯 도구에 별도 인증이 없습니다. revise_lesson 요청이 위키 저장 경계에서 거부되면 서버 내부의 S3RM_WIKI_REVISION_HMAC_KEYmediawikimcp-http가 같은 값으로 읽는지 확인하되 값을 출력하지 않습니다.

서버에서 기존 HTTPS 토큰과 해시를 바꾸거나 출력하지 않고 확인합니다.

./scripts/configure-mcp-token.sh \
  --public-url https://s3wiki.yonsei.ac.kr/mcp

비공개 클라이언트 토큰이 없거나 맞지 않는다고 나오면 기존 전달 사본에서 원문을 복구합니다. 모든 쓰기 클라이언트를 함께 바꿀 수 있을 때만 --rotate를 사용하세요. mcp-http를 다시 만들면 이전 토큰이 무효가 됩니다.

쓰기 클라이언트에서는 시작 전에 지원하는 secret 또는 환경 설정으로 토큰을 불러옵니다. 추적되는 JSON·TOML이나 공개 진단에 넣지 마세요. 정적 헤더를 저장하지 못하는 호스팅 커넥터에는 OAuth 진입점이 필요합니다.

MCP가 HTTP 403을 반환함

이 파일럿에서 403은 보통 요청 Origin이 정확히 허용되지 않았다는 뜻입니다. 민감한 값을 지운 요청 Origin을 S3RM_MCP_PUBLIC_URL, S3RM_MCP_ALLOWED_ORIGINS와 비교합니다. 의도한 정확한 HTTP(S) Origin만 쉼표로 추가하고 서비스를 다시 만듭니다.

docker compose up -d --force-recreate mcp-http

*를 추가하거나 Origin 검사를 끄지 마세요. Origin은 전체 /mcp URL이 아닙니다. CLI 클라이언트는 보통 Origin을 생략하지만 호스팅형·브라우저형 클라이언트는 보낼 수 있습니다.

여러 변경 작업이 rate limit에서 멈춤

모든 MCP 클라이언트는 같은 S3RM_WIKI_USERNAME으로 저장하므로 계정별 편집 한도를 함께 사용합니다. 기본 agent 한도는 6000회/분이며, 일반 로그인 계정의 90회/분과 편집 이외 작업의 제한은 그대로입니다. 현재 컨테이너에 들어간 값은 비밀값을 출력하지 않고 확인할 수 있습니다.

printf '%s\n' 'var_export( $wgRateLimits["edit"]["agent"] );' \
  | docker compose exec -T mediawiki php maintenance/run.php eval.php

결과가 6000, 60이 아니면 .envS3RM_AGENT_EDITS_PER_MINUTE를 확인하고 MediaWiki 컨테이너를 다시 만듭니다. restart만 하면 기존 컨테이너 환경이 유지됩니다.

docker compose up -d --force-recreate mediawiki

저장소의 MediaWiki 설정 파일을 갱신한 첫 배포에는 --build도 붙입니다. 한도를 조정할 때 일반 user 값을 바꾸거나 agentnoratelimit 권한을 주지 마세요.

오류 code가 write_rate_limited 또는 server_busy이면 MediaWiki보다 앞의 MCP admission입니다. /healthz의 비밀값 없는 현재 active·waiting·principal 수와 설정된 상한을 확인합니다.

curl --fail --silent --show-error http://127.0.0.1:8765/healthz \
  | jq '.write_admission'
docker compose exec mcp-http sh -c \
  'env | sort | grep "^S3RM_MCP_WRITE_"'

기본은 동시 8개, queue 64개·2초, principal별 지속 600회/분·burst 120입니다. open 모드는 client IP별, github는 subject별, token은 공유 token 전체가 한 bucket입니다. retry_after_seconds를 따르고, queue가 계속 차면 MariaDB latency와 job queue를 먼저 확인합니다. 필요한 경우 S3RM_MCP_WRITE_MAX_CONCURRENCY나 queue 값을 작게 조정한 뒤 mcp-http--force-recreate합니다. rate와 burst를 높이면 오작동·오용 범위도 늘며, MediaWiki 6000회/분 공유 상한을 넘을 수는 없습니다.

공개 요청의 IP별 bucket이 하나로 합쳐진다면 configure-public-domain.sh를 다시 실행해 .env의 서버 전용 S3RM_MCP_PROXY_SECRET을 준비하고 Caddy와 mcp-http를 함께 다시 만듭니다. 값 자체를 출력하거나 caller 헤더를 허용 목록처럼 등록하지 마세요.

MCP가 HTTP 421을 반환함

421은 정확한 Host 헤더가 공개 URL host 또는 S3RM_MCP_ALLOWED_HOSTS 항목과 맞지 않는다는 뜻입니다. 프록시가 Host를 바꾸거나, 클라이언트가 이전 IP URL을 쓰거나, 공개 URL의 포트가 틀린 경우가 많습니다.

클라이언트 주소를 S3RM_MCP_PUBLIC_URL과 맞춥니다. 저장소의 Caddyfile은 공개 Host를 보존하므로 Caddy를 통과한 421은 보통 .env나 클라이언트가 이전 주소를 쓰는 경우입니다. 도메인 스크립트를 다시 실행하고 mcp-http와 Caddy를 다시 만듭니다. 등록된 공용 URL 대신 localhost나 IP를 쓰지 마세요.

MCP HTTP는 열리지만 도구가 위키 인증에 실패함

초기화는 성공하지만 조회와 변경 도구에서 위키 인증 오류가 나오면 기본 S3RM_WIKI_USERNAME, S3RM_WIKI_BOT_APP_ID 또는 32자 S3RM_WIKI_BOT_PASSWORDbootstrap이 만든 BotPassword와 맞지 않을 수 있습니다. primary S3RM_WIKI_PASSWORDmcp-http에 전달되지 않는 것이 정상입니다. 전용 credential을 one-shot에서 맞추고 공용 어댑터를 다시 만듭니다.

docker compose stop mcp-http
docker compose run --rm bootstrap
docker compose logs --tail=100 bootstrap
docker compose up -d --build --force-recreate mcp-http
curl --fail --silent --show-error http://127.0.0.1:8765/healthz \
  | jq --exit-status \
      '.canonical_store == "ok" and .job_runner == "ok"'

Compose 내부 API 주소는 http://mediawiki/api.php입니다. 호스트의 localhost 주소가 아닙니다. 관리자 자격 증명을 넣거나, api.php를 클라이언트에 직접 열거나, MCP 변경 인증을 끄는 방식으로 우회하지 마세요.

Cursor가 잘못되었거나 요청과 맞지 않음

Cursor는 불투명한 값이며, 이를 만든 도구, query, filter, 선택 field, citation detail, analogy threshold에 묶입니다. get_lesson의 하위 cursor와 audit_lessons cursor는 읽은 revision에도 묶입니다. next_cursor를 바꾸지 말고 그대로 넘깁니다. parameter나 대상 revision이 달라졌다면 이전 cursor를 버리고 첫 페이지부터 시작합니다. Cursor는 Lesson ID가 아니며 해독하거나 편집하지 않습니다.

결과가 잘렸거나 limit보다 적음

모든 직렬화 결과에는 크기 제한이 있습니다. 목록 페이지에는 page_limited_by_output, 모든 결과에는 output_truncated가 표시될 수 있습니다. field 수나 limit을 줄이고 next_cursor를 이어서 사용합니다. 목록 limit은 1~20이며 기본값은 5입니다. S3RM_MAX_OUTPUT_BYTES 기본값은 32768이고 2048~65536만 받습니다. 그보다 높은 hard limit으로 올릴 수 없습니다. SDK 검증 오류를 포함한 전체 JSON-RPC HTTP body에는 별도 212992바이트 제한이 있습니다.

큰 결과를 한 번에 받으려고 제한을 높이지 마세요. 먼저 검색 요약을 받고, 추린 결과에 대해 선택 field로 get_lesson을 호출합니다. citation_detail=full에서 근거 한 항목이 한도에 맞지 않으면 note를 조용히 자르지 않고 output_budget_error를 반환합니다. 필요한 근거를 더 작은 페이지로 요청하되, 한 항목 자체가 너무 크면 서버의 허용된 근거 길이와 출력 설정을 확인합니다.

의미 검색이 켜지지 않음

번들 GPU 모델을 쓸 때는 제공자와 Compose profile을 함께 켭니다.

S3RM_EMBEDDING_PROVIDER=openai-compatible
COMPOSE_PROFILES=domain,embedding

설정 뒤 고정 모델 확인, 런타임 시작, mcp-http 재생성과 연결 검사를 한 번에 실행합니다.

make embedding-up
make verify-embedding

응답의 semantic_status=disabled는 실행 오류가 아니라 제공자가 꺼진 상태입니다. building은 첫 exact index를 만드는 중이며 이때 기존 검색으로 즉시 대체합니다. unavailable이면 아래 런타임 로그와 mcp-http 로그를 확인합니다. 외부 임베딩 API로 자동 전환되지는 않습니다.

effective_mode=hybrid_v1인데 계속 semantic_status=stale이면 전체 Lesson 수가 S3RM_SEARCH_SCAN_LIMIT보다 많은지 확인합니다. 현재 수보다 조금 큰 값으로 올리되 scan limit × 3 × dimensions가 3,000,000개 벡터 구성요소를 넘지 않게 합니다. 설정을 바꾼 뒤 mcp-http를 다시 만듭니다.

GPU 임베딩 서비스가 시작되지 않음

모델 확인 작업과 런타임을 나눠 봅니다.

docker compose --profile embedding ps -a embedding-model-init embedding
docker compose --profile embedding logs --tail=200 embedding-model-init embedding
ls -l /dev/dri/renderD128
vulkaninfo --summary

embedding-model-init의 checksum 오류는 모델 파일, source commit, 크기, SHA-256, generation revision 중 일부가 서로 다른 상태입니다. 값을 임의로 완화하지 말고 같은 artifact 기준으로 모두 맞춥니다. 부분 파일은 다음 실행에서 이어받고, 잘못된 완성 파일은 init 작업이 제거한 뒤 다시 받습니다.

embedding에서 render node 권한 오류가 나면 호스트의 실제 장치와 group ID를 확인해 S3RM_EMBEDDING_DRI_DEVICE, S3RM_RENDER_GID를 맞춥니다. 이 서버의 기본값은 /dev/dri/renderD128, 987입니다. 장치 번호를 바꾼 뒤 컨테이너 안의 별칭은 계속 Vulkan0인지 로그에서 확인합니다. 호스트 포트 8080을 열어 우회하지 마세요.

긴 입력에서 timeout이나 GPU 메모리 오류가 반복되면 먼저 기본 동시성 1과 batch 6을 유지하고 query 길이를 줄입니다. 16K token보다 큰 입력은 같은 generation 계약에서 지원하지 않습니다. 실패 중에도 legacy 검색은 계속 사용할 수 있습니다.

관계 추가가 거부됨

양쪽 Lesson이 이미 있어야 하고 기준 ID 형식이어야 합니다. 자기 자신으로 향하는 관계, 같은 edge 중복, 일곱 개 어휘 밖의 관계 이름은 거부합니다. analogous_to는 양방향으로 조회하지만 한 번만 저장합니다. 양끝을 바꾼 edge를 따로 만들 필요가 없습니다.

supersedes 관계를 추가해도 연결만 기록합니다. 두 Lesson의 호환 상태는 바꾸지 않습니다.

근거 첨부가 거부됨

다음을 확인합니다.

  • 대상 Lesson이 있습니다.
  • citation은 추적 가능한 3~500자의 사람이 읽을 수 있는 참고문헌입니다.
  • kindpaper, code, dataset, log, benchmark, other 중 하나입니다.
  • verification_basisfull_text, official_abstract, partial_source, metadata_only 중 하나를 명시합니다. not_recorded는 이전 자료를 읽는 호환값이며 새 evidence에는 쓸 수 없습니다.
  • 선택 사항인 evidence ID는 해당 Lesson에서 고유하고 영문·숫자·._:-만 사용합니다.
  • 선택 URL은 HTTP(S)이며 사용자 이름과 비밀번호가 없습니다.
  • note는 2000자 이하입니다.

확인 범위는 신뢰도가 아니라 실제 읽은 자료 범위입니다. 서지정보만 확인했다면 metadata_only로 표시하고 기술 내용을 채우지 않습니다. 검증을 없애거나 관리자 자격 증명을 넣지 마세요. 브라우저 양식은 not_recorded를 기본으로 선택하지 않으며 새 근거에 쓸 수 없습니다. high confidence Lesson을 새로 만들 때는 full_text 또는 official_abstract, supports인 초기 검증이 observation, interpretation, reusable_lesson 전부를 claim_fields로 연결해야 합니다.

근거 검증 기록이 거부됨

get_lesson에서 현재 revision, evidence content_digest, 기존 검증 기록을 다시 읽습니다. record_evidence_verification은 다음 조건을 모두 요구합니다.

  • expected_revision_id가 현재 Lesson revision과 같습니다.
  • expected_evidence_digest가 대상 evidence의 현재 content_digest와 같습니다.
  • verification_basis가 이번에 실제로 검사한 범위이고 not_recorded가 아닙니다.
  • source_identity, source_locator, coverage에 실제 식별자·위치·범위가 있으며 문장부호만 입력하지 않았습니다.
  • source_sha256은 실제로 검사한 64자 소문자 SHA-256입니다.
  • outcomesupports, contradicts, inconclusive 중 하나이고 claim_fields는 중복 없이 실제로 확인한 주장만 가리킵니다.

원본 파일을 검사했다면 그 파일 바이트를 해시합니다. 저장·정규화한 텍스트를 검사했다면 그 정확한 텍스트의 UTF-8 바이트를 해시하고, 변환 방법과 보존한 산출물을 source_identitycoverage에 명시합니다. 이전 기록은 수정·삭제할 수 없으므로 판정을 바꾸려면 새 기록을 추가합니다. MCP 호출에서는 서비스가 verified_by와 시각을 만듭니다. 브라우저·raw 저장은 MediaWiki revision actor·timestamp가 권위 출처이며, 하위 actor가 현재 저장 계정과 정확히 같고 시각이 저장 시각 전후 5분 안이며 monotonic인지 훅이 확인합니다. 양식을 5분 넘게 열어 두었다면 작성 내용을 안전하게 복사한 뒤 새로고침하거나, 편집 가능한 시각 필드를 현재 UTC로 갱신한 뒤 저장합니다. 숨겨진 시각은 직접 우회하지 말고 양식을 새로고침합니다.

revision이나 evidence digest가 바뀌었다면 자동 병합하지 말고 현재 내용으로 검증을 다시 계산합니다. 같은 Lesson에서 같은 operation_id를 일반 쓰기, revision, 검증 기록의 다른 종류나 digest에 쓰면 idempotency_conflict입니다. 정확히 같은 종류·요청 digest를 재시도할 때만 같은 ID를 사용합니다.

Lesson revision이 거부됨

revise_lesson은 현재 expected_revision_id, 다른 값이 있는 핵심 필드, 고유한 operation_id, 이유와 이미 붙은 basis evidence 1~8개가 필요합니다.

  • revision_conflict: 다른 변경이 먼저 저장됐습니다. get_lesson을 다시 읽고 변경안을 재계산합니다. 자동 병합하거나 이전 revision으로 반복하지 않습니다.
  • missing_basis_evidence: 근거를 먼저 attach_evidence로 붙이고, 응답의 current_revision_id를 새 기준으로 사용합니다.
  • no_changes: 현재 내용과 같은 요청입니다. 새 Technical Review Lesson을 만들지 않습니다.
  • idempotency_conflict: 같은 operation_id를 다른 요청에 재사용했습니다. 성공 여부가 불분명한 재시도는 원래 요청을 그대로 보내고, 새 작업에는 새 ID를 사용합니다.
  • write_outcome_unknown: 원래 operation_id와 요청을 그대로 재시도합니다. 적용 여부를 확인하기 전에 새 ID로 같은 수정을 보내지 않습니다.

operation replay와 충돌 검사는 해당 Lesson의 최신 revision부터 최대 10000개를 확인합니다.

observation, interpretation, reusable_lesson을 바꾸고 저장 후 결과 confidence가 medium 또는 high인 revision은 사용한 basis evidence의 최신 검증 기록이 full_text 또는 official_abstract, supports이고 바꾸는 필드를 claim_fields로 연결해야 합니다. confidence를 high로 하면 세 필드 모두를 뒷받침해야 합니다. contradictsinconclusive는 이 조건을 충족하지 않습니다. 결과 confidence가 low인 교정은 medium/high에서 낮추거나 low를 유지하는 경우 모두 not_recorded가 아닌 basis evidence로 할 수 있습니다. write_outcome_unknown은 즉시 같은 요청으로 재시도하고, 오래 보관할 작업 ID는 다시 쓰지 않습니다.

같은 주장이나 같은 논문의 보강은 같은 Lesson ID의 revision으로 남깁니다. 출처만 추가할 때는 evidence, 독립된 새 주장이 이전 주장을 실제로 대신할 때만 새 Lesson과 supersedes를 사용합니다.

Lesson 내용 정기 감사가 실패함

먼저 종료 코드와 Lesson 본문을 포함하지 않는 최근 로그를 확인합니다.

systemctl status s3rm-content-audit.service
sudo journalctl -u s3rm-content-audit.service --since '8 days ago'

종료 코드 2는 감사를 완료했지만 확인할 Lesson이 있다는 뜻입니다. 로그의 lesson_id, revision_id, codes로 현재 Lesson과 근거를 확인합니다. 감사 작업은 Lesson을 자동으로 바꾸지 않습니다. verification_timestamp_order_invalid는 같거나 거꾸로 된 검증 시각 때문에 append 순서의 최신 판정을 안전하게 정할 수 없다는 뜻입니다. 감사는 저장 구조·digest·검증 수령 기록의 일관성만 확인하며 외부 자료의 사실 진위를 판정하지 않습니다.

종료 코드 1은 감사를 완료하지 못했다는 뜻입니다. 중앙 MCP와 HTTPS 상태를 먼저 확인합니다. 감사 중 Lesson이 바뀌었다면 현재 revision으로 다시 실행합니다. 목록이 scan_limit에서 잘렸다면 누락된 Lesson이 있으므로 성공으로 처리하지 마세요. 전체 Lesson 수에 맞춰 S3RM_SEARCH_SCAN_LIMIT과 서비스의 --max-lessons를 맞추되 각 값은 최대 1000을 넘기지 않습니다. 1000개가 넘으면 현재 작업으로 전수 감사했다고 표시하지 말고, 한도가 있는 분할 조회를 먼저 설계합니다. mcp-http를 다시 만든 뒤 수동 감사를 확인합니다. operations.md를 참고하세요.

백업이 실패함

완료되지 않은 출력은 진단을 위해 보존하되 복구 지점으로 표시하지 않습니다. 디스크 여유 공간, 대상 권한, 서비스 상태, 마지막 오류를 확인합니다. 이전 정상 백업을 지우지 마세요. 원인을 고친 뒤 새 시각의 디렉터리로 다시 실행합니다.

SHOW MASTER STATUS, embedded coordinate 또는 database-binlogs.tar.gz 오류는 PITR 구간이 없거나 dump와 좌표가 다르다는 뜻입니다. binary log 검증을 빼거나 manifest 좌표를 손으로 고치지 마세요. db의 binlog 설정·볼륨·보존 기간을 고친 뒤 앱 쓰기를 멈춘 새 백업을 만듭니다. repository_untracked=present_not_archived는 untracked 파일이 있었다는 상태만 기록하며 그 이름·내용이나 .env를 백업하지 않은 것이 정상입니다.

복원 스크립트가 백업을 거부함

checksum, manifest, archive, version 검증 실패는 현재 기준 데이터를 보호합니다. --yes를 반복해서 붙이거나, manifest를 고치거나, 다른 시각의 파일을 섞거나, SQL만 직접 가져오지 마세요. 복구 세트의 작업 사본에서 출처나 전송 손상을 조사하고, 가장 최근의 온전하고 호환되는 백업을 사용합니다. operations.md를 참고하세요.

기준 restore는 dump import를 binary log에 다시 쓰지 않고, DB를 멈춘 뒤 기존의 더 새로운 db_binlogs timeline을 비워 새 timeline을 시작합니다. 이 단계에서 중단되면 클라이언트를 계속 멈추고 restore 로그를 보존하세요. backup의 archived binlog는 자동 replay되지 않으므로 목표 시각 PITR이 필요하면 격리 환경에서 별도 검증합니다.

격리 검증이 project 이름, 운영 .env, loopback bind, 보조 checksum 또는 PITR 좌표를 거부했다면 guard를 끄지 마세요. mode 0600의 별도 복원 env와 s3rm-restore-... 이름, 사용하지 않는 loopback 포트를 새로 정합니다. 실패한 격리 볼륨은 진단을 위해 남으므로 다음 시도 전에 출력된 정확한 down --volumes 명령으로 그 project만 정리합니다. 운영 project나 이름이 비슷한 volume을 지우지 마세요.

컨테이너가 OOM 종료되거나 PID 상한에 닿음

health와 함께 실제 종료 원인, 사용량, 재시작 수를 확인합니다.

docker stats --no-stream
docker inspect --format \
  '{{.Name}} oom={{.State.OOMKilled}} exit={{.State.ExitCode}} restarts={{.RestartCount}}' \
  $(docker compose ps -q)
docker compose logs --tail=200 SERVICE

무한 재시작, 큰 요청, job backlog, DB query와 임베딩 batch를 먼저 확인합니다. 단순히 mem_limit·pids_limit를 없애면 호스트 전체 장애 범위가 커집니다. 정상 peak와 호스트 여유를 측정한 뒤 해당 서비스 상한만 조정하고 재발 여부를 봅니다. Docker json-file 로그는 파일당 10MB·5개로 회전하므로 오래된 진단이 필요하면 사전에 접근이 제한된 외부 로그 보관을 설정합니다.

디스크 사용량이 갑자기 늘어남

DB, db_binlogs, upload, build cache, log, 보관 백업을 나눠 확인합니다. 이름이 있는 볼륨을 지우지 마세요. 업로드 근거 기준과 백업 보관 정책을 확인합니다. 다시 만들 수 있는 Docker build cache·image만 서버 담당자의 확인 뒤 정리합니다. 큰 원본 파일은 정해진 object store로 옮기고 Lesson에는 오래 유지되는 인용을 남깁니다.

그래도 문제가 남으면 버전, 정확한 명령, 종료 코드, 민감한 값을 지운 로그 끝부분, docker compose ps -a, 비파괴 재시작 뒤에도 재현되는지를 기록합니다.