> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daven.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 에이전트 스펙

> 코딩 에이전트용 결정적 계약 — 추측 없이 구현

이 페이지는 **사람이 읽는 Quickstart가 아니라 에이전트 구현 스펙**입니다. 우선 `@davenai/mcp` 도구 \*\*`daven_guidelines`\*\*를 호출하세요 (`topic=edu_app`, 학생 제출 시 `topic=submission`). 모호하면 추측하지 말고 그 출력을 따르세요.

원문: [`llms.txt`](/edu/llms.txt) · Proxy: [`openapi.yaml`](/edu/openapi.yaml) · 제출: [`lms-openapi.yaml`](/edu/lms-openapi.yaml) · [`activity-submission.schema.json`](/edu/activity-submission.schema.json)

이미 있는 앱을 교실에 올릴 때는 [기존 앱을 Daven에 얹기](/edu/hosted-app)의 불변 조건을 지키세요. 학습 화면을 Hub나 Vibe로 바꾸지 마세요.

## MUST

1. HTTPS(또는 local) 웹앱을 호스팅한다. Hub **시작**은 **새 탭**으로 연다.
2. TypeScript 앱은 `@davenai/sdk/edu`를 우선 사용한다. SDK가 launch의 `session`, `proxy`를 읽고 주소에서 제거한다.
3. Daven 호출은 SDK 또는 `{proxy}` base만 사용한다.
4. raw fetch fallback은 매 요청에 `Authorization: Bearer {session}` **그리고** `X-Edu-Session: {session}`을 넣는다.
5. 응답 envelope를 파싱한다: `status === "success" | "error"`.
6. 프론트에 API 키 / MCP 시크릿을 넣지 않는다.
7. SDK 기본 신뢰 proxy만 사용한다. custom proxy가 필요하면 앱의 정적 설정으로만 `additionalTrustedProxyUrls`에 추가하고 launch query 값을 그대로 allowlist하지 않는다.
8. `shouldRelaunchEducationApp(error)`가 `true`이면 저장한 작업은 유지하고 Hub에서 활동을 다시 열도록 안내한다.

## MUST NOT

1. 브라우저에서 `daven.ai` / `app.daven.ai`를 직접 호출하지 않는다.
2. 추가 OAuth 팝업을 Edu 기본 플로우에 필수로 두지 않는다.
3. 학생 원문을 AI 결과로 자동 덮어쓰지 않는다.

## Defaults

| Key                 | Value                                       |
| ------------------- | ------------------------------------------- |
| `proxy` (prod)      | `https://api.edu.daven.ai/api/v1/mcp-proxy` |
| `proxy` (local Hub) | `http://localhost:3001/api/v1/mcp-proxy`    |
| Content-Type        | `application/json`                          |

## Session v2 (deterministic)

새 세션은 서명된 HS256 JWT이며 `version: 2`, `purpose`, `actorId`를 포함하고 `sub === actorId`입니다.

| `purpose`          | `actor.role` | `studentId` | 제출 |
| ------------------ | ------------ | ----------- | -- |
| `student_activity` | `student`    | 필수 (본인)     | 가능 |
| `student_preview`  | `teacher`    | 필수          | 불가 |
| `teacher_manage`   | `teacher`    | 선택 / `null` | 불가 |

`actor.id`는 인증된 Hub 프로필이고 `studentId`는 작업 대상 학생입니다. 앱은 JWT를 직접 신뢰하지 말고 `GET /api/session/context`를 사용합니다. context의 학생·교사 `actorId`를 앱 사용자 매핑 키로 사용합니다. 이름이나 학생 코드로 매칭하지 마세요. `teacher_manage`만 `GET /api/session/roster`를 사용할 수 있으며, 응답에는 이메일·학생 코드·잔액·payer가 없습니다. 앱 전용 교사 작업 공간을 제공할 때 Master 승인 카탈로그의 `supportsTeacherManage`를 켜면 Hub가 이 목적의 실행 버튼을 표시합니다. 서버는 전환을 위해 서명된 v1 JWT를 정규화하지만 서명 없는 base64 세션은 거부합니다.

## Submission v2 (deterministic)

* 최종 제출 POST는 `student_activity` Edu 세션만 사용할 수 있습니다.
* `externalArtifactId`는 선택이며 `[A-Za-z0-9._:-]` 문자 1–128자입니다.
* 생략하면 `__default__`가 되어 기존 앱처럼 기본 행 하나를 갱신합니다.
* Identity는 `(activityId, studentId, appSlug, externalArtifactId)`입니다. ID별로 여러 artifact와 재제출을 지원합니다.
* 제출 URL은 공개 영구 HTTPS만 허용합니다. localhost, IP literal, 내부 hostname, credential 포함 URL은 금지합니다.
* 제출 목록 GET은 Edu 세션 API가 아닙니다. 일반 Hub 교사 bearer와 해당 교실 소유권이 필요하며 익명 조회는 금지합니다.

## Endpoints (complete)

구현 시 이 표만 있으면 됩니다. 앞의 proxy 경로는 launch의 `{proxy}`를 base로 사용합니다. 학생 제출은 SDK가 `{proxy}`에서 계산한 Edu API base를 사용합니다. body 필드 상세는 OpenAPI와 제출 schema를 따릅니다.

| Op             | Method | Path                                                                                |
| -------------- | ------ | ----------------------------------------------------------------------------------- |
| health         | GET    | `/health`                                                                           |
| sessionContext | GET    | `/api/session/context`                                                              |
| sessionRoster  | GET    | `/api/session/roster`                                                               |
| account        | GET    | `/api/account`                                                                      |
| quote          | POST   | `/api/quote` (`{ media_type }` only — school-pool credits, not Daven MCP slug/code) |
| textCreate     | POST   | `/api/text/create`                                                                  |
| uploadUrl      | POST   | `/api/upload-url`                                                                   |
| mediaCreate    | POST   | `/api/media/create`                                                                 |
| mediaGet       | POST   | `/api/media/get`                                                                    |
| catalog        | GET    | `/api/catalog`                                                                      |
| studentSubmit  | POST   | `{eduApi}/activities/{activityId}/submissions`                                      |

## Canonical SDK client

```ts theme={null}
import { createDavenEduClient } from "@davenai/sdk/edu";

const daven = createDavenEduClient({
  expectedAppSlug: "your-registered-app-slug",
});
const context = await daven.session.getContext();
const account = await daven.billing.getAccount();
const quote = await daven.billing.quote({ mediaType: "image" });
const image = await daven.ai.image.generate(
  { prompt: "A reading passport stamp" },
  { idempotencyKey: crypto.randomUUID() },
);
```

Node BFF에서는 명시적인 `session`, `proxy`, `expectedAppSlug`로 `@davenai/sdk/server`를 사용합니다. `expectedAppSlug`가 session context와 다르면 SDK는 요청을 중단합니다. SDK를 사용할 수 없는 런타임만 [`llms.txt`](/edu/llms.txt)의 raw fetch fallback을 구현합니다.

## Acceptance (binary)

에이전트는 아래가 전부 true일 때만 “연동 완료”라고 보고합니다.

* [ ] `daven.billing.getAccount()` 성공
* [ ] 세션 헤더 제거 시 → 401
* [ ] SDK AI 호출은 사용자 동작마다 operation ID를 생성하고 같은 요청 재시도에만 재사용
* [ ] 최종 제출은 `student_activity`에서만 실행하고 artifact별 `externalArtifactId`를 재사용
* [ ] session 만료/401에서 저장 작업을 유지한 재실행 UI 표시
* [ ] DevTools Network에 Edu proxy host만 존재
* [ ] 새 탭(최상위 페이지)에서 핵심 플로우 완료 가능
* [ ] 학생 Start로 텍스트·이미지가 각 1번 나온 뒤에만 카탈로그 Approve. 함정은 [hosted-app](/edu/hosted-app#image-studio에서-깨진-것)

## Reference paths

* App: `dv_mcp/apps/sihwa_project`
* Proxy: `dv-edu/apps/api/src/mcp-proxy/mcp-proxy.controller.ts`
