diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2eb35cc3..85f6cd60 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,6 +24,9 @@ jobs: - name: Validate skill catalog run: dotnet run --file src/checkskills.cs -- ./docs/SkillCatalog.xml ./docs/SkillCatalog.xsd + - name: Run skill catalog schema regression checks + run: dotnet run --file src/testskills.cs -- ./docs/SkillCatalog.xsd + # Run Catalog Builder tool - name: Run Catalog Builder Tool run: dotnet run --file src/catalogutil.cs -- ./docs/ ./outputs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9941fd91..669b6c50 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,14 @@ dotnet run --file src/catalogutil.cs -- ./docs/ ./outputs/ 대상 서비스의 이용 자체에 로그인이 필요한 것은 무방하지만, 설치 파일을 내려받기 위해 로그인이 필요한 항목은 수록하지 않습니다. +## 환경 변수로 자격 증명을 받는 신규 스킬의 접수 보류 + +사용자 API 키, 토큰이나 클라이언트 시크릿을 환경 변수로 전달받는 신규 스킬의 등록 접수를 현재 보류합니다. 환경 변수의 비밀값은 평문 노출과 자식 프로세스 상속의 위험이 있으며 식탁보의 스킬별 자격 증명 저장 및 전달 기능은 아직 구현하지 않았습니다. [TableCloth #321](https://github.com/yourtablecloth/TableCloth/issues/321)의 대응 기능 개발과 검증을 마친 뒤 접수를 재개합니다. + +사용자 키 없이 독립적으로 동작하는 기능을 제안하는 경우에는 지원 명령, 출력 범위와 추가 의존성을 명시하고 사용자 키를 사용하는 경로와 구분합니다. 기존 카탈로그 항목에 키 없는 독립 기능이 있더라도 신규 자격 증명 전달 경로는 계속 접수를 보류합니다. 시스템 또는 사용자 환경 변수, `.env` 파일이나 채팅 프롬프트로 키를 입력하게 하는 우회 사용법은 안내하지 않습니다. 코드, 카탈로그, 문서와 PR에는 실제 키, 토큰이나 클라이언트 시크릿을 포함하지 않으며 예시는 비밀값이 아닌 자리표시자를 사용합니다. + +스킬의 등록, 제거와 실행 조건은 [원격 스킬 카탈로그 안내](docs/SkillCatalog.README.ko.md)와 [영문 안내](docs/SkillCatalog.README.md)에서 설명합니다. + ## 출처 메타데이터 기재 (권장) 새로운 항목을 제출하실 때에는 그 값을 무엇을 근거로 언제 확인하셨는지 함께 남겨주시기를 권장합니다. 아래 속성은 모두 선택 사항이며, 비워두셔도 검증은 통과합니다. diff --git a/README.md b/README.md index 0e663b58..02434c99 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ - [docs/Catalog.xml](docs/Catalog.xml): 각 기관, 서비스 웹 사이트마다 필요로 하는 추가 소프트웨어들의 목록을 저장하고 관리하는 XML 파일입니다. - XML 파일 편집은 [Microsoft XML Notepad](https://microsoft.github.io/XmlNotepad/)를 사용하시는 것을 권장합니다. -- [docs/SkillCatalog.xml](docs/SkillCatalog.xml): 원격 스킬과 MCP 연동 후보, 고정 원본 및 검토 상태를 관리합니다. 형식과 설치 승인 조건은 [원격 스킬 카탈로그 안내](docs/SkillCatalog.README.ko.md)를 참고하실 수 있습니다. +- [docs/SkillCatalog.xml](docs/SkillCatalog.xml): 설치할 수 있는 원격 스킬과 고정 원본을 관리합니다. 항목 추가, 주석을 통한 제거 및 버전 갱신 방식은 [원격 스킬 카탈로그 안내](docs/SkillCatalog.README.ko.md)를 참고하실 수 있습니다. - [docs/sites.xml](docs/sites.xml): Internet Explorer Mode로 띄우려는 웹 사이트들의 목록을 저장하고 관리하는 XML 파일입니다. (식탁보 0.5.5 버전부터 사용) - XML 파일 편집은 [Microsoft XML Notepad](https://microsoft.github.io/XmlNotepad/)를 사용하시는 것을 권장합니다. - Microsoft Edge 그룹 정책에서 주소를 직접 지정하여 설정을 적용할 수 있습니다. diff --git a/docs/SkillCatalog.README.ko.md b/docs/SkillCatalog.README.ko.md index ef16c871..49dfa622 100644 --- a/docs/SkillCatalog.README.ko.md +++ b/docs/SkillCatalog.README.ko.md @@ -1,47 +1,51 @@ -# 원격 스킬 카탈로그 후보 +# 원격 스킬 카탈로그의 추가와 갱신 [English](SkillCatalog.README.md) -2026년 10월 25일 기준으로 [SkillCatalog.xml](SkillCatalog.xml)에 커뮤니티 스킬과 MCP 연동 후보 12개를 등록했습니다. 모든 항목의 라이선스 검토와 설치 검토는 `pending`을 유지합니다. +2026년 10월 25일 기준으로 [SkillCatalog.xml](SkillCatalog.xml)은 스킬 5개를 활성 항목으로 제공합니다. 초기 목록에서 보류한 후보 5개와 `SKILL.md`가 없는 MCP 후보 2개는 XML 주석으로 보존합니다. -이 문서는 배포 위치, 원본 고정 방식, 검토 상태와 검증 범위를 다룹니다. 항목을 등록해도 식탁보가 설치나 MCP 서버 설정을 수행하지 않습니다. +이 문서는 항목 추가와 제거, 고정 버전 갱신, 실행 선행 조건, 라이선스 및 게시 전 검증을 다룹니다. 활성 `Skill` 요소는 별도 검토 상태 없이 설치 대상으로 표시합니다. -카탈로그의 배포 경로부터 설명하고 검토 상태와 원본 확인 근거를 정리하겠습니다. 마지막으로 설치 승인 전 검증 범위를 짚겠습니다. +항목 편집 방식부터 정리하겠습니다. 이후 버전과 실행 조건을 구분하고 검증 및 배포 절차를 설명하겠습니다. -## 기존 Pages 배포 경로 +## 활성 요소와 주석을 통한 항목 관리 -카탈로그 배포 경로를 살펴보겠습니다. [publish.yml](../.github/workflows/publish.yml)은 카탈로그 빌더가 복사한 `docs` 전체를 GitHub Pages에 게시합니다. 이 절차를 거친 XML은 `https://catalog.yourtablecloth.app/SkillCatalog.xml`에서 제공합니다. 식탁보는 저장소 체크아웃 없이 XML을 읽습니다. +활성 항목의 등록 방식을 살펴보겠습니다. `Skill` 요소에 고유 ID, 실제 스킬 디렉터리, 전체 커밋 SHA와 같은 저장소 및 커밋의 라이선스 URL을 기록합니다. `Install.Mode`는 `upstream`을 사용하며 목록 상태와 검토 상태 속성은 기록하지 않습니다. -## 커밋 고정과 외부 패키지 의존성 +이어서 항목 제거 방식을 짚겠습니다. 해당 `Skill` 요소 전체를 XML 주석으로 감싸고 제거 사유를 짧게 남깁니다. 요소 본문에 연속된 하이픈 두 개가 있으면 `--`로 표기해 블록을 유효한 XML 주석으로 감쌀 수 있습니다. XML 파서는 원래 문자를 읽으며 주석을 설치 목록에 포함하지 않습니다. 원격 목록에서 항목을 제거해도 사용자는 기존 로컬 설치를 계속 관리하거나 제거할 수 있습니다. [현재 XML](SkillCatalog.xml)은 순수 MCP 후보 2개의 블록을 이 방식으로 보존합니다. -원본 고정 범위를 검토하겠습니다. 각 `Source`는 공개 GitHub 저장소, 상대 디렉터리와 전체 커밋 SHA를 기록합니다. `Path="."`는 스킬 디렉터리가 없는 MCP 후보의 저장소 루트를 가리킵니다. +## 환경 변수로 자격 증명을 받는 신규 스킬의 접수 보류 -디렉터리 이름 내부의 점은 `skills/example.v1`처럼 사용할 수 있지만 각 이름의 끝에는 점을 붙일 수 없습니다. 루트 표시 `.`는 계속 허용합니다. 이 규칙으로 마켓플레이스 클라이언트가 거부하는 경로의 게시를 방지합니다. +사용자 API 키, 토큰이나 클라이언트 시크릿을 환경 변수로 전달받는 신규 스킬의 등록 접수를 현재 보류합니다. 환경 변수의 비밀값은 평문 노출과 자식 프로세스 상속의 위험이 있으며 식탁보의 스킬별 자격 증명 저장 및 전달 기능은 아직 구현하지 않았습니다. [TableCloth #321](https://github.com/yourtablecloth/TableCloth/issues/321)의 대응 기능 개발과 검증을 마친 뒤 접수를 재개합니다. [기여 정책](../CONTRIBUTING.md)에도 같은 기준을 적용합니다. -다만 Git 커밋을 고정해도 원본 지침이 실행 중 내려받는 패키지까지 고정하지는 않습니다. NomaDamas 후보는 현재 `npx` CLI로 지침을 읽습니다. 패키지 버전과 실행 환경 검토는 계속 `pending`으로 남깁니다. +사용자 키 없이 독립적으로 동작하는 기능을 제안하는 경우에는 지원 명령, 출력 범위와 추가 의존성을 명시하고 사용자 키를 사용하는 경로와 구분합니다. 기존 카탈로그 항목에 키 없는 독립 기능이 있더라도 신규 자격 증명 전달 경로는 계속 접수를 보류합니다. 시스템 또는 사용자 환경 변수, `.env` 파일이나 채팅 프롬프트로 키를 입력하게 하는 우회 사용법은 안내하지 않습니다. 코드, 카탈로그, 문서와 PR에는 실제 키, 토큰이나 클라이언트 시크릿을 포함하지 않으며 예시는 비밀값이 아닌 자리표시자를 사용합니다. -## 목록 노출과 설치 승인 조건 +## 고정 커밋을 기준으로 한 업데이트 -검토 상태의 역할을 확인하겠습니다. `Status`는 `listed`와 `unlisted`를 허용합니다. `License.ReviewStatus`는 `pending`, `verified`, `rejected`를 허용합니다. `Install.ReviewStatus`는 `pending`, `approved`, `rejected`를 허용합니다. +같은 스킬을 갱신할 때는 `Id`, `Source.Repository`와 `Source.Path`를 유지합니다. `Source.Ref`를 새 전체 커밋 SHA로 바꾸고 `License.Source`도 같은 커밋으로 갱신합니다. 식탁보는 설치한 원본 커밋과 카탈로그 커밋을 비교해 업데이트를 제공합니다. -이어서 설치 조건을 구체적으로 적겠습니다. 식탁보는 목록 상태 `listed`, 라이선스 검토 `verified`, 설치 검토 `approved`, 설치 방식 `upstream`, 런타임 `Codex`와 고정 커밋을 모두 충족하는 항목의 설치를 허용합니다. 라이선스 근거도 같은 저장소와 커밋으로 고정하며 설치 관리자가 해당 파일을 `LICENSE.upstream`으로 보존합니다. `adapter` 항목은 연동 개발 후보로 표시하며 MCP 서버를 설치하거나 설정하지 않습니다. +다만 원본 지침이 실행 중 내려받는 패키지는 자체 버전 동작을 따릅니다. [NomaDamas 원본](https://github.com/NomaDamas/k-skill/tree/0102e5133865f00886e37ac8cf39a49e2c7a3d39)의 스킬은 현재 `npx` CLI로 지침을 읽으므로 Git 커밋 고정만으로 해당 CLI의 배포 버전까지 고정하지는 않습니다. -## 고정 원본의 라이선스 확인 근거 +## 스킬 파일 설치와 실행 선행 조건 -원본 LICENSE에서 [NomaDamas MIT](https://github.com/NomaDamas/k-skill/blob/0102e5133865f00886e37ac8cf39a49e2c7a3d39/LICENSE), [jjlabsio ISC](https://github.com/jjlabsio/korea-stock-mcp/blob/210595eec73a39cd04e2fb175f6e69b1516f0dd4/LICENSE), [kimlawtech Apache-2.0](https://github.com/kimlawtech/korean-jangbu-for/blob/592f6d7c7be138ae578be9ed6769239cc527ddea/LICENSE), [chrisryugj MIT](https://github.com/chrisryugj/korean-law-mcp/blob/4ccedb0dc89fce164493ffc9019b5ad35cb9b782/LICENSE), [mouseco MIT](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/LICENSE)와 [tae0y MIT](https://github.com/tae0y/real-estate-mcp/blob/1119eb1069d1b1bae15f7b38c6c0268fe07995c0/LICENSE)를 확인했습니다. +설치 관리자는 스킬 파일을 복사하고 같은 커밋의 라이선스를 `LICENSE.upstream`으로 보존합니다. 런타임, MCP 서버 등록과 API 키는 별도로 설정합니다. `Network`는 스킬 실행 시 네트워크 의존성을 나타내며 원격 설치에는 항상 다운로드를 사용합니다. -또한 mouseco의 [출처 기록](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/docs/attribution.md)은 이번 후보 4개를 직접 작성한 스킬로 구분합니다. API 응답 데이터와 참조 도구에는 각 원 출처의 조건을 적용합니다. LICENSE 확인만으로 재배포, 보안 또는 호환성 검토를 완료하지 않습니다. +[법령 스킬](https://github.com/chrisryugj/korean-law-mcp/blob/4ccedb0dc89fce164493ffc9019b5ad35cb9b782/skills/korean-law/SKILL.md)은 파일 설치 후 별도 `korean-law` MCP 서버와 법제처 API 인증키를 사용합니다. [장부 스킬](https://github.com/kimlawtech/korean-jangbu-for/blob/592f6d7c7be138ae578be9ed6769239cc527ddea/skills/korean-jangbu-for/SKILL.md)도 진입점 외에 MCP 서버와 하위 스킬을 사용합니다. 각 항목의 `Notes`에서 실행 조건을 확인할 수 있습니다. -## 스키마 검사와 실행 검증 범위 +## 원본 라이선스와 데이터 이용 조건 -[SkillCatalog.xsd](SkillCatalog.xsd)는 메타데이터, 검토 상태와 ID 중복을 검사합니다. 저장소 루트에서 `dotnet run --file src/checkskills.cs -- docs/SkillCatalog.xml docs/SkillCatalog.xsd`로 실행할 수 있습니다. Pages 워크플로도 게시 전에 같은 검사를 실행합니다. 설치 검토에서는 전체 파일과 라이선스 고지, 보조 스크립트 경로, 패키지 버전, 식탁보 전용 Codex 환경 호환성, API 키와 문서 접근 범위를 함께 다룹니다. +활성 항목은 [NomaDamas MIT](https://github.com/NomaDamas/k-skill/blob/0102e5133865f00886e37ac8cf39a49e2c7a3d39/LICENSE), [kimlawtech Apache-2.0](https://github.com/kimlawtech/korean-jangbu-for/blob/592f6d7c7be138ae578be9ed6769239cc527ddea/LICENSE), [chrisryugj MIT](https://github.com/chrisryugj/korean-law-mcp/blob/4ccedb0dc89fce164493ffc9019b5ad35cb9b782/LICENSE)와 [mouseco MIT](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/LICENSE)를 원본 고정 커밋에서 참조합니다. -경로 회귀 검사는 `dotnet run --file src/testskills.cs -- docs/SkillCatalog.xsd`로 실행할 수 있습니다. [PR 워크플로](../.github/workflows/skill-catalog.yml)가 실제 스키마를 대상으로 카탈로그 검증과 회귀 검사를 함께 실행합니다. +또한 mouseco의 [출처 기록](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/docs/attribution.md)은 후보 4개를 직접 작성한 스킬로 구분합니다. API 응답 데이터와 참조 도구에는 각 원 출처의 조건을 적용합니다. 카탈로그는 자격증명을 저장하거나 원본 코드를 실행하지 않습니다. -카탈로그는 자격증명을 저장하거나 원본 코드를 실행하지 않습니다. `Network`는 스킬 실행 시 네트워크 의존성을 표시합니다. 원격 스킬 다운로드에는 별도로 네트워크를 사용합니다. Windows Sandbox 내부 스킬 실행은 별도 구현과 검증을 전제로 합니다. +## 스키마 검사와 기존 Pages 배포 -## 후보 등록 이후의 검토 범위 +[SkillCatalog.xsd](SkillCatalog.xsd)는 고정 커밋과 라이선스 URL을 필수로 요구하고 활성 항목을 최대 256개로 제한합니다. [checkskills.cs](../src/checkskills.cs)는 XML 크기를 512 KiB로 제한하며 라이선스 URL의 저장소 및 커밋 일치와 경로를 검사합니다. 디렉터리 이름 내부의 점은 허용하지만 끝점은 거부합니다. 루트 표시 `.`는 계속 허용합니다. -여기까지 정리하면 카탈로그는 원본과 채택 조건을 고정해 검토 후보를 제공합니다. 현재 12개 후보 중 설치 승인을 마친 항목은 없습니다. +저장소 루트에서 `dotnet run --file src/checkskills.cs -- docs/SkillCatalog.xml docs/SkillCatalog.xsd`와 `dotnet run --file src/testskills.cs -- docs/SkillCatalog.xsd`를 실행할 수 있습니다. [PR 워크플로](../.github/workflows/skill-catalog.yml)와 [Pages 워크플로](../.github/workflows/publish.yml)가 두 검사를 함께 실행합니다. 기존 카탈로그 빌더는 `docs`를 복사해 `https://catalog.yourtablecloth.app/SkillCatalog.xml`에 게시합니다. -우선 각 후보의 라이선스 고지와 실행 환경을 검증할 수 있습니다. MCP 어댑터 구현, 민감정보 통제와 외부 패키지 고정은 후속 개발 범위로 남깁니다. 검토 상태와 `Notes`를 함께 읽으면 각 후보의 진행 범위를 판단할 수 있습니다. +## 카탈로그와 로컬 설치의 수명 주기 + +여기까지 정리하면 활성 XML 요소가 설치 목록을 구성하고 고정 커밋의 변경이 업데이트를 구분합니다. XML 주석은 원본 기록을 남기면서 항목을 목록에서 제거합니다. + +원격 항목 편집은 앞으로 제공할 설치와 업데이트에 영향을 줍니다. 사용자는 기존 로컬 설치를 계속 관리할 수 있습니다. MCP 서버와 API 키 같은 실행 조건은 각 스킬의 `Notes`와 원본 문서에 따라 설정할 수 있습니다. diff --git a/docs/SkillCatalog.README.md b/docs/SkillCatalog.README.md index 4590e2d5..00f91938 100644 --- a/docs/SkillCatalog.README.md +++ b/docs/SkillCatalog.README.md @@ -1,31 +1,35 @@ -# Remote skill catalog candidates +# Remote skill catalog lifecycle [한국어](SkillCatalog.README.ko.md) -## Candidate scope +## Installable catalog entries -As of October 25, 2026, [SkillCatalog.xml](SkillCatalog.xml) lists twelve community skill and MCP integration candidates. Every license review and installation review remains pending. Listing an entry does not enable installation or endorse its upstream service. +As of October 25, 2026, [SkillCatalog.xml](SkillCatalog.xml) contains five active skill entries. Each active `Skill` element offers an upstream skill for installation. There are no listing or review states. The catalog retains seven other candidates as XML comments: five deferred from the initial catalog and two MCP candidates whose pinned repositories provide no `SKILL.md`. -## Catalog location +## Adding and removing a skill -The catalog is published at `https://catalog.yourtablecloth.app/SkillCatalog.xml` after the repository's normal Pages deployment. [publish.yml](../.github/workflows/publish.yml) copies the complete `docs` directory through the catalog builder. The application reads this XML without checking out the repository. +Add an active `Skill` element with a stable ID, an existing upstream skill directory, a full commit SHA and a license URL pinned to that same repository and commit. `Install.Mode` is `upstream`. To remove a skill from the remote catalog, comment out its entire `Skill` element and record a short reason outside or inside that comment. Encode consecutive hyphens in element text as `--` so the raw block can become a valid XML comment; XML readers preserve the original text. XML comments never become marketplace entries. A catalog removal leaves existing local installations available to manage and remove. -## Immutable upstream references +## Deferral of new skills that receive credentials through environment variables -Each `Source` records the public GitHub repository, a relative directory and a full commit SHA. `Path="."` denotes the repository root for MCP candidates without a skill directory. A pinned Git commit does not pin packages subsequently downloaded by an upstream instruction. The NomaDamas candidates currently load instructions through an `npx` CLI; package pinning and runtime review remain open. +New submissions of skills that receive user API keys, tokens or client secrets through environment variables are currently deferred. Secrets in environment variables risk plaintext exposure and child-process inheritance, and TableCloth has not yet implemented per-skill credential storage and delivery. Submissions will resume after the corresponding features in [TableCloth #321](https://github.com/yourtablecloth/TableCloth/issues/321) are implemented and verified. See the [contribution policy](../CONTRIBUTING.md). -Directory segments can contain internal dots, such as `skills/example.v1`, but cannot end with a dot. The single root marker `.` remains valid. This prevents publishing paths that the marketplace client rejects. +For functionality that works independently without a user key, specify the supported commands, output scope and additional dependencies separately from credential-dependent paths. New credential-delivery paths remain deferred even when an existing catalog entry also provides independently usable, keyless functionality. Do not suggest system or user environment variables, `.env` files or chat prompts as workarounds for the deferral. Never include real keys, tokens or client secrets in code, catalog entries, documentation or pull requests; use nonsecret placeholders in examples. -## Independent review states +## Pinned versions and updates -`Status` accepts `listed` or `unlisted`. `License.ReviewStatus` accepts `pending`, `verified` or `rejected`. `Install.ReviewStatus` accepts `pending`, `approved` or `rejected`. Installation requires `listed`, a verified license, an approved installation review, `Mode="upstream"`, `Runtime="Codex"`, an immutable source ref and a license source pinned to the same repository and commit. The installer preserves that file as `LICENSE.upstream`. Other entries remain visible for review. `Mode="adapter"` records integration work; it does not install or configure an MCP server. +Keep `Id`, `Source.Repository` and `Source.Path` unchanged when updating the same skill. Change `Source.Ref` to the new full commit SHA and change `License.Source` to the matching commit. The application compares the installed source commit with the current catalog commit to offer an update. A pinned Git commit does not pin packages downloaded later by upstream instructions. The NomaDamas skills currently load instructions through an `npx` CLI, so those packages have their own version behavior. -## Upstream license evidence +## Skill files and execution prerequisites -The license identifiers come from the pinned source files: [NomaDamas MIT](https://github.com/NomaDamas/k-skill/blob/0102e5133865f00886e37ac8cf39a49e2c7a3d39/LICENSE), [jjlabsio ISC](https://github.com/jjlabsio/korea-stock-mcp/blob/210595eec73a39cd04e2fb175f6e69b1516f0dd4/LICENSE), [kimlawtech Apache-2.0](https://github.com/kimlawtech/korean-jangbu-for/blob/592f6d7c7be138ae578be9ed6769239cc527ddea/LICENSE), [chrisryugj MIT](https://github.com/chrisryugj/korean-law-mcp/blob/4ccedb0dc89fce164493ffc9019b5ad35cb9b782/LICENSE), [mouseco MIT](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/LICENSE) and [tae0y MIT](https://github.com/tae0y/real-estate-mcp/blob/1119eb1069d1b1bae15f7b38c6c0268fe07995c0/LICENSE). The four mouseco candidates are identified as original contributions in the pinned [attribution notes](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/docs/attribution.md). Their API data and referenced tools retain separate terms. These observations do not complete redistribution, security or compatibility review. +Installing a skill copies its files and preserves the pinned license as `LICENSE.upstream`. It does not install runtimes, register MCP servers or configure API keys. The [Korean law skill](https://github.com/chrisryugj/korean-law-mcp/blob/4ccedb0dc89fce164493ffc9019b5ad35cb9b782/skills/korean-law/SKILL.md) is installable as a skill file and requires a separately configured `korean-law` MCP server. The [jangbu entry](https://github.com/kimlawtech/korean-jangbu-for/blob/592f6d7c7be138ae578be9ed6769239cc527ddea/skills/korean-jangbu-for/SKILL.md) also depends on a separate MCP server and sibling skills. Each entry's `Notes` lists the relevant prerequisites. `Network` describes execution; remote installation always downloads files. -## Validation and qualification +## License and source boundaries -[SkillCatalog.xsd](SkillCatalog.xsd) validates the metadata, review values and unique IDs. Run `dotnet run --file src/checkskills.cs -- docs/SkillCatalog.xml docs/SkillCatalog.xsd` from the repository root. The Pages workflow runs the same check before publishing. Installation review also covers the complete file set and license notices, helper paths, package versions, compatibility with TableCloth's dedicated Codex environment, API keys and document access. Executing skills inside Windows Sandbox would require separate implementation and qualification. The catalog stores no credentials and runs no upstream code. `Network` describes skill execution; installing any remote skill still requires a download. The initial catalog intentionally offers no installable entries. +The pinned licenses identify [NomaDamas MIT](https://github.com/NomaDamas/k-skill/blob/0102e5133865f00886e37ac8cf39a49e2c7a3d39/LICENSE), [kimlawtech Apache-2.0](https://github.com/kimlawtech/korean-jangbu-for/blob/592f6d7c7be138ae578be9ed6769239cc527ddea/LICENSE), [chrisryugj MIT](https://github.com/chrisryugj/korean-law-mcp/blob/4ccedb0dc89fce164493ffc9019b5ad35cb9b782/LICENSE) and [mouseco MIT](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/LICENSE). All four mouseco candidates have original-contribution records in the pinned [attribution notes](https://github.com/mouseco/k-gov-skills/blob/a55816b9b44e8391576204a095bf6337940d80f3/docs/attribution.md). API response data and referenced tools retain their own terms. The catalog stores no credentials and executes no upstream code. -Run `dotnet run --file src/testskills.cs -- docs/SkillCatalog.xsd` to check path regressions against the actual schema. The [PR workflow](../.github/workflows/skill-catalog.yml) runs this check alongside catalog validation. +## Validation and publication + +[SkillCatalog.xsd](SkillCatalog.xsd) requires immutable references and license sources, limits the catalog to 256 active entries and rejects review attributes. [checkskills.cs](../src/checkskills.cs) also limits the XML to 512 KiB, checks source paths and requires each license URL to match the source repository and commit. Directory segments may contain internal dots but cannot end with a dot; the root marker `.` remains valid. The application separately bounds downloads, rejects symlinks, submodules and nested `SKILL.md` files, and verifies Git blob identities before installation. + +Run `dotnet run --file src/checkskills.cs -- docs/SkillCatalog.xml docs/SkillCatalog.xsd` and `dotnet run --file src/testskills.cs -- docs/SkillCatalog.xsd` from the repository root. The [PR workflow](../.github/workflows/skill-catalog.yml) and [Pages workflow](../.github/workflows/publish.yml) run both checks. The existing catalog builder copies `docs` to the Pages output, publishing the XML at `https://catalog.yourtablecloth.app/SkillCatalog.xml` without requiring a client-side Git checkout. diff --git a/docs/SkillCatalog.xml b/docs/SkillCatalog.xml index 3a35a7ec..41d6ea56 100644 --- a/docs/SkillCatalog.xml +++ b/docs/SkillCatalog.xml @@ -1,112 +1,126 @@ - - + + + 한국은행 ECOS의 기준금리, 물가, 환율과 통화량 통계를 조회합니다. - + - - 우선 검토합니다. 원본은 ECOS에 직접 연결하며 sample 키 또는 사용자 KSKILL_BOK_ECOS_API_KEY를 사용합니다. Node.js 18 이상, npx CLI와 Python 3.9 이상 의존성을 검토합니다. CLI 버전과 조회 범위를 고정하고 식탁보 전용 Codex 환경 호환성을 검증합니다. + + ECOS에 직접 연결하며 sample 키 또는 사용자 KSKILL_BOK_ECOS_API_KEY를 사용합니다. sample 키는 호출당 최대 10행으로 제한합니다. 실행에는 Node.js 18 이상, npx CLI와 Python 3.9 이상을 사용합니다. - + + 은행 CSV, 카드 명세서와 영수증을 활용한 장부 작성을 지원합니다. - + - - 조건부로 검토합니다. 원본 진입점은 jangbu-mcp와 하위 스킬에 의존합니다. 단일 디렉터리 설치만으로 전체 기능을 제공하지 않습니다. MCP와 하위 스킬 배치, Windows 호환성, 민감정보 마스킹, 로컬 파일 접근과 CODEF 자격증명 처리를 검증합니다. + + 설치 관리자는 진입점 SKILL.md를 복사합니다. 실행에는 jangbu-mcp 서버와 별도 하위 스킬, OCR 환경을 설정합니다. 은행 CSV, 카드 명세서와 CODEF 자격증명은 사용자가 허용한 범위에서 다룹니다. - + + 법령, 판례와 조례를 조회하고 인용 내용을 검증합니다. - + - - 우선 검토합니다. 원본은 skills/korean-law/SKILL.md를 제공하지만 korean-law MCP 서버 연결을 전제로 합니다. Codex MCP 등록과 서버 실행, API 자격증명 및 인용 검증 도구 연동을 구현하고 검증합니다. + + 원본의 skills/korean-law/SKILL.md를 설치합니다. 법령 도구 실행에는 korean-law MCP 서버 연결과 사용자 법제처 Open API 인증키 OC를 별도로 설정합니다. 로컬 MCP 서버 패키지는 Node.js 20.19.0 이상을 사용합니다. 스킬 설치가 MCP 서버 설치나 등록을 수행하지는 않습니다. - + KOSIS의 인구, 가구, 고용과 물가 통계를 검색하고 조회합니다. - + - - 조건부로 검토합니다. 원본의 일반 조회는 외부 hosted proxy를 기본 사용합니다. 식탁보에서는 사용자 KSKILL_KOSIS_API_KEY와 --direct를 사용하는 경로로 제한하는 방안을 검토합니다. Node.js 18 이상, npx CLI, Python 3.9 이상과 식탁보 전용 Codex 환경 호환성을 검증합니다. + + 기본 일반 조회는 외부 hosted proxy를 사용합니다. 사용자 KSKILL_KOSIS_API_KEY와 --direct 옵션으로 KOSIS에 직접 연결할 수 있습니다. 실행에는 Node.js 18 이상, npx CLI와 Python 3.9 이상을 사용합니다. - + + + + + 공공기관 보고서와 내부 결재문서의 구성 및 초안 작성을 지원합니다. - + - - 우선 검토합니다. 원본 문서와 references를 함께 보존하고 docs/attribution.md를 검토합니다. 사용자가 선택한 로컬 문서로 접근 범위를 제한하며 문서 외부 전송, 생성 문안과 기존 사용자 지침의 충돌 여부를 검증합니다. + + SKILL.md와 references 문서를 함께 설치합니다. 사용자가 선택한 로컬 문서와 자료를 바탕으로 보고서 초안을 작성합니다. 문서 접근 범위와 외부 전송은 별도 실행 환경 설정을 따릅니다. diff --git a/docs/SkillCatalog.xsd b/docs/SkillCatalog.xsd index f960d144..c45a3ad5 100644 --- a/docs/SkillCatalog.xsd +++ b/docs/SkillCatalog.xsd @@ -27,20 +27,11 @@ - - - - - - - - - - + @@ -53,7 +44,7 @@ - + @@ -65,8 +56,7 @@ - - + @@ -78,7 +68,6 @@ - @@ -86,7 +75,6 @@ - diff --git a/src/checkskills.cs b/src/checkskills.cs index 5ac69415..75bc9af4 100644 --- a/src/checkskills.cs +++ b/src/checkskills.cs @@ -2,6 +2,7 @@ #:property PublishAot=false using System.Xml; +using System.Xml.Linq; using System.Xml.Schema; if (args.Length > 2) @@ -16,11 +17,14 @@ try { + if (new FileInfo(catalogPath).Length > 512 * 1024) + throw new XmlException("The skill catalog exceeds the 512 KiB limit."); var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null, ValidationType = ValidationType.Schema, + MaxCharactersInDocument = 512 * 1024, ValidationFlags = XmlSchemaValidationFlags.ProcessIdentityConstraints | XmlSchemaValidationFlags.ReportValidationWarnings, }; @@ -39,7 +43,21 @@ settings.Schemas.XmlResolver = null; settings.Schemas.Add(null, schemaReader); using var catalogReader = XmlReader.Create(catalogPath, settings); - while (catalogReader.Read()) { } + var catalog = XDocument.Load(catalogReader); + if (errors != 0) return 1; + var ids = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var skill in catalog.Root!.Elements("Skill")) + { + var id = (string?)skill.Attribute("Id") ?? ""; + if (!ids.Add(id)) throw new XmlException("Skill IDs must be unique without regard to case."); + var source = skill.Element("Source")!; + var prefix = (string?)source.Attribute("Repository") + "/blob/" + (string?)source.Attribute("Ref") + "/"; + var license = (string?)skill.Element("License")?.Attribute("Source") ?? ""; + if (!license.StartsWith(prefix, StringComparison.Ordinal) || !SafePath(license[prefix.Length..])) + throw new XmlException($"Skill '{id}' must pin its license to the same repository and commit."); + if (!SafePath((string?)source.Attribute("Path") ?? "", allowRoot: true)) + throw new XmlException($"Skill '{id}' has an unsupported repository path."); + } } catch (Exception e) when (e is XmlException or XmlSchemaException or IOException or UnauthorizedAccessException) { @@ -52,3 +70,18 @@ Console.WriteLine("Skill catalog schema validation passed."); return 0; + +static bool SafePath(string path, bool allowRoot = false) +{ + if (allowRoot && path == ".") return true; + return path.Length is > 0 and <= 160 && path.Split('/').All(segment => + { + var stem = segment.Split('.')[0]; + var device = stem.Equals("CON", StringComparison.OrdinalIgnoreCase) || stem.Equals("PRN", StringComparison.OrdinalIgnoreCase) || + stem.Equals("AUX", StringComparison.OrdinalIgnoreCase) || stem.Equals("NUL", StringComparison.OrdinalIgnoreCase) || + stem.Length == 4 && (stem.StartsWith("COM", StringComparison.OrdinalIgnoreCase) || stem.StartsWith("LPT", StringComparison.OrdinalIgnoreCase)) && + char.IsAsciiDigit(stem[3]); + return segment.Length is > 0 and <= 100 && segment is not ("." or "..") && !segment.EndsWith('.') && !device && + segment.All(c => char.IsAsciiLetterOrDigit(c) || c is '-' or '_' or '.'); + }); +} diff --git a/src/testskills.cs b/src/testskills.cs index 6e07f85e..a73b6560 100644 --- a/src/testskills.cs +++ b/src/testskills.cs @@ -5,16 +5,18 @@ using System.Xml.Linq; using System.Xml.Schema; -if (args.Length > 1) +if (args.Length > 2) { - Console.Error.WriteLine("Usage: dotnet run --file src/testskills.cs -- [schema.xsd]"); + Console.Error.WriteLine("Usage: dotnet run --file src/testskills.cs -- [schema.xsd] [catalog.xml]"); return 2; } try { var schemas = new XmlSchemaSet { XmlResolver = null }; - using var schemaReader = XmlReader.Create(args.Length == 1 ? args[0] : "docs/SkillCatalog.xsd", + var schemaPath = args.Length > 0 ? args[0] : "docs/SkillCatalog.xsd"; + var catalogPath = args.Length > 1 ? args[1] : Path.Combine(Path.GetDirectoryName(Path.GetFullPath(schemaPath))!, "SkillCatalog.xml"); + using var schemaReader = XmlReader.Create(schemaPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }); schemas.Add(null, schemaReader); schemas.Compile(); @@ -32,24 +34,54 @@ var failures = 0; foreach (var (path, expected) in cases) { - var fixture = XElement.Parse(""" - - - Path validation fixture. - - - - - - - """); + var fixture = Fixture(); fixture.Element("Skill")!.Element("Source")!.SetAttributeValue("Path", path); if (Accepts(fixture, schemas) == expected) continue; failures++; Console.Error.WriteLine($"Path '{path}': expected {(expected ? "acceptance" : "rejection")}."); } + var lifecycle = new (string Name, Action Change, bool Valid)[] + { + ("active entry without review attributes", _ => { }, true), + ("removed entry as XML comment", xml => xml.Element("Skill")!.ReplaceWith(new XComment(xml.Element("Skill")!.ToString())), true), + ("legacy listing status", xml => xml.Element("Skill")!.SetAttributeValue("Status", "listed"), false), + ("legacy license review", xml => xml.Element("Skill")!.Element("License")!.SetAttributeValue("ReviewStatus", "pending"), false), + ("legacy installation review", xml => xml.Element("Skill")!.Element("Install")!.SetAttributeValue("ReviewStatus", "pending"), false), + ("MCP adapter without a skill", xml => xml.Element("Skill")!.Element("Install")!.SetAttributeValue("Mode", "adapter"), false), + ("missing immutable source", xml => xml.Element("Skill")!.Element("Source")!.Attribute("Ref")!.Remove(), false), + ("mutable source branch", xml => xml.Element("Skill")!.Element("Source")!.SetAttributeValue("Ref", "main"), false), + ("missing license source", xml => xml.Element("Skill")!.Element("License")!.Attribute("Source")!.Remove(), false), + ("duplicate skill ID", xml => xml.Add(new XElement(xml.Element("Skill")!)), false), + ("updated pinned source", xml => + { + var version = new string('2', 40); + xml.Element("Skill")!.Element("Source")!.SetAttributeValue("Ref", version); + xml.Element("Skill")!.Element("License")!.SetAttributeValue("Source", "https://github.com/fixture/skills/blob/" + version + "/LICENSE"); + }, true), + }; + foreach (var (name, change, expected) in lifecycle) + { + var fixture = Fixture(); + change(fixture); + if (Accepts(fixture, schemas) == expected) continue; + failures++; + Console.Error.WriteLine($"Lifecycle '{name}': expected {(expected ? "acceptance" : "rejection")}."); + } + var catalogText = File.ReadAllText(catalogPath); + if (!AcceptsXml(catalogText, schemas, out var originalCount)) + throw new XmlException("The catalog fixture must pass schema validation before removal checks."); + var removals = 0; + foreach (var (id, start, length) in RawSkillSpans(catalogText)) + { + removals++; + var commented = catalogText[..start] + "" + catalogText[(start + length)..]; + if (AcceptsXml(commented, schemas, out var remaining) && remaining == originalCount - 1) continue; + failures++; + Console.Error.WriteLine($"Removal '{id}': commenting out the raw Skill block must remove exactly one valid entry."); + } + if (removals != originalCount) throw new XmlException("Could not locate every active raw Skill block."); if (failures != 0) return 1; - Console.WriteLine($"All {cases.Length} repository-path schema regression checks passed."); + Console.WriteLine($"All {cases.Length + lifecycle.Length + removals} path, lifecycle and raw-block removal checks passed."); return 0; } catch (Exception e) when (e is XmlException or XmlSchemaException or IOException or UnauthorizedAccessException) @@ -58,8 +90,23 @@ return 1; } -static bool Accepts(XElement fixture, XmlSchemaSet schemas) +static XElement Fixture() => XElement.Parse(""" + + + Path and lifecycle validation fixture. + + + + + + + """); + +static bool Accepts(XElement fixture, XmlSchemaSet schemas) => AcceptsXml(fixture.ToString(), schemas, out _); + +static bool AcceptsXml(string fixture, XmlSchemaSet schemas, out int activeCount) { + activeCount = 0; var valid = true; var settings = new XmlReaderSettings { @@ -71,7 +118,49 @@ static bool Accepts(XElement fixture, XmlSchemaSet schemas) Schemas = schemas, }; settings.ValidationEventHandler += (_, _) => valid = false; - using var reader = XmlReader.Create(new StringReader(fixture.ToString()), settings); - while (reader.Read()) { } - return valid; + try + { + using var reader = XmlReader.Create(new StringReader(fixture), settings); + var catalog = XDocument.Load(reader); + activeCount = catalog.Root?.Elements("Skill").Count() ?? 0; + return valid; + } + catch (Exception e) when (e is XmlException or XmlSchemaException) { return false; } +} + +static IReadOnlyList<(string Id, int Start, int Length)> RawSkillSpans(string xml) +{ + var lines = new List { 0 }; + for (var index = 0; index < xml.Length; index++) + { + if (xml[index] == '\r') + { + if (index + 1 < xml.Length && xml[index + 1] == '\n') index++; + lines.Add(index + 1); + } + else if (xml[index] == '\n') lines.Add(index + 1); + } + var spans = new List<(string, int, int)>(); + using var reader = XmlReader.Create(new StringReader(xml), + new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }); + var start = -1; + var id = ""; + while (reader.Read()) + { + if (reader.Depth != 1 || reader.Name != "Skill") continue; + var line = (IXmlLineInfo)reader; + var position = lines[line.LineNumber - 1] + line.LinePosition - 1; + var tagStart = xml.LastIndexOf('<', position); + if (reader.NodeType == XmlNodeType.Element) + { + start = tagStart; + id = reader.GetAttribute("Id") ?? ""; + } + else if (reader.NodeType == XmlNodeType.EndElement && start >= 0) + { + spans.Add((id, start, xml.IndexOf('>', tagStart) + 1 - start)); + start = -1; + } + } + return spans; }