---
title: Format 패키지 편집하기
description: GitHub Contents API를 쓰듯이, /v1로 내 Format의 파일을 읽고 커밋하세요.
---

모든 커스텀 [Format](/formats)은 작은 파일 패키지입니다 — 진입 파일 `SKILL.md`와, 선택적으로
`references/` 아래의 참고 문서들. 그리고 그 패키지에는 실제 히스토리가 남습니다. Contents API를
쓰면 API 키로 그 파일들을 읽고 변경을 커밋할 수 있습니다. 코딩 에이전트가 이미 저장소를 편집하는
방식 그대로 Format을 편집할 수 있다는 뜻입니다.

모양은 의도적으로 GitHub Contents API와 같습니다. `gh api repos/{owner}/{repo}/contents/{path}`를
안다면 이것도 아는 것입니다:

```
POST   /v1/formats                                   # Format 만들기
GET    /v1/formats/{handle}/{slug}/contents
GET    /v1/formats/{handle}/{slug}/contents/{path}
PUT    /v1/formats/{handle}/{slug}/contents          # 여러 파일, 커밋 하나
PUT    /v1/formats/{handle}/{slug}/contents/{path}
DELETE /v1/formats/{handle}/{slug}/contents/{path}
```

`{handle}/{slug}`는 Format을 호출할 때 쓰는 바로 그 주소입니다. 읽기는 `formats:read`, 쓰기는
`formats:write`가 필요하고, 커밋의 author는 항상 키의 소유자입니다 — 바디의 `author`나
`committer`는 무시됩니다.

이것은 실행이 아니라 작성입니다. 패키지를 편집해도 이미 진행 중인 run에는 영향이 없습니다. 각
run은 시작할 때의 패키지를 그대로 읽습니다.

## Format 만들기

`POST /v1/formats`는 GitHub에서 `POST /repos`가 저장소를 여는 것처럼 Format과 그 패키지 저장소를
엽니다. `formats:write`가 필요하고, Format은 **호출한 키의 워크스페이스**에 만들어집니다 — 주소에
`{handle}`이 없으므로 다른 워크스페이스에 만들 방법 자체가 없습니다.

```bash
export SUME_API_KEY=sume_…   # 워크스페이스 키. 절대 커밋하지 마세요

curl -sS -X POST "https://api.dev.sume.com/v1/formats" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "content-type: application/json" \
  -d '{"slug":"plate-shots","title":"Plate shots","description":"스튜디오 플레이트 촬영."}'
```

```json
{
  "data": {
    "id": "skl_…",
    "object": "format",
    "handle": "mobidoo",
    "slug": "plate-shots",
    "version": 1,
    "package_sha": "3f2b1c…",
    "contents_url": "https://api.dev.sume.com/v1/formats/mobidoo/plate-shots/contents",
    "vanity_invoke_url": "https://api.dev.sume.com/v1/formats/mobidoo/plate-shots/runs"
  }
}
```

`auto_init`(기본값 `true`)은 유효한 최소 `SKILL.md`를 커밋합니다. 그래서 만들자마자 아래 엔드포인트로
읽고 쓸 수 있고, 그 파일을 실제 내용으로 교체하는 것이 다음 호출입니다. `false`는 `400`입니다. 모든
Format 패키지에는 `SKILL.md`가 있어야 하므로 빈 Format이라는 것은 존재할 수 없습니다. 바디로 패키지
파일을 받지는 않습니다. 그건 Contents API가 할 일이고, Format의 파일은 누가 썼든 하나의 규칙만
따라야 합니다.

응답의 `package_sha`와 `contents_url`을 보관하세요. 앞의 것은 다음 쓰기의 `If-Match` 전제 조건이고,
뒤의 것은 그 쓰기를 보낼 주소입니다. 워크스페이스에 이미 있는 slug는 덮어쓰지 않고 `409`입니다.

저장소는 카탈로그 행보다 **먼저** 열립니다. 패키지 히스토리에 닿지 못하면 커밋을 아무도 해석할 수
없는 Format이 생기는 대신 `503 format_git_unavailable`로 실패하고 Format은 만들어지지 않습니다.

## 패키지 읽기

```bash
curl -sS "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": [
    { "type": "dir", "name": "references", "path": "references", "sha": "9c1a…", "size": 0 },
    { "type": "file", "name": "SKILL.md", "path": "SKILL.md", "sha": "0ee8…", "size": 4213 }
  ]
}
```

경로 하나를 지정하면 파일 자체가 base64로 돌아옵니다:

```bash
curl -sS "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents/SKILL.md" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": {
    "type": "file",
    "name": "SKILL.md",
    "path": "SKILL.md",
    "sha": "0ee8784832cd08154436f264ddeb38703dc89cd7",
    "size": 4213,
    "encoding": "base64",
    "content": "LS0tCm5hbWU6IGxpdmUtY29tbWVyY2UK…"
  }
}
```

**`sha`를 보관하세요.** 저장된 파일의 git blob sha이고, 모든 쓰기가 요구하는 전제 조건입니다.
디렉터리를 가리키는 경로(`…/contents/references`)는 그 디렉터리의 항목 목록을 돌려줍니다.

## 패키지 전체를 한 번에 읽기

루트를 나열하고 경로마다 다시 요청하면 파일 수만큼 왕복이 생깁니다. 필요한 것이 패키지 전체라면
— 예를 들어 에이전트 컨텍스트에 통째로 올리는 경우 — 한 번의 호출로 받으세요:

```bash
curl -sS "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents?recursive=1" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": [
    {
      "type": "file",
      "name": "SKILL.md",
      "path": "SKILL.md",
      "sha": "0ee8784832cd08154436f264ddeb38703dc89cd7",
      "size": 4213,
      "encoding": "base64",
      "content": "LS0tCm5hbWU6IGxpdmUtY29tbWVyY2UK…"
    },
    {
      "type": "file",
      "name": "plan.md",
      "path": "references/plan.md",
      "sha": "3f7b1c0d9e2a4b6c8d0e1f2a3b4c5d6e7f809a1b",
      "size": 128,
      "encoding": "base64",
      "content": "IyBQbGFuCg=="
    }
  ]
}
```

모든 행은 본문을 포함한 파일이고 `path` 순으로 정렬됩니다. `dir` 행은 없습니다 — 디렉터리는 경로
자체로 드러납니다. `recursive=true`도 됩니다. 그 밖의 값은, 아예 생략한 것과 마찬가지로 위의 기본
루트 목록입니다. 각 행의 `sha`는 쓰기가 요구하는 그 전제 조건이므로, 재귀 읽기 한 번이면 바로
편집을 시작할 수 있습니다.

이것은 목록 조회에만 적용됩니다. `…/contents/{path}`는 이 파라미터가 있든 없든 똑같이 답합니다.

## 변경 커밋하기

`PUT`은 파일 하나를 통째로 쓰고 커밋 하나를 만듭니다. patch가 아니라 replace이므로, 새 본문
전체를 base64로 보내세요.

```bash
curl -sS -X PUT \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents/references/plan.md" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "message": "tighten gate 3",
        "content": "IyBQbGFuCg==",
        "sha": "3f7b1c0d9e2a4b6c8d0e1f2a3b4c5d6e7f809a1b"
      }'
```

```json
{
  "data": {
    "content": { "type": "file", "path": "references/plan.md", "sha": "5a2f…", "size": 7 },
    "commit": {
      "sha": "b41c9a…",
      "message": "tighten gate 3",
      "tree": { "sha": "77de8a…" },
      "parents": [{ "sha": "0a91f2…" }]
    },
    "version": 12
  }
}
```

`commit.tree.sha`는 Format의 새 패키지 identity이고, `version`은 대시보드에 표시되는 카운터입니다.

경로가 이미 있으면 `sha`를 보내고, 새 파일을 만들 때는 생략하세요. 두 에이전트가 *같은 파일*을
편집해도 서로를 조용히 덮어쓸 수 없습니다 — 나중 쪽의 `sha`가 더 이상 맞지 않기 때문입니다. 한
Format의 *서로 다른 파일*을 편집하는 경우는 [`If-Match`](#if-match로-패키지-전체-지키기)를 보세요.

`DELETE`는 `message`와 `sha`를 요구하고, `content: null`로 답합니다:

```bash
curl -sS -X DELETE \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents/references/plan.md" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "drop the old plan", "sha": "5a2f1b3c4d5e6f708192a3b4c5d6e7f809a1b2c3"}'
```

## 여러 파일을 한 번에 커밋하기

폴더를 한 경로씩 편집하면 파일 수만큼 커밋과 왕복이 듭니다. 패키지 루트에 `PUT`하면 —
`{path}` 없이 — `files` 목록을 받아 전부를 커밋 **하나**, `version` 증가 하나, 새 `package_sha`
하나로 씁니다:

```bash
curl -sS -X PUT \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "message": "edit plan + facts",
        "files": [
          { "path": "references/plan.md",  "content": "IyBQbGFuCg==", "sha": "3f7b1c0d9e2a4b6c8d0e1f2a3b4c5d6e7f809a1b" },
          { "path": "references/facts.md", "content": "IyBGYWN0cwo=", "sha": "9b1d0e2f3a4b5c6d7e8f90a1b2c3d4e5f6a7b8c9" },
          { "path": "references/new-note.md", "content": "IyBOZXcK" }
        ]
      }'
```

각 항목은 단일 파일 쓰기 규칙을 그대로 따릅니다. `content`는 base64로 인코딩한 파일 전체이고,
`sha`는 경로가 이미 있으면 필수, 새로 만들 때는 생략합니다. 응답의 `content`는 이 커밋이 쓴
파일들의 목록입니다.

**`files`는 패키지가 아니라 변경 집합입니다.** Format이 갖고 있지만 이 바디가 지목하지 않은
경로는 그대로 유지됩니다 — 12개 중 2개를 고치면서 나머지 10개를 잃을 일이 없습니다. 그래서 삭제는
여기서 표현할 수 없습니다. 파일 제거는 `DELETE …/contents/{path}`로 하세요. 삭제는 항상 요청한
결과여야 하고, 말하는 걸 잊어서 생기는 결과여서는 안 되기 때문입니다.

항목 중 하나라도 `sha`가 오래됐거나 결과 패키지가 규칙을 어기면 **아무것도** 커밋되지 않습니다.
배치는 통째로 반영되거나 통째로 실패합니다.

## `If-Match`로 패키지 전체 지키기

파일별 `sha`는 "내가 읽은 뒤로 패키지가 움직이지 않았다"를 표현하지 못합니다. 두 에이전트가 서로
*다른* 파일을 편집하면 각자의 `sha`는 여전히 유효하므로 둘 다 반영되고, 나중 쪽은 이미 존재하지
않는 트리를 기준으로 편집을 계획한 셈이 됩니다.

그 틈을 막으려면 패키지 자신의 sha를 보내세요. 마지막 쓰기의 `commit.tree.sha`이고, Format
레코드가 `package_sha`로 보고하는 값과 같습니다:

```bash
curl -sS -X PUT \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "If-Match: 77de8a9b1c2d3e4f5061728394a5b6c7d8e9f001" \
  -H "Content-Type: application/json" \
  -d '{"message": "edit plan", "files": [{"path": "references/plan.md", "content": "IyBQbGFuCg==", "sha": "3f7b…"}]}'
```

패키지가 이미 움직였다면 쓰기는 `409 format_package_sha_mismatch`로 거부되고,
`error.details.package_sha`에 현재 값이 담겨 옵니다. 한 번의 왕복으로 다시 읽고 재시도할 수
있다는 뜻입니다. 이 헤더는 `PUT …/{path}`와 `DELETE …/{path}`에도 적용되며, *추가* 조건입니다 —
파일별 `sha` 검사는 그대로 유효합니다.

여기서 `If-Match`는 불투명한 ETag가 아니라 40자 hex 문자열 그대로의 패키지 sha입니다.
`package_sha`가 생기기 전에 발행된 Format은 이 값이 없으므로, 헤더를 무시합니다 — 만족시킬 방법이
없는 `409`를 돌려주지 않기 위해서입니다.

## 패키지에 담을 수 있는 것

대시보드 편집기가 강제하는 규칙과 같습니다:

- 패키지 루트의 `SKILL.md`는 필수이고, frontmatter의 `name`은 Format의 slug와 같아야 합니다.
  삭제할 수 없습니다.
- 파일은 루트에 있거나 `references/` 또는 `agents/` 아래 한 단계까지만 둘 수 있습니다. `..`,
  절대 경로는 불가.
- 파일 이름은 `^[A-Za-z0-9][A-Za-z0-9._-]*$`를 만족해야 합니다 — 앞에 `_`나 `.`은 불가.
- `.md`, `.json`, `.yaml`, `.yml`, `.txt`만 지원합니다.
- 파일당 최대 100 MiB, 패키지당 최대 100 MiB. Contents 배치의 `files[]`는 최대 1000개 경로.

이 중 하나라도 어기는 패키지는 아무것도 커밋되기 전에 거부됩니다. 경로가 거부되면
`skill_path_invalid`가 허용 규칙 전체를 함께 돌려주므로, 첫 거부만으로 이름을 고칠 수 있습니다.

## 처리해야 할 에러

| Status | `error.code` | 무슨 일인가 |
|---|---|---|
| `403` | `insufficient_scope` | 키에 `formats:read` / `formats:write`가 없거나 service-account 키입니다. service-account 키는 패키지를 만들거나 편집할 수 없습니다. `next_action`은 `authenticate`입니다. 기존 키에 스코프를 덧붙일 수는 없으니 새 키를 만드세요. 빠진 스코프는 `format_not_found`가 아닙니다. |
| `409` | `skill_slug_taken` | 워크스페이스에 같은 slug의 Format이 이미 있습니다. 만들기는 덮어쓰지 않으니 다른 slug를 쓰세요. |
| `409` | `skill_slug_reserved` | Sume 제공 Format이 그 slug를 전역으로 쓰고 있습니다. 다른 slug를 쓰세요. |
| `404` | `format_not_found` | 알 수 없는 Format이거나, 호출한 키의 워크스페이스 밖입니다. 팀 Format에는 그 워크스페이스에서 만든 키가 필요합니다. |
| `404` | `format_content_not_found` | Format은 있지만 그 경로에는 아무것도 없습니다. |
| `409` | `format_content_sha_required` | 경로가 이미 존재하는데 `sha`를 보내지 않았습니다. 읽고 다시 시도하세요. |
| `409` | `format_content_sha_mismatch` | `sha`가 오래됐습니다 — 다른 쪽이 먼저 커밋했습니다. 다시 읽고 재시도하세요. |
| `409` | `format_package_sha_mismatch` | `If-Match` 패키지 sha가 오래됐습니다. `error.details.package_sha`에 현재 값이 있습니다. |
| `400` | `skill_path_invalid`, `skill_frontmatter_invalid`, `skill_limit_exceeded`, … | 결과 패키지가 위 규칙을 어겼습니다. 아무것도 커밋되지 않았습니다. |
| `503` | `format_git_unavailable` | 패키지 히스토리가 커밋을 받지 못해서 **아무것도 저장되지 않았습니다**. 재시도하세요. |

마지막 항목은 의도된 설계입니다. 쓰기는 커밋되거나 실패하거나 둘 중 하나입니다. Format은 바뀌었는데
커밋은 없는 상태는 존재하지 않습니다.

## 이 API가 아닌 것

공개 git 엔드포인트도, clone URL도, 히스토리 writer에 직접 닿는 경로도 없습니다. 커밋은 이 API가
만들기 때문에 생깁니다. revert, blame, 히스토리 브라우징은 아직 이 surface에 없습니다.
