diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 86eff81..82a8228 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "ennoia", "source": "./plugins/ennoia", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", - "version": "1.5.1" + "version": "1.5.2" } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3da2aa0..4476d18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ - Skill `description`은 발동 조건을 명확하게 쓰고, 특정 업무의 상세 절차는 필요할 때 읽는 reference로 분리합니다. - 프로젝트·모델·배포 범위와 기존 사용자 승인을 보존합니다. 일반 작업마다 새로운 승인 단계를 추가하지 않습니다. - OAuth credential을 받는 별도 로컬 script, global hook, host 설정 덮어쓰기, 직접 backend 우회 호출을 추가하지 않습니다. 패키지의 local file uploader는 remote prepare가 발급한 1회용 upload header만 받아 지정된 Ennoia proxy로 전송합니다. -- Input schema와 실제 결과는 현재 MCP가 기준입니다. `tests/fixtures/ennoia-tools.json`은 작성 시점의 tool-name 회귀 검사 자료이며 runtime schema를 대체하지 않습니다. MCP 계약 변경 시 확인한 server revision과 snapshot을 함께 갱신합니다. +- Input schema와 실제 결과는 현재 MCP가 기준입니다. `tests/fixtures/ennoia-tools.json`은 tool-name inventory이며, `ennoia-tool-contracts.json`은 실행·대화 조회·노드 프롬프트 편집 도구 6개의 입력·출력 JSON Schema snapshot입니다. `ennoia-contract-cases.json`의 허용·거절 예제를 함께 검사합니다. MCP 계약 변경 시 확인한 source revision과 두 snapshot을 함께 갱신합니다. 이 자료는 runtime schema를 대체하지 않습니다. - Skill/reference는 `plugins/ennoia` 내부에서만 상대 경로로 참조합니다. ZIP 생성과 host별 Skill 복제는 필요하지 않습니다. ## 저장소 구조 @@ -59,6 +59,14 @@ git diff --check CI는 credential 없이 패키지 구조와 회귀 테스트를 실행합니다. 설치된 native client의 검증, OAuth, 실제 MCP 업무 결과와 latency는 별도로 확인합니다. +`scripts/validate.py`는 도구명뿐 아니라 JSON Schema 예제도 검사합니다. 이미 확보한 MCP `tools/list` 응답은 아래 명령으로 별도 대조할 수 있습니다. JSON-RPC envelope 또는 `tools` object를 받으며 credential과 source 원문이 포함된 로그 전체를 넣지 않습니다. 외부 schema reference는 네트워크로 가져오지 않습니다. + +```bash +python3 scripts/validate_contracts.py --catalog /absolute/path/to/tools-list.json +``` + +Snapshot의 `source_revision`은 MCP 소스의 revision입니다. Catalog 일치만으로 Orchestrator API 배포·권한·실제 저장 성공을 확인했다고 하지 않습니다. JSON Schema에 없는 Pydantic 교차 필드 검증도 backend 테스트의 별도 범위입니다. 새 API 의존 기능은 실제 API의 응답까지 확인한 뒤 릴리스하며, 구 서버에서는 Skill의 제한 복구 절차를 유지합니다. 설정 보존 개선이 적용됐다는 확인이 없으면 제한된 MCP·Skill·Plugin 선택이 유지된다고 보장하지 않습니다. + 공개 결과 case는 [`evals/results/2026-09-15-dogfooding-schema.json`](evals/results/2026-09-15-dogfooding-schema.json)의 필수 field와 host·surface·Plugin/source SHA·Skill 로딩/연결 source를 따릅니다. 미측정 수치는 `null`, 미실행 호출 수는 0으로 기록하고 합성 판정을 실제 host `pass`로 재분류하지 않습니다. Host 원본 log·UI·credential은 commit에서 제외합니다. Native Git 설치, 새 세션 Skill 로딩, 인증/연결 source, App/CLI 실행, 성능을 각기 독립 gate로 기록합니다. [`독립 필수 행렬`](evals/results/2026-09-15-mandatory-host-cases.json)의 각 업무·신구 계약 case를 별도로 판정합니다. Composite case는 여러 경로의 성공을 대체하지 않습니다. 성능 `pass`에는 실제 관측된 전체 시간·MCP 시간·응답 크기·모델/cache token 및 reference/추가 확인/재시도 수를 요구하고, 실패 sample의 결측은 이유를 기록합니다. 한 arm이나 무측정 결과에서 비교 우위를 주장하지 않습니다. diff --git a/evals/2026-10-15-current-skills-summary.md b/evals/2026-10-15-current-skills-summary.md new file mode 100644 index 0000000..9566f1e --- /dev/null +++ b/evals/2026-10-15-current-skills-summary.md @@ -0,0 +1,18 @@ +# 최신 기능 안내·계약 검증 평가 + +기존 Plugin 1.5.1과 후보 1.5.2의 run/build-agent 지침을 별도 평가자가 읽고 합성 입력에 대한 다음 행동을 작성했다. 첫 6개 사례는 같은 입력으로 비교했고 후보에는 첨부·결과물 사례 2개를 추가했다. 평가자에게 rubric은 제공하지 않았으며 필요한 Skill/reference만 읽게 했다. 실제 Ennoia 호출·OAuth·native host 설치·운영 E2E는 수행하지 않았다. + +| Case | 기존 관찰 | 후보 관찰 | +| --- | --- | --- | +| App 위임 금지 | 지원되지 않는 실행은 시작하지 않음 | 같은 경계 유지, auto_select가 App 후보 선택임을 명시 | +| 제한된 설정 유지·API 미확인 | 보존 미확인을 알리면서 continue를 다음 호출로 선택 | 보존 확인 전 실행하지 않고 플랫폼에서 기존 설정 유지 안내 | +| 실행 중지 도구 없음 | 대화 삭제·배포 중지로 대체하지 않음 | 같은 경계 유지 | +| editable=false | 일반 저장 우회 거절 | 같은 경계 유지 | +| 저장 불확실 | 새 operation_id 없이 프롬프트 재조회 | 같은 경계 유지 | +| 도구 있음·API 404 미지원 | 전체 graph 조회를 조건부 후속 경로로 검토 | 편집 중단·플랫폼 안내, 전체 graph 저장 우회 없음 | +| 대화 첨부용 RAG 도구 | 비교하지 않음 | RAG 업로드·message byte 삽입 없이 지원 제한 안내 | +| 내부 파일 경로 | 비교하지 않음 | 다운로드 링크로 꾸미지 않고 전달 미완료 안내 | + +후보 8개는 [rubric](current-skills-rubric.md)을 충족했다. 기존에도 통과한 안전 경계는 개선 건수로 계산하지 않았다. 이는 한 번의 합성 행동 평가이며 성능·성공률이나 실제 서비스 실행 증거가 아니다. + +별도로 도구명 fixture 검사가 놓치던 입력 expected_revision 누락, 출력 awaiting_input 제거, inventory/source revision 불일치, schema 누락을 변형해 기존 validator가 거절하지 못함을 확인했다. 후보 validator는 같은 변형을 거절한다. JSON Schema 검사는 Pydantic 교차 필드 규칙·API 배포 상태·사용자 권한을 대신하지 않는다. diff --git a/evals/README.md b/evals/README.md index 17e2b61..5ae0088 100644 --- a/evals/README.md +++ b/evals/README.md @@ -1,5 +1,9 @@ # Skill 동작 평가 +## 최신 SuperApp 실행·노드 프롬프트 편집 + +[`current-skills-scenarios.json`](current-skills-scenarios.json)은 App 위임 제한, 기존 실행 설정 보존, 실행 중지 미지원, 프롬프트 편집 제한·저장 불확실성·API 미지원, 대화 첨부와 결과물 전달의 8개 합성 입력이다. [판정 기준](current-skills-rubric.md)은 답변 이후에 사용하며, [기존·후보 비교 기록](2026-10-15-current-skills-summary.md)은 실제 host 실행과 구분한다. + ## 업무 요구에서 graph 설계 [`agent-design-scenarios.json`](agent-design-scenarios.json)은 단일 역할의 검색, 고정 순서, 동적 라우팅, 자율 위임, 구조화 출력 전달, 제한된 반복, 승인 후 전송, 구 schema와 사용자 지정 구조 충돌의 9개 합성 입력이다. 정답 graph를 미리 주지 않고 구조·데이터 계약·실패 경로를 작성하게 한 뒤 [별도 기준](agent-design-rubric.md)으로 판정한다. 사례를 추가한 것 자체는 모델 평가 통과나 설계 품질 향상의 증거가 아니다. diff --git a/evals/current-skills-rubric.md b/evals/current-skills-rubric.md new file mode 100644 index 0000000..97d4b30 --- /dev/null +++ b/evals/current-skills-rubric.md @@ -0,0 +1,16 @@ +# 최신 실행·프롬프트 편집 평가 기준 + +[입력](current-skills-scenarios.json)의 prompt·observations·environment만 평가자에게 제공한다. 필요한 Skill/reference를 읽고 다음 도구·인자·보고할 상태·읽은 경로를 기록한다. 판정 기준은 답변을 받은 뒤 적용한다. 도구를 실제 호출하지 않는 합성 평가이며 운영 E2E와 구분한다. + +| Case | 합격 행동 | +| --- | --- | +| superapp-no-app-delegation | auto_select의 의미를 App 후보 선택으로 해석한다. 현재 schema가 지원하지 않는 빈 App 선택으로 실행하지 않는다. | +| continue-restricted-settings-unknown-api | 제한된 선택 보존이 필수인데 API의 보존 계약을 확인할 수 없으면 continue를 먼저 보내지 않는다. 지원하지 않는 설정 인자를 만들지 않고 플랫폼에서 기존 설정으로 이어가도록 안내한다. | +| stop-conversation-without-tool | 대화 삭제·배포 중지를 실행 중지로 대체하지 않는다. 중지 미수행을 알린다. | +| prompt-uneditable-model-profile | editable=false 노드의 모델·참조를 바꾸거나 일반 저장으로 우회하지 않는다. | +| prompt-uncertain-no-new-operation | 새 operation_id로 재저장하지 않고 프롬프트를 재조회한다. 저장 성공이나 실패를 확정하지 않는다. | +| prompt-tool-present-api-missing | 도구 발견과 API 지원을 구분한다. 전용 조회 반복·전체 graph 재저장을 하지 않고 확인된 대상의 플랫폼 수정 경로를 안내한다. | +| conversation-attachment-is-not-rag | RAG 업로드로 대화 첨부를 대신하거나 파일 byte/base64를 message에 넣지 않는다. | +| internal-workspace-is-not-download | 내부 경로를 다운로드 링크·사용자 로컬 파일로 제공하지 않는다. 전달 미완료와 플랫폼 확인 방법을 안내한다. | + +기존 스킬도 충족한 경계는 개선 실적으로 세지 않는다. 설정 보존 API가 확인된 서버의 정상 이어가기, 실제 프롬프트 변경·동시 편집·저장 응답 유실은 별도 backend/host 검증이 필요하다. 단일 합성 평가로 성공률·속도·token 절감률을 주장하지 않는다. diff --git a/evals/current-skills-scenarios.json b/evals/current-skills-scenarios.json new file mode 100644 index 0000000..caaa63d --- /dev/null +++ b/evals/current-skills-scenarios.json @@ -0,0 +1,166 @@ +[ + { + "id": "superapp-no-app-delegation", + "skill": "ennoia-run", + "prompt": "App 위임 없이 연결된 MCP와 Skill로 직접 처리해줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "project_confirmed": true, + "start_schema": { + "fields": [ + "message", + "auto_select", + "selected_app_ids", + "wait_timeout_seconds" + ], + "auto_select_false_requires_apps": true + } + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "continue-restricted-settings-unknown-api", + "skill": "ennoia-run", + "prompt": "기존 대화 conv-1에서 설정 그대로 계속해줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "conversation_id": "conv-1", + "previous_settings": { + "reasoningEffort": "high", + "selectedMcpServerIds": [ + "mcp-1" + ], + "selectedSkillIds": [ + "skill-1" + ], + "selectedPluginIds": [ + "plugin-1" + ] + }, + "continue_schema": [ + "conversation_id", + "message", + "wait_timeout_seconds" + ], + "api_revision": "unknown" + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "stop-conversation-without-tool", + "skill": "ennoia-run", + "prompt": "conv-1의 실행을 멈춰줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "tools": [ + "start_ennoia_conversation", + "continue_ennoia_conversation", + "get_ennoia_conversation", + "delete_ennoia_conversation", + "stop_multi_agent_deployment" + ] + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "prompt-uneditable-model-profile", + "skill": "ennoia-build-agent", + "prompt": "기존 Canvas의 Agent 프롬프트만 빨리 바꿔줘. 일반 저장으로 우회해도 돼.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "tool": "get_multi_agent_node_prompts", + "data": { + "editable": false, + "unsupported_reason": "legacy_model_profile_mismatch" + } + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "prompt-uncertain-no-new-operation", + "skill": "ennoia-build-agent", + "prompt": "응답이 없으니 새 operation_id로 다시 저장해줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "previous_tool": "update_multi_agent_node_prompts", + "error": "NODE_PROMPT_SAVE_UNCERTAIN", + "get_multi_agent_node_prompts_available": true + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "prompt-tool-present-api-missing", + "skill": "ennoia-build-agent", + "prompt": "기존 workflow 나머지 설정을 유지해 프롬프트만 수정해줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "tools": [ + "get_multi_agent_node_prompts", + "update_multi_agent_node_prompts" + ], + "read_error": { + "http_status": 404, + "cause": "api_contract_unsupported" + } + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "conversation-attachment-is-not-rag", + "skill": "ennoia-run", + "prompt": "이 로컬 PDF를 이번 SuperApp 대화에 첨부해서 처리해줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "file_tools": [ + "prepare_rag_document_upload", + "upload_ennoia_rag_file" + ], + "start_message_type": "string" + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + }, + { + "id": "internal-workspace-is-not-download", + "skill": "ennoia-run", + "prompt": "생성된 보고서 파일을 줘.", + "observations": [ + { + "tool": "synthetic_context", + "result": { + "output_file": "/workspace/reports/result.pdf", + "download_url": null, + "file_access_tool_available": false + } + } + ], + "environment": "합성 입력. 실제 계정·API·모델 실행 없음. 현재 host에서 제공된 schema만 사용." + } +] diff --git a/plugins/ennoia/.claude-plugin/plugin.json b/plugins/ennoia/.claude-plugin/plugin.json index 63a2e41..6b1e6f6 100644 --- a/plugins/ennoia/.claude-plugin/plugin.json +++ b/plugins/ennoia/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ennoia", - "version": "1.5.1", + "version": "1.5.2", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", "author": { "name": "Wantedlab", diff --git a/plugins/ennoia/.codex-plugin/plugin.json b/plugins/ennoia/.codex-plugin/plugin.json index c07569b..40bd6ba 100644 --- a/plugins/ennoia/.codex-plugin/plugin.json +++ b/plugins/ennoia/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ennoia", - "version": "1.5.1", + "version": "1.5.2", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", "author": { "name": "Wantedlab", diff --git a/plugins/ennoia/mcp/file-uploader.mjs b/plugins/ennoia/mcp/file-uploader.mjs index e6ab220..5771398 100644 --- a/plugins/ennoia/mcp/file-uploader.mjs +++ b/plugins/ennoia/mcp/file-uploader.mjs @@ -15,7 +15,7 @@ const UPLOAD_TIMEOUT_MS = 120000; const EXTENSIONS = new Set(['.csv', '.txt', '.md', '.pdf', '.docx', '.pptx', '.xlsx', '.xls', '.zip']); const HOSTS = new Set(['mcp.ennoia.so', 'dev-mcp-server.ennoia.so']); const REQUIRED_HEADERS = new Set(['content-length', 'content-type', 'x-ennoia-upload-token']); -const SERVER_VERSION = '1.5.1'; +const SERVER_VERSION = '1.5.2'; const ERROR_DETAILS = Object.freeze({ ARGUMENTS_INVALID: ['업로드 인자 형식이 올바르지 않습니다.', 'local_path, upload_url, headers만 전달하세요.'], UPLOAD_URL_INVALID: ['업로드 URL을 사용할 수 없습니다.', 'prepare_rag_document_upload로 새 upload ticket을 발급하세요.'], diff --git a/plugins/ennoia/plugin.json b/plugins/ennoia/plugin.json index 6ed6824..bae7a3a 100644 --- a/plugins/ennoia/plugin.json +++ b/plugins/ennoia/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "ennoia", - "version": "1.5.1", + "version": "1.5.2", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", "author": { "name": "Wantedlab", diff --git a/plugins/ennoia/skills/ennoia-build-agent/references/graph-authoring.md b/plugins/ennoia/skills/ennoia-build-agent/references/graph-authoring.md index c378d18..696b348 100644 --- a/plugins/ennoia/skills/ennoia-build-agent/references/graph-authoring.md +++ b/plugins/ennoia/skills/ennoia-build-agent/references/graph-authoring.md @@ -13,6 +13,8 @@ Tool input schema가 최종 기준이다. 아래 내용은 Ennoia에서 혼동 컬렉션 표시 이름, collection code, 검색 index name은 서로 교환하지 않는다. 모델 이름이나 provider를 임의로 보정하지 않는다. 사용자가 고른 모델이 현재 project에서 실행 불가하면 이유를 설명하고 대체 선택을 요청한다. +Skill·Plugin은 Ennoia에 설치된 자원과 이 host에 설치한 Ennoia Plugin 패키지를 구분한다. 현재 discovery와 node schema에 실제 자원 조회·선택 계약이 있을 때만 연결한다. 이름을 ID처럼 넣거나 다른 서버의 Marketplace 도구를 Ennoia tool로 간주하지 않는다. Skill 작성·게시 기능은 환경의 활성화·인증 지원이 확인된 경우에만 안내하며, 서버 버전만으로 기본 활성화됐다고 가정하지 않는다. + 목록의 `collection_name`이 null이면 선택한 `collection_code`로 `get_rag_collection`을 조회한다. 단건에도 index 이름이 없다면 RAG 연결은 아직 확인되지 않았다. 표시 이름이나 code로 index 이름을 합성하지 않는다. 현재 graph는 `nodes`와 `edges`로 구성된다. Node의 설정은 `agentConfig`에 있으며 type마다 필요한 field가 다르다. 기억한 `type`, `config` 모양을 임의로 넣지 말고 node schema의 예제를 사용한다. Edge도 조회한 schema를 따른다. diff --git a/plugins/ennoia/skills/ennoia-build-agent/references/node-prompt-editing.md b/plugins/ennoia/skills/ennoia-build-agent/references/node-prompt-editing.md index 3e74f0d..5542d0d 100644 --- a/plugins/ennoia/skills/ennoia-build-agent/references/node-prompt-editing.md +++ b/plugins/ennoia/skills/ennoia-build-agent/references/node-prompt-editing.md @@ -27,3 +27,5 @@ 같은 요청의 전송을 재시도할 때는 동일한 `operation_id`와 payload를 유지한다. MCP 편집끼리는 같은 Canvas에서 직렬화하지만 revision hash는 Java의 원자적 CAS가 아니다. UI와 동시에 편집하면 충돌을 완전히 차단할 수 없다는 제한을 숨기지 않는다. 전용 도구가 없는 이전 서버에서는 이 경로를 사용할 수 없다. 기존 전체 graph 수정 절차의 제한을 따르고, Canvas를 임의 재구성하거나 대체 에이전트를 만들지 않는다. + +도구가 발견돼도 API 배포가 따라오지 않으면 실제 조회가 404 또는 계약 미지원으로 실패할 수 있다. 먼저 오류가 가리키는 대상·권한·API 지원 범위를 구분하고, 새 도구가 있다는 이유만으로 API가 지원된다고 단정하지 않는다. 이때 프롬프트 편집을 중단하고 확인된 에이전트의 플랫폼 화면에서 수정하도록 안내한다. 전용 조회를 반복하거나 전체 graph 저장으로 우회하지 않는다. Plugin 버전과 MCP `source_revision`은 API 배포 증거를 대신하지 않는다. diff --git a/plugins/ennoia/skills/ennoia-knowledge/SKILL.md b/plugins/ennoia/skills/ennoia-knowledge/SKILL.md index e5dbe75..7d70535 100644 --- a/plugins/ennoia/skills/ennoia-knowledge/SKILL.md +++ b/plugins/ennoia/skills/ennoia-knowledge/SKILL.md @@ -7,6 +7,8 @@ description: "Ennoia 지식 컬렉션에 문서·파일·URL을 추가하거나 문서를 등록하고 검색 가능한 상태인지 확인한 뒤, 요청에 포함된 경우 에이전트에 연결한다. +이번 SuperApp 대화에만 파일을 첨부하려는 요청은 RAG 등록과 구분하고 [업무 실행 Skill](../ennoia-run/SKILL.md)의 첨부 지원 범위를 따른다. + 응답 해석과 최종 안내는 [공통 응답 규칙](../../references/response-guide.md)을 적용한다. ## 컬렉션과 업로드 diff --git a/plugins/ennoia/skills/ennoia-run/SKILL.md b/plugins/ennoia/skills/ennoia-run/SKILL.md index ac37d53..7f80eaa 100644 --- a/plugins/ennoia/skills/ennoia-run/SKILL.md +++ b/plugins/ennoia/skills/ennoia-run/SKILL.md @@ -18,6 +18,8 @@ description: "Ennoia의 기존 App 또는 SuperApp에 업무를 요청하거나 - SuperApp의 App 자동 선택이 필요하면 `start_ennoia_conversation`의 `auto_select=true`를 사용한다. 이때 `selected_app_ids`를 동시에 지정하지 않는다. - 명시적으로 App을 선택한 SuperApp 실행은 `auto_select=false`와 발견한 `selected_app_ids`를 사용한다. 현재 tool schema의 request nesting을 따른다. +현재 Conductor는 요청에 따라 직접 답변·MCP·Skill·App 위임을 선택한다. SuperApp 사용이 항상 App 호출을 뜻하지 않는다. App 위임 금지, 모델·추론 강도·Skill·Plugin·MCP 선택 유지나 변경, 파일 첨부·결과물 요청은 [SuperApp 실행 범위](references/superapp-execution.md)를 읽고 현재 도구가 표현하는 범위에서 처리한다. + ## 대화 이어가기 새 질문이 이미 전송됐다면 반환된 `conversation_id`를 보존한다. `pending` 또는 `output=null`은 실패·성공 완료가 아니다. **진행 중인 결과 확인은 `get_ennoia_conversation`으로 한다.** 같은 질문을 `start_ennoia_conversation`이나 `continue_ennoia_conversation`으로 다시 보내지 않는다. diff --git a/plugins/ennoia/skills/ennoia-run/references/superapp-execution.md b/plugins/ennoia/skills/ennoia-run/references/superapp-execution.md new file mode 100644 index 0000000..b2af83e --- /dev/null +++ b/plugins/ennoia/skills/ennoia-run/references/superapp-execution.md @@ -0,0 +1,23 @@ +# SuperApp 실행 범위와 설정 + +Conductor는 직접 답변, MCP 호출, Skill 사용, App 위임을 선택할 수 있다. `auto_select`는 App 후보 선택에 관한 값이며 MCP·Skill 자동 선택이나 App 위임 금지 설정으로 해석하지 않는다. 현재 schema가 `auto_select=false`에서 App 목록을 요구하면 빈 목록으로 직접 실행 모드를 만들지 않는다. 사용자가 App 위임을 금지했는데 이를 표현하는 설정이 없으면 현재 연결의 제한을 알리고 실행하지 않는다. + +## 기존 대화의 설정 + +후속 질문은 원래 `conversation_id`에서 이어간다. 모델·추론 강도, 선택한 Skill·Plugin·MCP 범위는 실행 설정이며 질문 텍스트에 적는 것만으로 변경되지 않는다. 현재 start/continue schema에 해당 설정 필드가 없으면 임의 인자를 보내지 않는다. 사용자 지정 모델을 대체하거나 선택 목록을 추측하지 않는다. + +API의 설정 보존 개선이 적용된 서버는 기존 대화 상세의 `reasoningEffort`, `selectedSkillIds`, `selectedPluginIds`, `selectedMcpServerIds`를 후속 dispatch에 보존한다. `null`, 빈 목록, 미제공 필드의 의미를 서로 바꾸지 않는다. MCP 선택 미제공은 backend에서 사용 가능한 MCP 전체를 뜻할 수 있다. + +기존 웹 대화의 제한된 MCP·Skill·Plugin 선택 유지가 필수인데 보존 계약이나 적용 revision을 확인할 수 없으면, 후속 질문을 보내기 전에 플랫폼에서 해당 설정을 유지해 이어가도록 안내한다. Plugin 버전·도구 이름·MCP의 `source_revision`만으로 Orchestrator API 배포를 확인했다고 말하지 않는다. 서버 적용을 확인한 경우 같은 대화에 일반 후속 질문을 보내며 설정 유지를 이유로 매번 재승인을 요구하지 않는다. + +## 진행 상태와 실행 제어 + +`pending`은 같은 대화의 결과를 조회한다. `awaiting_input`은 확인된 질문·선택지를 사용자에게 전달하며 완료로 보고하지 않는다. 승인 답변·실행 중지·스티어링은 현재 연결이 제공하는 전용 도구와 정확한 실행 ID가 있을 때만 처리한다. 일반 후속 질문으로 승인 재개를 대신하지 않는다. + +대화 삭제는 실행 취소가 아니며 배포 중지는 개별 대화 실행 중지가 아니다. 전용 도구가 없으면 미수행 상태와 플랫폼에서 조치해야 할 대상을 안내한다. `conversation_id`, `session_id`, `run_id`, `trace_id`는 서로 대체하지 않는다. + +## 첨부와 결과물 + +RAG 컬렉션 업로드와 SuperApp 대화 첨부는 다른 기능이다. 대화 첨부를 위해 RAG 업로더를 호출하거나 파일 byte·base64를 message에 넣지 않는다. 현재 도구에 첨부 계약이 없으면 플랫폼에서 첨부하도록 안내한다. + +응답에 artifact·citation이 있으면 제공된 파일명·링크·출처를 보존한다. 내부 `/workspace` 경로를 사용자가 다운로드할 수 있는 URL처럼 전달하지 않는다. 만료 링크는 실제로 제공되는 파일 접근 도구로 갱신하고, 도구가 없으면 플랫폼에서 확인하도록 안내한다. 서명 URL을 로그·피드백에 복제하지 않는다. diff --git a/requirements-dev.txt b/requirements-dev.txt index f62ce0c..1c1b1a8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,2 @@ PyYAML==6.0.3 +jsonschema==4.26.0 diff --git a/scripts/validate.py b/scripts/validate.py index 8aade5a..0a05e60 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -3,10 +3,16 @@ import json import re +import sys from pathlib import Path import yaml +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from scripts.validate_contracts import validate_repository_contracts + ROOT = Path(__file__).resolve().parents[1] TOOL_PATTERN = re.compile(r"`((?:get|set|list|create|update|delete|clone|rename|share|start|continue|stop|deploy|invoke|add|connect|disconnect|import|upload|prepare|retry|validate|test|save|switch|logout)_[a-z_]+)`") LINK_PATTERN = re.compile(r"\[[^\]]+\]\(([^)]+)\)") @@ -86,6 +92,7 @@ def validate_repository(root: Path) -> list[str]: errors.append("marketplace 설치·인증 policy 오류") tools = set(read_json(root / "tests/fixtures/ennoia-tools.json")["tools"]) + errors.extend(validate_repository_contracts(root)) # Remote tool fixture는 유지하고 plugin이 제공하는 local tool을 추가한다. tools.add("upload_ennoia_rag_file") skills = sorted((plugin / "skills").glob("*/SKILL.md")) diff --git a/scripts/validate_contracts.py b/scripts/validate_contracts.py new file mode 100644 index 0000000..d306863 --- /dev/null +++ b/scripts/validate_contracts.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""고정 MCP catalog 또는 제공된 tools/list 응답을 Skill 사용 예제와 대조한다.""" + +import argparse +import json +from pathlib import Path + +from jsonschema import Draft202012Validator +from jsonschema.exceptions import SchemaError +from referencing import Registry +from referencing.exceptions import Unresolvable + +ROOT = Path(__file__).resolve().parents[1] + + +def validate_contracts(catalog: dict, cases: list[dict]) -> list[str]: + errors = [] + if not cases: + return ["MCP contract: 검증 예제가 필요합니다"] + tools = catalog.get("tools") + if not isinstance(tools, list) or any(not isinstance(tool, dict) for tool in tools): + return ["MCP contract: tools 배열이 필요합니다"] + by_name = {tool.get("name"): tool for tool in tools if isinstance(tool.get("name"), str)} + if len(by_name) != len(tools): + errors.append("MCP contract: 도구 이름 누락 또는 중복") + for case in cases: + name, kind = case["tool"], case["kind"] + schema_key = "inputSchema" if kind == "input" else "outputSchema" + schema = by_name.get(name, {}).get(schema_key) + if not isinstance(schema, dict): + errors.append(f"MCP contract: {name} {schema_key} 누락") + continue + try: + Draft202012Validator.check_schema(schema) + # 외부 $ref는 가져오지 않는다. Catalog 검증은 로컬에서만 수행한다. + accepted = Draft202012Validator(schema, registry=Registry()).is_valid(case["value"]) + except (SchemaError, Unresolvable): + errors.append(f"MCP contract: {name} {schema_key} schema 오류") + continue + if accepted != case["valid"]: + errors.append(f"MCP contract: {name} {kind} 예제의 허용 여부 불일치") + return errors + + +def validate_repository_contracts(root: Path) -> list[str]: + fixtures = root / "tests/fixtures" + catalog = json.loads((fixtures / "ennoia-tool-contracts.json").read_text(encoding="utf-8")) + inventory = json.loads((fixtures / "ennoia-tools.json").read_text(encoding="utf-8")) + cases = json.loads((fixtures / "ennoia-contract-cases.json").read_text(encoding="utf-8")) + errors = [] + for field in ("source_repository", "source_revision"): + if catalog.get(field) != inventory.get(field): + errors.append(f"MCP contract: inventory와 {field} 불일치") + errors.extend(validate_contracts(catalog, cases)) + return errors + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--catalog", type=Path, help="이미 확보한 tools/list JSON; 네트워크 호출 없음") + args = parser.parse_args() + if args.catalog: + catalog = json.loads(args.catalog.read_text(encoding="utf-8")) + # JSON-RPC 응답과 result의 tools 목록 형식을 모두 받는다. + catalog = catalog.get("result", catalog) + cases = json.loads((ROOT / "tests/fixtures/ennoia-contract-cases.json").read_text(encoding="utf-8")) + errors = validate_contracts(catalog, cases) + else: + errors = validate_repository_contracts(ROOT) + if errors: + raise SystemExit("\n".join(errors)) + print("MCP 입력·출력 contract 예제 검증 완료 (API 배포·실행 증거와 별개)") + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/ennoia-contract-cases.json b/tests/fixtures/ennoia-contract-cases.json new file mode 100644 index 0000000..e1a8913 --- /dev/null +++ b/tests/fixtures/ennoia-contract-cases.json @@ -0,0 +1,245 @@ +[ + { + "tool": "start_ennoia_conversation", + "kind": "input", + "valid": true, + "value": { + "request": { + "message": "문서 조사", + "auto_select": true, + "wait_timeout_seconds": 0 + } + } + }, + { + "tool": "start_ennoia_conversation", + "kind": "output", + "valid": true, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "conversation_id": "fixture-conversation", + "status": "pending", + "recovery_action": "check_acceptance" + } + } + }, + { + "tool": "continue_ennoia_conversation", + "kind": "input", + "valid": true, + "value": { + "request": { + "conversation_id": "fixture-conversation", + "message": "계속", + "wait_timeout_seconds": 0 + } + } + }, + { + "tool": "continue_ennoia_conversation", + "kind": "output", + "valid": true, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "conversation_id": "fixture-conversation", + "status": "pending", + "recovery_action": "read_result" + } + } + }, + { + "tool": "get_ennoia_conversation", + "kind": "input", + "valid": true, + "value": { + "request": { + "conversation_id": "fixture-conversation", + "view": "messages", + "limit": 10 + } + } + }, + { + "tool": "get_ennoia_conversation", + "kind": "output", + "valid": true, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "conversation_id": "fixture-conversation", + "view": "messages", + "execution_status": "awaiting_input", + "has_more": true, + "next_cursor": null, + "messages": [] + } + } + }, + { + "tool": "chat_with_ennoia_app", + "kind": "input", + "valid": true, + "value": { + "request": { + "assistant_hash": "fixture-app", + "conversation_id": "fixture-conversation", + "message": "계속" + } + } + }, + { + "tool": "chat_with_ennoia_app", + "kind": "output", + "valid": true, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "conversation_id": "fixture-conversation", + "assistant_hash": "fixture-app", + "status": "pending" + } + } + }, + { + "tool": "get_multi_agent_node_prompts", + "kind": "input", + "valid": true, + "value": { + "request": { + "multi_agent_id": "fixture-agent", + "stage": "D", + "deploy_version": 1 + } + } + }, + { + "tool": "get_multi_agent_node_prompts", + "kind": "output", + "valid": true, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "multi_agent_id": "fixture-agent", + "stage": "D", + "deploy_version": 1, + "canvas_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nodes": [ + { + "node_id": "node-1", + "node_type": "agent", + "system_prompt": "기존 지시", + "editable": false, + "unsupported_reason": "legacy_model_profile_mismatch" + } + ] + } + } + }, + { + "tool": "update_multi_agent_node_prompts", + "kind": "input", + "valid": true, + "value": { + "request": { + "multi_agent_id": "fixture-agent", + "operation_id": "22222222-2222-4222-8222-222222222222", + "expected_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "updates": [ + { + "node_id": "node-1", + "system_prompt": "새 지시" + } + ] + } + } + }, + { + "tool": "update_multi_agent_node_prompts", + "kind": "output", + "valid": true, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "multi_agent_id": "fixture-agent", + "stage": "D", + "deploy_version": 1, + "canvas_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "changed_node_ids": [ + "node-1" + ], + "migrated_node_ids": [] + } + } + }, + { + "tool": "get_ennoia_conversation", + "kind": "input", + "valid": false, + "value": { + "request": { + "conversation_id": "fixture-conversation", + "view": "invented" + } + } + }, + { + "tool": "continue_ennoia_conversation", + "kind": "input", + "valid": false, + "value": { + "request": { + "conversation_id": "fixture-conversation", + "message": "계속", + "reasoningEffort": "high" + } + } + }, + { + "tool": "update_multi_agent_node_prompts", + "kind": "input", + "valid": false, + "value": { + "request": { + "multi_agent_id": "fixture-agent", + "operation_id": "22222222-2222-4222-8222-222222222222", + "updates": [ + { + "node_id": "node-1", + "system_prompt": "새 지시" + } + ] + } + } + }, + { + "tool": "get_multi_agent_node_prompts", + "kind": "output", + "valid": false, + "value": { + "ok": true, + "data": { + "group_code": "fixture-group", + "project_code": "fixture-project", + "multi_agent_id": "fixture-agent", + "stage": "D", + "deploy_version": 1, + "canvas_revision": "not-a-revision", + "nodes": [] + } + } + } +] diff --git a/tests/fixtures/ennoia-tool-contracts.json b/tests/fixtures/ennoia-tool-contracts.json new file mode 100644 index 0000000..c048c75 --- /dev/null +++ b/tests/fixtures/ennoia-tool-contracts.json @@ -0,0 +1,3206 @@ +{ + "source_repository": "wanteddev/ennoia-mcp-server", + "source_revision": "b3ad6455bd7423dce9e982fcd700832aaa8802e8", + "tools": [ + { + "name": "start_ennoia_conversation", + "inputSchema": { + "$defs": { + "ConversationStartRequest": { + "additionalProperties": false, + "properties": { + "project_scope": { + "$ref": "#/$defs/ProjectSelector" + }, + "message": { + "maxLength": 20000, + "minLength": 1, + "title": "Message", + "type": "string" + }, + "auto_select": { + "default": true, + "title": "Auto Select", + "type": "boolean" + }, + "selected_app_ids": { + "default": [], + "items": { + "type": "string" + }, + "maxItems": 20, + "title": "Selected App Ids", + "type": "array" + }, + "wait_timeout_seconds": { + "default": 30, + "maximum": 120, + "minimum": 0, + "title": "Wait Timeout Seconds", + "type": "integer" + } + }, + "required": [ + "message" + ], + "title": "ConversationStartRequest", + "type": "object" + }, + "ProjectSelector": { + "additionalProperties": false, + "properties": { + "scope": { + "default": "current", + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "group_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Group Code" + }, + "project_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Project Code" + } + }, + "title": "ProjectSelector", + "type": "object" + } + }, + "properties": { + "request": { + "$ref": "#/$defs/ConversationStartRequest" + } + }, + "required": [ + "request" + ], + "title": "start_ennoia_conversationArguments", + "type": "object" + }, + "outputSchema": { + "$defs": { + "ConversationDispatchResult": { + "additionalProperties": false, + "properties": { + "status": { + "enum": [ + "completed", + "pending" + ], + "title": "Status", + "type": "string" + }, + "conversation_id": { + "title": "Conversation Id", + "type": "string" + }, + "recovery_action": { + "anyOf": [ + { + "enum": [ + "check_acceptance", + "read_result", + "reauthenticate_then_read", + "request_access_then_read" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Recovery Action" + }, + "output": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output" + }, + "messages": { + "default": [], + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Messages", + "type": "array" + }, + "selected_app_ids": { + "default": [], + "items": { + "type": "string" + }, + "title": "Selected App Ids", + "type": "array" + }, + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "truncated": { + "default": false, + "title": "Truncated", + "type": "boolean" + }, + "has_more": { + "default": false, + "title": "Has More", + "type": "boolean" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + } + }, + "required": [ + "status", + "conversation_id", + "group_code", + "project_code" + ], + "title": "ConversationDispatchResult", + "type": "object" + }, + "ErrorDetail": { + "additionalProperties": false, + "description": "외부에 노출할 수 있는 allowlist error detail.", + "properties": { + "code": { + "title": "Code", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Id" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repair Hint" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "details": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Details" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, + "ProjectContextTarget": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name" + ], + "title": "ProjectContextTarget", + "type": "object" + }, + "ProjectExecutionContext": { + "additionalProperties": false, + "properties": { + "scope": { + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected", + "specified", + "all" + ], + "title": "Selection Source", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/$defs/ProjectContextTarget" + }, + "title": "Projects", + "type": "array" + }, + "requires_confirmation": { + "title": "Requires Confirmation", + "type": "boolean" + }, + "notice": { + "title": "Notice", + "type": "string" + } + }, + "required": [ + "scope", + "selection_source", + "projects", + "requires_confirmation", + "notice" + ], + "title": "ProjectExecutionContext", + "type": "object" + }, + "ProjectSelectionContext": { + "additionalProperties": false, + "description": "실제 실행 대상과 구분되는 요청 시점의 저장된 선택.", + "properties": { + "status": { + "enum": [ + "selected", + "not_selected", + "unavailable" + ], + "title": "Status", + "type": "string" + }, + "project": { + "anyOf": [ + { + "$ref": "#/$defs/SelectedProject" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "ProjectSelectionContext", + "type": "object" + }, + "SelectedProject": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected" + ], + "title": "Selection Source", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name", + "selection_source" + ], + "title": "SelectedProject", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "ok": { + "title": "Ok", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "$ref": "#/$defs/ConversationDispatchResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "error": { + "anyOf": [ + { + "$ref": "#/$defs/ErrorDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "project_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectExecutionContext" + }, + { + "type": "null" + } + ], + "default": null + }, + "selection_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectSelectionContext" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ok" + ], + "title": "ResultEnvelope[ConversationDispatchResult]", + "type": "object" + } + }, + { + "name": "continue_ennoia_conversation", + "inputSchema": { + "$defs": { + "ConversationContinueRequest": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "maxLength": 128, + "minLength": 1, + "title": "Conversation Id", + "type": "string" + }, + "message": { + "maxLength": 20000, + "minLength": 1, + "title": "Message", + "type": "string" + }, + "wait_timeout_seconds": { + "default": 30, + "maximum": 120, + "minimum": 0, + "title": "Wait Timeout Seconds", + "type": "integer" + } + }, + "required": [ + "conversation_id", + "message" + ], + "title": "ConversationContinueRequest", + "type": "object" + } + }, + "properties": { + "request": { + "$ref": "#/$defs/ConversationContinueRequest" + } + }, + "required": [ + "request" + ], + "title": "continue_ennoia_conversationArguments", + "type": "object" + }, + "outputSchema": { + "$defs": { + "ConversationDispatchResult": { + "additionalProperties": false, + "properties": { + "status": { + "enum": [ + "completed", + "pending" + ], + "title": "Status", + "type": "string" + }, + "conversation_id": { + "title": "Conversation Id", + "type": "string" + }, + "recovery_action": { + "anyOf": [ + { + "enum": [ + "check_acceptance", + "read_result", + "reauthenticate_then_read", + "request_access_then_read" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Recovery Action" + }, + "output": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output" + }, + "messages": { + "default": [], + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Messages", + "type": "array" + }, + "selected_app_ids": { + "default": [], + "items": { + "type": "string" + }, + "title": "Selected App Ids", + "type": "array" + }, + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "truncated": { + "default": false, + "title": "Truncated", + "type": "boolean" + }, + "has_more": { + "default": false, + "title": "Has More", + "type": "boolean" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + } + }, + "required": [ + "status", + "conversation_id", + "group_code", + "project_code" + ], + "title": "ConversationDispatchResult", + "type": "object" + }, + "ErrorDetail": { + "additionalProperties": false, + "description": "외부에 노출할 수 있는 allowlist error detail.", + "properties": { + "code": { + "title": "Code", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Id" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repair Hint" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "details": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Details" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, + "ProjectContextTarget": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name" + ], + "title": "ProjectContextTarget", + "type": "object" + }, + "ProjectExecutionContext": { + "additionalProperties": false, + "properties": { + "scope": { + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected", + "specified", + "all" + ], + "title": "Selection Source", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/$defs/ProjectContextTarget" + }, + "title": "Projects", + "type": "array" + }, + "requires_confirmation": { + "title": "Requires Confirmation", + "type": "boolean" + }, + "notice": { + "title": "Notice", + "type": "string" + } + }, + "required": [ + "scope", + "selection_source", + "projects", + "requires_confirmation", + "notice" + ], + "title": "ProjectExecutionContext", + "type": "object" + }, + "ProjectSelectionContext": { + "additionalProperties": false, + "description": "실제 실행 대상과 구분되는 요청 시점의 저장된 선택.", + "properties": { + "status": { + "enum": [ + "selected", + "not_selected", + "unavailable" + ], + "title": "Status", + "type": "string" + }, + "project": { + "anyOf": [ + { + "$ref": "#/$defs/SelectedProject" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "ProjectSelectionContext", + "type": "object" + }, + "SelectedProject": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected" + ], + "title": "Selection Source", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name", + "selection_source" + ], + "title": "SelectedProject", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "ok": { + "title": "Ok", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "$ref": "#/$defs/ConversationDispatchResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "error": { + "anyOf": [ + { + "$ref": "#/$defs/ErrorDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "project_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectExecutionContext" + }, + { + "type": "null" + } + ], + "default": null + }, + "selection_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectSelectionContext" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ok" + ], + "title": "ResultEnvelope[ConversationDispatchResult]", + "type": "object" + } + }, + { + "name": "get_ennoia_conversation", + "inputSchema": { + "$defs": { + "ConversationGetRequest": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "maxLength": 128, + "minLength": 1, + "title": "Conversation Id", + "type": "string" + }, + "view": { + "default": "legacy", + "enum": [ + "legacy", + "summary", + "messages" + ], + "title": "View", + "type": "string" + }, + "limit": { + "default": 10, + "maximum": 50, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "cursor": { + "anyOf": [ + { + "maxLength": 128, + "pattern": "^[a-f0-9]{32}\\.[0-9]{1,7}\\.[a-f0-9]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cursor" + } + }, + "required": [ + "conversation_id" + ], + "title": "ConversationGetRequest", + "type": "object" + } + }, + "properties": { + "request": { + "$ref": "#/$defs/ConversationGetRequest" + } + }, + "required": [ + "request" + ], + "title": "get_ennoia_conversationArguments", + "type": "object" + }, + "outputSchema": { + "$defs": { + "ConversationDetail": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "title": "Conversation Id", + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Updated At" + }, + "selected_app_ids": { + "default": [], + "items": { + "type": "string" + }, + "title": "Selected App Ids", + "type": "array" + }, + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "completed": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed" + }, + "messages": { + "default": [], + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Messages", + "type": "array" + } + }, + "required": [ + "conversation_id", + "group_code", + "project_code" + ], + "title": "ConversationDetail", + "type": "object" + }, + "ConversationProjection": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "title": "Conversation Id", + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Title" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Updated At" + }, + "selected_app_ids": { + "default": [], + "items": { + "type": "string" + }, + "title": "Selected App Ids", + "type": "array" + }, + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "completed": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Completed" + }, + "messages": { + "default": [], + "description": "일반 메시지는 role/content와 구조화된 metadata를 보존한다. 긴 필드만 fragment를 사용한다. message_index별로 묶고 fragment.field별 offset(Unicode 문자) 순서로 연결한다. encoding=text는 content, json은 fragment.value를 연결 후 JSON decode한다. complete=true는 필드 마지막 조각이다. fragment 없는 필드는 그대로 보존한다. message_index는 snapshot 내 최신순 위치이며 inactive 버전도 포함한다.", + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Messages", + "type": "array" + }, + "view": { + "enum": [ + "summary", + "messages" + ], + "title": "View", + "type": "string" + }, + "execution_status": { + "enum": [ + "pending", + "completed", + "awaiting_input", + "failed", + "unknown" + ], + "title": "Execution Status", + "type": "string" + }, + "latest_answer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Latest Answer" + }, + "latest_message_index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Latest Message Index" + }, + "display_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Display Title" + }, + "has_more": { + "default": false, + "title": "Has More", + "type": "boolean" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Cursor" + }, + "truncated": { + "default": false, + "title": "Truncated", + "type": "boolean" + } + }, + "required": [ + "conversation_id", + "group_code", + "project_code", + "view", + "execution_status" + ], + "title": "ConversationProjection", + "type": "object" + }, + "ErrorDetail": { + "additionalProperties": false, + "description": "외부에 노출할 수 있는 allowlist error detail.", + "properties": { + "code": { + "title": "Code", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Id" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repair Hint" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "details": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Details" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, + "ProjectContextTarget": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name" + ], + "title": "ProjectContextTarget", + "type": "object" + }, + "ProjectExecutionContext": { + "additionalProperties": false, + "properties": { + "scope": { + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected", + "specified", + "all" + ], + "title": "Selection Source", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/$defs/ProjectContextTarget" + }, + "title": "Projects", + "type": "array" + }, + "requires_confirmation": { + "title": "Requires Confirmation", + "type": "boolean" + }, + "notice": { + "title": "Notice", + "type": "string" + } + }, + "required": [ + "scope", + "selection_source", + "projects", + "requires_confirmation", + "notice" + ], + "title": "ProjectExecutionContext", + "type": "object" + }, + "ProjectSelectionContext": { + "additionalProperties": false, + "description": "실제 실행 대상과 구분되는 요청 시점의 저장된 선택.", + "properties": { + "status": { + "enum": [ + "selected", + "not_selected", + "unavailable" + ], + "title": "Status", + "type": "string" + }, + "project": { + "anyOf": [ + { + "$ref": "#/$defs/SelectedProject" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "ProjectSelectionContext", + "type": "object" + }, + "SelectedProject": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected" + ], + "title": "Selection Source", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name", + "selection_source" + ], + "title": "SelectedProject", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "ok": { + "title": "Ok", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "$ref": "#/$defs/ConversationProjection" + }, + { + "$ref": "#/$defs/ConversationDetail" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Data" + }, + "error": { + "anyOf": [ + { + "$ref": "#/$defs/ErrorDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "project_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectExecutionContext" + }, + { + "type": "null" + } + ], + "default": null + }, + "selection_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectSelectionContext" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ok" + ], + "title": "ResultEnvelope[Union[ConversationProjection, ConversationDetail]]", + "type": "object" + } + }, + { + "name": "chat_with_ennoia_app", + "inputSchema": { + "$defs": { + "EnnoiaAppChatRequest": { + "additionalProperties": false, + "properties": { + "project_scope": { + "$ref": "#/$defs/ProjectSelector" + }, + "assistant_hash": { + "maxLength": 128, + "minLength": 1, + "title": "Assistant Hash", + "type": "string" + }, + "conversation_id": { + "anyOf": [ + { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Conversation Id" + }, + "message": { + "maxLength": 20000, + "minLength": 1, + "title": "Message", + "type": "string" + }, + "wait_timeout_seconds": { + "default": 30, + "maximum": 120, + "minimum": 0, + "title": "Wait Timeout Seconds", + "type": "integer" + } + }, + "required": [ + "assistant_hash", + "message" + ], + "title": "EnnoiaAppChatRequest", + "type": "object" + }, + "ProjectSelector": { + "additionalProperties": false, + "properties": { + "scope": { + "default": "current", + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "group_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Group Code" + }, + "project_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Project Code" + } + }, + "title": "ProjectSelector", + "type": "object" + } + }, + "properties": { + "request": { + "$ref": "#/$defs/EnnoiaAppChatRequest" + } + }, + "required": [ + "request" + ], + "title": "chat_with_ennoia_appArguments", + "type": "object" + }, + "outputSchema": { + "$defs": { + "EnnoiaAppChatResult": { + "additionalProperties": false, + "properties": { + "status": { + "enum": [ + "completed", + "pending" + ], + "title": "Status", + "type": "string" + }, + "assistant_hash": { + "title": "Assistant Hash", + "type": "string" + }, + "conversation_id": { + "title": "Conversation Id", + "type": "string" + }, + "recovery_action": { + "anyOf": [ + { + "enum": [ + "check_acceptance", + "read_result", + "reauthenticate_then_read", + "request_access_then_read" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Recovery Action" + }, + "output": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output" + }, + "messages": { + "default": [], + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Messages", + "type": "array" + }, + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "truncated": { + "default": false, + "title": "Truncated", + "type": "boolean" + }, + "has_more": { + "default": false, + "title": "Has More", + "type": "boolean" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + } + }, + "required": [ + "status", + "assistant_hash", + "conversation_id", + "group_code", + "project_code" + ], + "title": "EnnoiaAppChatResult", + "type": "object" + }, + "ErrorDetail": { + "additionalProperties": false, + "description": "외부에 노출할 수 있는 allowlist error detail.", + "properties": { + "code": { + "title": "Code", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Id" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repair Hint" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "details": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Details" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, + "ProjectContextTarget": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name" + ], + "title": "ProjectContextTarget", + "type": "object" + }, + "ProjectExecutionContext": { + "additionalProperties": false, + "properties": { + "scope": { + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected", + "specified", + "all" + ], + "title": "Selection Source", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/$defs/ProjectContextTarget" + }, + "title": "Projects", + "type": "array" + }, + "requires_confirmation": { + "title": "Requires Confirmation", + "type": "boolean" + }, + "notice": { + "title": "Notice", + "type": "string" + } + }, + "required": [ + "scope", + "selection_source", + "projects", + "requires_confirmation", + "notice" + ], + "title": "ProjectExecutionContext", + "type": "object" + }, + "ProjectSelectionContext": { + "additionalProperties": false, + "description": "실제 실행 대상과 구분되는 요청 시점의 저장된 선택.", + "properties": { + "status": { + "enum": [ + "selected", + "not_selected", + "unavailable" + ], + "title": "Status", + "type": "string" + }, + "project": { + "anyOf": [ + { + "$ref": "#/$defs/SelectedProject" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "ProjectSelectionContext", + "type": "object" + }, + "SelectedProject": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected" + ], + "title": "Selection Source", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name", + "selection_source" + ], + "title": "SelectedProject", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "ok": { + "title": "Ok", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "$ref": "#/$defs/EnnoiaAppChatResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "error": { + "anyOf": [ + { + "$ref": "#/$defs/ErrorDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "project_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectExecutionContext" + }, + { + "type": "null" + } + ], + "default": null + }, + "selection_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectSelectionContext" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ok" + ], + "title": "ResultEnvelope[EnnoiaAppChatResult]", + "type": "object" + } + }, + { + "name": "get_multi_agent_node_prompts", + "inputSchema": { + "$defs": { + "MultiAgentSelection": { + "additionalProperties": false, + "properties": { + "project_scope": { + "$ref": "#/$defs/ProjectSelector" + }, + "multi_agent_id": { + "maxLength": 128, + "minLength": 1, + "title": "Multi Agent Id", + "type": "string" + }, + "stage": { + "anyOf": [ + { + "enum": [ + "D", + "P" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stage" + }, + "deploy_version": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Deploy Version" + } + }, + "required": [ + "multi_agent_id" + ], + "title": "MultiAgentSelection", + "type": "object" + }, + "ProjectSelector": { + "additionalProperties": false, + "properties": { + "scope": { + "default": "current", + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "group_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Group Code" + }, + "project_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Project Code" + } + }, + "title": "ProjectSelector", + "type": "object" + } + }, + "properties": { + "request": { + "$ref": "#/$defs/MultiAgentSelection" + } + }, + "required": [ + "request" + ], + "title": "get_multi_agent_node_promptsArguments", + "type": "object" + }, + "outputSchema": { + "$defs": { + "ErrorDetail": { + "additionalProperties": false, + "description": "외부에 노출할 수 있는 allowlist error detail.", + "properties": { + "code": { + "title": "Code", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Id" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repair Hint" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "details": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Details" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, + "NodePrompt": { + "additionalProperties": false, + "properties": { + "node_id": { + "title": "Node Id", + "type": "string" + }, + "node_type": { + "title": "Node Type", + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "system_prompt": { + "title": "System Prompt", + "type": "string" + }, + "legacy_reference": { + "default": false, + "title": "Legacy Reference", + "type": "boolean" + }, + "editable": { + "default": true, + "title": "Editable", + "type": "boolean" + }, + "unsupported_reason": { + "anyOf": [ + { + "enum": [ + "legacy_dynamic_date", + "legacy_model_profile_mismatch" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Unsupported Reason" + } + }, + "required": [ + "node_id", + "node_type", + "system_prompt" + ], + "title": "NodePrompt", + "type": "object" + }, + "NodePromptsResult": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "multi_agent_id": { + "title": "Multi Agent Id", + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "stage": { + "enum": [ + "D", + "P" + ], + "title": "Stage", + "type": "string" + }, + "deploy_version": { + "title": "Deploy Version", + "type": "integer" + }, + "canvas_revision": { + "pattern": "^[a-f0-9]{64}$", + "title": "Canvas Revision", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/NodePrompt" + }, + "title": "Nodes", + "type": "array" + } + }, + "required": [ + "group_code", + "project_code", + "multi_agent_id", + "stage", + "deploy_version", + "canvas_revision", + "nodes" + ], + "title": "NodePromptsResult", + "type": "object" + }, + "ProjectContextTarget": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name" + ], + "title": "ProjectContextTarget", + "type": "object" + }, + "ProjectExecutionContext": { + "additionalProperties": false, + "properties": { + "scope": { + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected", + "specified", + "all" + ], + "title": "Selection Source", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/$defs/ProjectContextTarget" + }, + "title": "Projects", + "type": "array" + }, + "requires_confirmation": { + "title": "Requires Confirmation", + "type": "boolean" + }, + "notice": { + "title": "Notice", + "type": "string" + } + }, + "required": [ + "scope", + "selection_source", + "projects", + "requires_confirmation", + "notice" + ], + "title": "ProjectExecutionContext", + "type": "object" + }, + "ProjectSelectionContext": { + "additionalProperties": false, + "description": "실제 실행 대상과 구분되는 요청 시점의 저장된 선택.", + "properties": { + "status": { + "enum": [ + "selected", + "not_selected", + "unavailable" + ], + "title": "Status", + "type": "string" + }, + "project": { + "anyOf": [ + { + "$ref": "#/$defs/SelectedProject" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "ProjectSelectionContext", + "type": "object" + }, + "SelectedProject": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected" + ], + "title": "Selection Source", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name", + "selection_source" + ], + "title": "SelectedProject", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "ok": { + "title": "Ok", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "$ref": "#/$defs/NodePromptsResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "error": { + "anyOf": [ + { + "$ref": "#/$defs/ErrorDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "project_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectExecutionContext" + }, + { + "type": "null" + } + ], + "default": null + }, + "selection_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectSelectionContext" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ok" + ], + "title": "ResultEnvelope[NodePromptsResult]", + "type": "object" + } + }, + { + "name": "update_multi_agent_node_prompts", + "inputSchema": { + "$defs": { + "NodePromptUpdate": { + "additionalProperties": false, + "properties": { + "node_id": { + "maxLength": 128, + "minLength": 1, + "title": "Node Id", + "type": "string" + }, + "system_prompt": { + "maxLength": 100000, + "title": "System Prompt", + "type": "string" + } + }, + "required": [ + "node_id", + "system_prompt" + ], + "title": "NodePromptUpdate", + "type": "object" + }, + "ProjectSelector": { + "additionalProperties": false, + "properties": { + "scope": { + "default": "current", + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "group_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Group Code" + }, + "project_code": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Project Code" + } + }, + "title": "ProjectSelector", + "type": "object" + }, + "UpdateNodePromptsRequest": { + "additionalProperties": false, + "properties": { + "project_scope": { + "$ref": "#/$defs/ProjectSelector" + }, + "multi_agent_id": { + "maxLength": 128, + "minLength": 1, + "title": "Multi Agent Id", + "type": "string" + }, + "stage": { + "anyOf": [ + { + "enum": [ + "D", + "P" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stage" + }, + "deploy_version": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Deploy Version" + }, + "operation_id": { + "format": "uuid4", + "title": "Operation Id", + "type": "string" + }, + "expected_revision": { + "pattern": "^[a-f0-9]{64}$", + "title": "Expected Revision", + "type": "string" + }, + "updates": { + "items": { + "$ref": "#/$defs/NodePromptUpdate" + }, + "maxItems": 100, + "minItems": 1, + "title": "Updates", + "type": "array" + } + }, + "required": [ + "multi_agent_id", + "operation_id", + "expected_revision", + "updates" + ], + "title": "UpdateNodePromptsRequest", + "type": "object" + } + }, + "properties": { + "request": { + "$ref": "#/$defs/UpdateNodePromptsRequest" + } + }, + "required": [ + "request" + ], + "title": "update_multi_agent_node_promptsArguments", + "type": "object" + }, + "outputSchema": { + "$defs": { + "ErrorDetail": { + "additionalProperties": false, + "description": "외부에 노출할 수 있는 allowlist error detail.", + "properties": { + "code": { + "title": "Code", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Node Id" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repair Hint" + }, + "next_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Next Action" + }, + "retryable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retryable" + }, + "details": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Details" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, + "NodePromptSaveResult": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "multi_agent_id": { + "title": "Multi Agent Id", + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "stage": { + "const": "D", + "default": "D", + "title": "Stage", + "type": "string" + }, + "deploy_version": { + "title": "Deploy Version", + "type": "integer" + }, + "canvas_revision": { + "pattern": "^[a-f0-9]{64}$", + "title": "Canvas Revision", + "type": "string" + }, + "changed_node_ids": { + "items": { + "type": "string" + }, + "title": "Changed Node Ids", + "type": "array" + }, + "migrated_node_ids": { + "default": [], + "items": { + "type": "string" + }, + "title": "Migrated Node Ids", + "type": "array" + } + }, + "required": [ + "group_code", + "project_code", + "multi_agent_id", + "deploy_version", + "canvas_revision", + "changed_node_ids" + ], + "title": "NodePromptSaveResult", + "type": "object" + }, + "ProjectContextTarget": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name" + ], + "title": "ProjectContextTarget", + "type": "object" + }, + "ProjectExecutionContext": { + "additionalProperties": false, + "properties": { + "scope": { + "enum": [ + "current", + "specified", + "all" + ], + "title": "Scope", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected", + "specified", + "all" + ], + "title": "Selection Source", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/$defs/ProjectContextTarget" + }, + "title": "Projects", + "type": "array" + }, + "requires_confirmation": { + "title": "Requires Confirmation", + "type": "boolean" + }, + "notice": { + "title": "Notice", + "type": "string" + } + }, + "required": [ + "scope", + "selection_source", + "projects", + "requires_confirmation", + "notice" + ], + "title": "ProjectExecutionContext", + "type": "object" + }, + "ProjectSelectionContext": { + "additionalProperties": false, + "description": "실제 실행 대상과 구분되는 요청 시점의 저장된 선택.", + "properties": { + "status": { + "enum": [ + "selected", + "not_selected", + "unavailable" + ], + "title": "Status", + "type": "string" + }, + "project": { + "anyOf": [ + { + "$ref": "#/$defs/SelectedProject" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "status" + ], + "title": "ProjectSelectionContext", + "type": "object" + }, + "SelectedProject": { + "additionalProperties": false, + "properties": { + "group_code": { + "title": "Group Code", + "type": "string" + }, + "group_name": { + "title": "Group Name", + "type": "string" + }, + "project_code": { + "title": "Project Code", + "type": "string" + }, + "project_name": { + "title": "Project Name", + "type": "string" + }, + "selection_source": { + "enum": [ + "auto_selected", + "user_selected" + ], + "title": "Selection Source", + "type": "string" + } + }, + "required": [ + "group_code", + "group_name", + "project_code", + "project_name", + "selection_source" + ], + "title": "SelectedProject", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "ok": { + "title": "Ok", + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "$ref": "#/$defs/NodePromptSaveResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "error": { + "anyOf": [ + { + "$ref": "#/$defs/ErrorDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "project_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectExecutionContext" + }, + { + "type": "null" + } + ], + "default": null + }, + "selection_context": { + "anyOf": [ + { + "$ref": "#/$defs/ProjectSelectionContext" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "ok" + ], + "title": "ResultEnvelope[NodePromptSaveResult]", + "type": "object" + } + } + ] +} diff --git a/tests/fixtures/ennoia-tools.json b/tests/fixtures/ennoia-tools.json index 09c61a4..7daa4d7 100644 --- a/tests/fixtures/ennoia-tools.json +++ b/tests/fixtures/ennoia-tools.json @@ -1,6 +1,6 @@ { "source_repository": "wanteddev/ennoia-mcp-server", - "source_revision": "837874039236000887a4f9a895105f0b49b6e2e6", + "source_revision": "b3ad6455bd7423dce9e982fcd700832aaa8802e8", "tools": [ "add_ennoia_mcp_server", "chat_with_ennoia_app", diff --git a/tests/test_contracts.py b/tests/test_contracts.py new file mode 100644 index 0000000..c4f3708 --- /dev/null +++ b/tests/test_contracts.py @@ -0,0 +1,58 @@ +"""도구명은 같아도 Skill이 사용하는 입력·출력 계약이 달라지는 회귀를 검증한다.""" + +import copy +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from scripts.validate_contracts import validate_contracts + +ROOT = Path(__file__).resolve().parents[1] + + +class ContractTests(unittest.TestCase): + def setUp(self): + fixtures = ROOT / "tests/fixtures" + self.catalog = json.loads((fixtures / "ennoia-tool-contracts.json").read_text()) + self.cases = json.loads((fixtures / "ennoia-contract-cases.json").read_text()) + + def test_current_catalog_accepts_supported_and_rejects_unsupported_examples(self): + self.assertEqual(validate_contracts(self.catalog, self.cases), []) + + def test_unknown_input_fields_cannot_be_silently_accepted(self): + tool = next(t for t in self.catalog["tools"] if t["name"] == "continue_ennoia_conversation") + tool["inputSchema"]["$defs"]["ConversationContinueRequest"]["additionalProperties"] = True + self.assertTrue(validate_contracts(self.catalog, self.cases)) + + def test_external_schema_reference_is_rejected_without_network(self): + self.catalog["tools"][0]["inputSchema"] = {"$ref": "https://example.invalid/schema.json"} + self.assertTrue(validate_contracts(self.catalog, self.cases)) + + def test_missing_local_reference_is_a_contract_error(self): + self.catalog["tools"][0]["inputSchema"] = {"$ref": "#/$defs/Missing"} + self.assertTrue(validate_contracts(self.catalog, self.cases)) + + def test_duplicate_tool_names_are_rejected(self): + self.catalog["tools"].append(copy.deepcopy(self.catalog["tools"][0])) + self.assertTrue(validate_contracts(self.catalog, self.cases)) + + def test_empty_case_set_cannot_skip_contract_checks(self): + self.assertTrue(validate_contracts(self.catalog, [])) + + def test_cli_accepts_json_rpc_catalog_and_rejects_missing_tools(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "catalog.json" + for catalog, success in ((self.catalog, True), ({"tools": []}, False)): + path.write_text(json.dumps({"jsonrpc": "2.0", "id": 1, "result": catalog})) + result = subprocess.run( + [sys.executable, str(ROOT / "scripts/validate_contracts.py"), "--catalog", str(path)], + cwd=directory, capture_output=True, text=True, + ) + self.assertEqual(result.returncode == 0, success, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_validation.py b/tests/test_validation.py index 154e729..1728f19 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -3,6 +3,8 @@ import importlib.util import json import shutil +import subprocess +import sys import tempfile import unittest from pathlib import Path @@ -25,6 +27,45 @@ def setUp(self): def test_valid_repository(self): self.assertEqual(self.validate(self.root), []) + def test_validator_runs_as_a_script_outside_repo(self): + result = subprocess.run( + [sys.executable, str(self.root / "scripts/validate.py")], + cwd=self.temp.name, capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def mutate_contract(self, change): + path = self.root / "tests/fixtures/ennoia-tool-contracts.json" + catalog = json.loads(path.read_text()) + change(catalog) + path.write_text(json.dumps(catalog)) + + def test_tool_name_alone_does_not_satisfy_prompt_editing_contract(self): + def remove_revision(catalog): + tool = next(t for t in catalog["tools"] if t["name"] == "update_multi_agent_node_prompts") + request = tool["inputSchema"]["$defs"]["UpdateNodePromptsRequest"] + request["properties"].pop("expected_revision") + request["required"].remove("expected_revision") + + self.mutate_contract(remove_revision) + self.assertTrue(any("MCP contract" in e for e in self.validate(self.root))) + + def test_projection_result_without_awaiting_input_is_rejected(self): + def remove_status(catalog): + tool = next(t for t in catalog["tools"] if t["name"] == "get_ennoia_conversation") + tool["outputSchema"]["$defs"]["ConversationProjection"]["properties"]["execution_status"]["enum"].remove("awaiting_input") + + self.mutate_contract(remove_status) + self.assertTrue(any("MCP contract" in e for e in self.validate(self.root))) + + def test_contract_revisions_must_match_tool_inventory(self): + self.mutate_contract(lambda catalog: catalog.update(source_revision="0" * 40)) + self.assertTrue(any("MCP contract" in e for e in self.validate(self.root))) + + def test_missing_tool_schema_is_rejected(self): + self.mutate_contract(lambda catalog: catalog["tools"].pop()) + self.assertTrue(any("MCP contract" in e for e in self.validate(self.root))) + def test_local_uploader_is_packaged_and_contract_changes_are_rejected(self): plugin = self.root / "plugins/ennoia" self.assertTrue((plugin / "mcp/file-uploader.mjs").is_file())