diff --git a/SPEC.md b/SPEC.md index 2910ba7..3b93b02 100644 --- a/SPEC.md +++ b/SPEC.md @@ -71,6 +71,7 @@ id 문자셋 방어: `.wsb` 주입 전 `^[A-Za-z0-9._-]+$` 만 허용. - 템플릿: `shared/wsb-template.xml` 로드 후 `__SITEIDS__` 를 사이트 사전선택 구문으로 치환. - 사이트 주입 채널: 환경변수 `TABLECLOTH_SITE_IDS`(PARAMETERIZED_WSB_SPEC.md §0.5). 치환 구문 예: ` $env:TABLECLOTH_SITE_IDS = '' '';` (id 없으면 빈 문자열). +- 다중 id honor: 여러 service id가 오면 **모두** 공백으로 join 해 `TABLECLOTH_SITE_IDS`에 **한 번만** 대입한다(처음 하나로 자르지 않는다). 이 계약은 `node/test/conformance.mjs`의 다중 id 회귀 검사로 고정한다. 실행 배관(러너/준비 스크립트/Spork)은 이 값을 받아 지정한 모든 사이트를 각각 **한 번씩** 연다(다운스트림 계약은 [TableCloth](https://github.com/yourtablecloth/TableCloth); 관련 이슈 [#2](https://github.com/yourtablecloth/TableClothMcp/issues/2)). - 실행 자산은 전부 GitHub 릴리스 공개 URL(`tablecloth-prepare.ps1` 등). 무설치 Express 레인. - `securityNote`: 응답에 항상 포함. 문구는 `strings.json` 의 `sandbox.securityNote`. - 알려진 이슈/하드닝: 명령이 원격 스크립트 실행 형태라 오탐될 수 있음 → [#1](https://github.com/yourtablecloth/TableClothMcp/issues/1). diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 2b1ea0b..8468e78 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -63,3 +63,12 @@ MCP 서버는 클라이언트가 시작할 때 프로세스로 떠서 실행되 - 원인: 그 `.wsb`의 ``는 공식 준비 스크립트를 원격에서 받아 실행하는 형태입니다(`ExecutionPolicy Bypass` + 원격 스크립트 `iex` + DNS 폴백). 이 조합이 일반적인 악성 다운로더와 겉모습이 같아 모델의 안전 검토가 발동합니다. 명령이 매우 긴 단일 라인이라 검토 도중 응답이 끊기기도 합니다. - 실제 동작: 이 스크립트는 공식 GitHub 릴리스에서 HTTPS로 받아 **일회용 샌드박스 내부에서만** 실행됩니다. 호스트 시스템에는 접근하거나 영향을 주지 않고, 샌드박스를 닫으면 모두 사라집니다. 자격증명이나 로그인은 다루지 않습니다. 따라서 어시스턴트의 실행 전 확인은 정상 동작으로 보시면 됩니다. 참고로 `generate_wsb`와 `launch_sandbox` 응답에는 이 동작을 명시하는 `securityNote` 필드가 포함됩니다. - 개선 방향: 정본 `.wsb`의 실행 명령을 최소화(로직을 준비 스크립트로 이관)하고, 다운로드 스크립트를 해시로 검증하는 방식으로 바꿔 오탐과 검토 끊김을 줄이는 작업을 추적 중입니다([#1](https://github.com/yourtablecloth/TableClothMcp/issues/1)). + +### 여러 은행/사이트를 지정해도 첫 번째만 열리거나, 사이트가 중복으로 열림 + +- 증상: `국민은행과 우리은행을 실행해줘`처럼 여러 사이트를 요청했는데 처음 하나만 열립니다. 또한 샌드박스가 사이트를 커맨드라인(사전선택) 모드로 열 때 같은 사이트가 두 번 열리기도 합니다([#2](https://github.com/yourtablecloth/TableClothMcp/issues/2)). +- MCP 서버의 역할(정상): `launch_sandbox`/`generate_wsb`는 넘어온 **모든** service id를 유지해 `.wsb`의 `TABLECLOTH_SITE_IDS` 환경변수 채널에 공백으로 join 해 한 번에 전달합니다(SPEC.md §7). 즉 다중 인자 honor는 MCP 경계에서 유지되며, 이는 `node/test/conformance.mjs`의 다중 id 회귀 테스트로 고정됩니다. +- 실제 원인(다운스트림): 사이트 사전선택을 실제로 실행하는 실행 배관은 GitHub 릴리스 자산인 [TableCloth](https://github.com/yourtablecloth/TableCloth) 의 무설치 러너입니다. `.wsb` 가 넘긴 여러 id 가 이 지점에서 소실됩니다. + - 첫 번째만 열림: `tools/no-install/tablecloth-prepare.ps1` 이 `Start-Process $launcher -ArgumentList @('--site-ids', $SiteIds)` 로 넘기면서 공백으로 join 된 값이 개별 인자로 흩어지고, `Spork.Bootstrapper/BootstrapOptions.cs` 의 `--site-ids` 파서가 **첫 토큰만** 값으로 취합니다. 준비 스크립트가 값을 인용(따옴표)해 넘기거나 부트스트래퍼가 나머지 토큰을 모두 수집하도록 고쳐야 합니다. + - 중복 열림: Spork 커맨드라인 모드에서 사이트를 여는 경로가 이중으로 발화되던 문제로, TableCloth `main` 에서 단계 구성(`StepsComposer`)의 `OpenWebSiteStep` 제거로 정리되었습니다(릴리스 반영 대기). +- 정리: 이 저장소(MCP)에서 고칠 부분은 없습니다. 러너/준비 스크립트 수정과 다음 릴리스 반영으로 해소됩니다. diff --git a/node/test/conformance.mjs b/node/test/conformance.mjs index 87155b0..48146c2 100644 --- a/node/test/conformance.mjs +++ b/node/test/conformance.mjs @@ -49,6 +49,8 @@ async function collect(cmd, args) { [4, "tools/call", { name: "list_categories", arguments: {} }], [5, "tools/call", { name: "get_service", arguments: { id: "Hometax" } }], [6, "tools/call", { name: "list_companions", arguments: {} }], + // 여러 사이트를 한 번에 지정했을 때 모든 id가 .wsb에 유지되는지(다중 인자 honor, 이슈 #2) 검증한다. + [7, "tools/call", { name: "generate_wsb", arguments: { serviceIds: ["Hometax", "ShinhanBank"] } }], ]; for (const [id, method, params] of reqs) c.send({ jsonrpc: "2.0", id, method, params }); const body = async (id) => JSON.parse((await c.wait(id)).result.content[0].text); @@ -60,6 +62,7 @@ async function collect(cmd, args) { cats: await body(4), getSvc: await body(5), companions: await body(6), + genWsbMulti: await body(7), }; c.close(); return out; @@ -116,6 +119,18 @@ check("securityNote==shared (both)", net.genWsb.securityNote === shared.sandbox. check("usage==shared (both)", net.genWsb.usage === shared.tools.generate_wsb.usage && node.genWsb.usage === shared.tools.generate_wsb.usage); check("siteIds equal", canon(net.genWsb.siteIds) === canon(node.genWsb.siteIds)); +// 다중 사이트 id honor (이슈 #2): 여러 개를 넘겨도 처음 하나로 잘리지 않고 모두 .wsb에 실려야 하며, +// TABLECLOTH_SITE_IDS 환경변수 채널에 공백으로 join 되어 한 번만 대입돼야 한다(SPEC.md §7). +const multiIds = ["Hometax", "ShinhanBank"]; +const assignmentCount = (wsb) => (wsb.match(/TABLECLOTH_SITE_IDS\s*=/g) ?? []).length; +for (const [impl, r] of [["net", net.genWsbMulti], ["node", node.genWsbMulti]]) { + check(`multi-id siteIds honored (${impl})`, canon(r.siteIds) === canon(multiIds), `siteIds=${JSON.stringify(r.siteIds)}`); + check(`multi-id all ids present in .wsb (${impl})`, multiIds.every((id) => r.wsb.includes(id))); + check(`multi-id single env assignment (${impl})`, assignmentCount(r.wsb) === 1, `count=${assignmentCount(r.wsb)}`); + check(`multi-id ids space-joined in env channel (${impl})`, r.wsb.includes(multiIds.join(" "))); +} +check("multi-id .wsb byte-equal (.NET vs Node)", net.genWsbMulti.wsb === node.genWsbMulti.wsb); + console.log("\n[5] list_categories parity"); check("totalServices equal", net.cats.totalServices === node.cats.totalServices, `net=${net.cats.totalServices} node=${node.cats.totalServices}`); check("category counts equal", canon(catMap(net.cats)) === canon(catMap(node.cats)), `net=${JSON.stringify(catMap(net.cats))}\n node=${JSON.stringify(catMap(node.cats))}`);