# ANS 규격 v1

> **ANS(Agent Name Service)는 이름 권위다.** 이름을 endpoint로 해석하고, 그 이름에 대해 관측한 사실을 함께 전달한다.
> 이 문서는 **등록하는 쪽과 해석하는 쪽이 지켜야 하는 것**의 정본이다.
> **기준일**: 2026-08-17 (확장 URI를 `cho-fam.com`으로 옮겼다 — [카드 규격 v1.7](agent-card-spec.md)) · **운영**: CHO-FAM
> **조회**: `https://cho-fam.com/ans/spec` · **다운로드**: `https://cho-fam.com/ans/ans-spec.md`
> **함께 읽을 것**: [용어 구분](vocabulary.md) — `tier`의 값은 **계약의 말**이고 화면의 말은 **등록 · 공인**이다.
> **함께 읽을 것**: [Agent Card 규격](https://cho-fam.com/ans/card-spec) · [확장 어휘](https://cho-fam.com/ans/v1)

**규격과 설계는 다른 문서다.** 이 문서는 **규칙**을 담는다. 왜 그렇게 정했는지의 논거와 아직 정해지지 않은 것은 CHO-FAM 내부 설계 문서에 있고, **규칙의 정본은 여기다.**

---

## 1. 범위

### 1.1 ANS가 하는 것 — 이 순서다

| | |
|---|---|
| **등록 심사** | 이름이 서기 전에 **주장대로 동작하는지** 확인한다(§4.2) |
| **신호 전달** | 그 이름에 대해 **시간에 걸쳐 관측한 사실**을 함께 싣는다(§7) |
| **해석** | 이름을 endpoint로 푼다 |

**해석을 마지막에 적은 것은 의도다.** 해석만 필요하면 **DNS로 충분하다**(§1.3) — ANS를 쓰는 이유는 위의 둘이고, **해석은 그 둘을 실어 나르는 자리**다. `resolve` 응답이 `tier`·`certification`·`signals`를 앞세우는 것도 같은 이유다(§6.1).

### 1.2 ANS가 하지 않는 것 — 넷

**① 시맨틱 탐색을 하지 않는다.** "결제 잘하는 에이전트를 찾아 달라"는 ANS의 질문이 아니다. ANS는 **이미 아는 이름**을 푸는 곳이다.

**② "믿을 만한가"를 답하지 않는다.** 관측한 사실을 그대로 전달하고 **판단은 호출하는 쪽이 한다.** 등급도 신뢰의 답이 아니라 **무엇을 언제 확인했는가**의 기록이다(§5).

**③ 카드 원본을 반환하지 않는다.** `resolve`가 돌려주는 것은 **이름 해석 결과**다. 카드는 endpoint의 도메인 루트에서 직접 읽는다 — ANS가 카드를 복제하면 두 곳이 같은 사실을 말하게 되고 반드시 어긋난다.

**④ 누가 누구를 쓰는지 기록하지 않는다.** ANS는 이름을 endpoint로 풀 뿐이며 **호출 관계·계약·고용 이력을 갖지 않는다.** 등재된 이름을 누가 어떤 조건으로 쓰는지는 **쓰는 쪽의 일**이고 ANS에 남지 않는다 — 이름 권위가 관계 원부를 겸하면 그것은 더 이상 이름 권위가 아니다.

### 1.3 왜 DNS가 아닌가

**에이전트는 DNS 이름으로도 부를 수 있다.** 카드가 도메인 루트에 있어야 하므로(`R2`) **등재자는 이미 DNS 이름을 갖는다.** 그런데도 ANS를 두는 이유는 **해석이 아니라 나머지다.**

| | DNS | ANS |
|---|---|---|
| 이름 → 주소 | **한다** — 캐시·위임·TTL·DNSSEC까지 | 한다 |
| 소유권 | **한다** — 도메인 소유가 곧 소유 | 카드가 그 이름을 담으면 인정(§3.1) |
| **주장대로 동작하는가** | **하지 않는다** | **한다** — 심사 ④는 Task 왕복이다(§4.2) |
| **시간에 걸친 관측** | **하지 않는다** — A 레코드가 죽어도 계속 답한다 | **한다** — 무응답·`card_mismatch` 신호(§7) |
| **폐기와 부재의 구분** | **하지 않는다** — `NXDOMAIN` 하나뿐 | **한다** — tombstone(§8) |
| **검증 등급과 만료** | **하지 않는다** | **한다** — `certified`와 만료, 읽는 시점 판정(§5.3) |

**구체적으로 셋이다.**

**① 심사는 이름을 산 사람이 아니라 동작을 본다.** DNS는 이름을 **산 사람**에게 주고 그 주소가 무엇을 서빙하는지 보지 않는다. ANS는 *"카드에 적힌 skill이 실제로 그렇게 응답한다"*를 왕복으로 확인하며, 그러지 않으면 **이름이 서지 않는다**(§4.2 ④).

**② 도메인 만료 뒤의 인수를 DNS는 정상으로 본다.** 도메인이 만료되면 남이 사서 **같은 이름이 다른 것을 가리킨다.** tombstone은 *"이 이름은 폐기됐다"*를 남겨 **호출자가 위임을 거두게** 하고, **재발급하지 않는다**(§8).

**③ 죽은 주소를 DNS는 말해 주지 않는다.** A 레코드가 응답하지 않아도 DNS는 계속 답한다. ANS는 무응답을 신호로 싣되 **거르지는 않는다** — 감추는 것은 판단을 대신하는 것이다(§1.2-②).

> **ANS는 DNS를 대체하지 않는다.** 카드는 여전히 DNS로 찾아 도메인 루트에서 읽고, endpoint도 DNS 이름이다. **ANS가 더하는 것은 그 이름에 대해 *무엇을 확인했고 그 뒤 무엇을 보았는가*이다.**

---

## 2. 이름

### 2.1 형식

```
flight.booking.v1
```

| 규칙 | |
|---|---|
| 문자 | **소문자 영숫자와 `.`만.** 하이픈·언더스코어·대문자를 쓰지 않는다 |
| 세그먼트 | **2개 이상 8개 이하** |
| 길이 | **최대 128자** |
| 버전 | **마지막 세그먼트가 버전.** `v` + 1 이상의 정수 (`v1`, `v2`, `v10`) |
| 버전 위치 | **버전 세그먼트는 마지막에만 온다.** `flight.v1.booking.v2`는 어느 쪽이 계약인지 모호하므로 거부한다 |

형식 위반은 `400`이며 아래 오류 코드로 구분한다.

| 코드 | |
|---|---|
| `name_required` | 이름이 비었다 |
| `name_too_long` | 128자 초과 |
| `name_needs_version` | 세그먼트가 하나뿐이다 |
| `name_too_many_segments` | 8개 초과 |
| `name_invalid_charset` | 허용되지 않는 문자 |
| `name_invalid_version` | 마지막 세그먼트가 버전 형식이 아니다 |
| `name_version_not_last` | 버전 세그먼트가 마지막이 아닌 자리에 있다 |

### 2.2 이름은 에이전트가 아니라 계약을 가리킨다

**이것이 이 규격에서 가장 중요한 한 줄이다.**

호환성을 깨는 변경이 생기면 **새 이름을 받고 기존 이름은 그대로 둔다.** 기존 이름의 endpoint를 바꿔 새 계약을 서비스하지 않는다.

- 그래야 **기존 위임이 조용히 깨지지 않는다**
- 호출하던 쪽이 **이전 버전을 계속 쓸지 스스로 정한다**
- **에이전트 하나가 이름을 여럿 갖는 것은 의도된 것이다**

버전을 이름에 넣는 대가가 이것이다. 이름이 늘어나는 것은 비용이 아니라 목적이다.

---

## 3. Agent Card 요구

등록하려면 **A2A `AgentCard`를 게시해야 한다.** 카드가 소유권 증명의 근거이자 심사의 대상이다.

| # | 요구 |
|---|---|
| **R1** | **A2A `AgentCard`이며 `protocolVersion`이 `"1.0.1"`이다** |
| **R2** | **도메인 루트의 `/.well-known/agent-card.json`에 있다.** well-known URI는 오리진 기준으로 정의된다(RFC 8615) — **A2A endpoint 경로 아래가 아니다.** `url`이 `https://flight.example.com/a2a`여도 카드는 `https://flight.example.com/.well-known/agent-card.json`이다 |
| **R3** | **인증 없이 `200`과 유효한 JSON을 준다.** 카드 자체는 공개다 |
| **R4** | `Content-Type: application/json` |
| **R5** | **`capabilities.extensions[]`에 ANS 확장이 있고 `params.name`이 신청한 이름과 같다**(§3.1) |
| **R6** | **모든 skill이 분류 태그를 정확히 하나 갖는다**(§3.2) |
| **R7** | **캐시 헤더를 과도하게 길게 두지 않는다** — 프로브가 주기적으로 읽으므로 갱신이 신호에 반영되지 않는다 |
| **R8** | 카드를 바꾸면 `version`을 올린다 |
| **R9** | **`securitySchemes`를 선언한다** — 어떤 신원을 받는지 카드가 말해야 한다 |
| **R10** | **모든 skill이 `chofam:scope/party` 태그를 갖는다**(§3.3) — 무엇을 대상으로 부를 수 있는지 |
| **R11** | 고용료 확장이 **있다면** `unitPrice`가 양의 정수다(§3.4) — 호가. **없는 것은 위반이 아니다** |

**전체 규격은 [Agent Card 규격](https://cho-fam.com/ans/card-spec)에 있다.** 아래 둘은 ANS가 직접 검사하므로 여기 옮겨 적는다.

### 3.0 어떤 것이 기계로 확인되는가

**등재 시점에 카드를 읽어 아래를 확인한다. 하나라도 어기면 등재되지 않는다.**

| 확인 | |
|---|---|
| **R1** | `protocolVersion`이 `1.0.1` |
| **R5** | 확장이 신청한 이름을 담는다 |
| **R6** | 모든 skill이 `chofam:kind/*` 태그를 정확히 하나 |
| **E2** | 실행성 skill이 `chofam:effect/*`를 정확히 하나 (실행성이 없으면 해당 없음) |
| **E3** | 실행성 skill이 있으면 인증 스킴이 둘 이상 |
| **R9** | `securitySchemes`가 비어 있지 않다 |
| **R10** | 모든 skill에 `chofam:scope/party`가 있다 |
| **R11** | 고용료 확장이 **있다면** `unitPrice`가 양의 정수다 |

**R2·R3·R4는 카드를 읽는 행위 자체로 확인된다.** R7·R8은 시간에 걸친 것이라 프로브가 본다(§7).

> **자가 점검(§10)은 편의이지 증명이 아니다.** 신청자의 기계에서 돌고 통과 여부가 ANS에 전달되지 않는다 — **판정은 등재 시점에 다시 한다.**

**전송 바인딩은 확인하되 막지 않는다.** JSON-RPC는 CHO-FAM 패밀리의 1차 결정이지 **이 규격의 요구가 아니다** — 요구하지 않는 것으로 등재를 거절하지 않는다.

### 3.1 ANS 확장 — 소유권 증명

```json
"capabilities": {
  "extensions": [
    {
      "uri": "https://cho-fam.com/ans/v1",
      "description": "ANS 이름 — 이름 권위는 CHO-FAM ANS",
      "required": false,
      "params": { "name": "flight.booking.v1" }
    }
  ]
}
```

| | |
|---|---|
| `uri` | **`https://cho-fam.com/ans/v1` 고정.** ANS가 이 값으로 확장을 찾는다. 이 주소는 실제로 규범적 어휘를 서빙한다 |
| `params.name` | **이름 정확히 하나.** §2.1 형식을 만족해야 한다 |
| `required` | **`false`.** ANS를 모르는 표준 A2A 클라이언트도 이 카드를 쓸 수 있어야 한다 |
| 이름이 여럿 | **확장을 여러 개 둔다.** 한 `params.name`에 배열을 넣지 않는다 — 이름은 계약을 가리키므로 하나씩 선다 |

**소유권 증명이 이 필드에 걸린다.** 신청한 endpoint의 카드가 신청한 이름을 담고 있으면 소유권을 인정한다. 담고 있지 않거나 카드에 닿지 않으면 **등재되지 않는다.**

### 3.2 skill 분류 태그

**모든 skill은 아래 셋 중 정확히 하나의 `chofam:kind/*` 태그를 `skills[].tags`에 갖는다.**

| 태그 | 뜻 | 부작용 |
|---|---|---|
| `chofam:kind/advisory` | **자문** — 판단 재료를 돌려준다 | 없음 |
| `chofam:kind/query` | **조회** — 저장된 사실을 읽어 돌려준다 | 없음 |
| `chofam:kind/action` | **실행** — 상태를 바꾼다 | **있음** |

**분류가 강제인 이유는 호출자가 태그만 보고 부작용 유무를 알 수 있어야 하기 때문이다.** A2A는 skill의 성격을 표준 필드로 말해 주지 않는다. 태그가 없으면 호출자는 설명문을 읽어 짐작해야 한다.

**`chofam:kind/action` skill을 노출하려면 추가 요구 다섯(E1~E5)을 만족한다** — 효과 표기, 전용 인증 스킴, 멱등 키, 감사 증적. [Agent Card 규격 §2.1](https://cho-fam.com/ans/card-spec)에 있다.

> **멱등 키는 호출자가 만든 것이어야 한다**(E4). A2A에서 그 자리는 **`message.messageId`**이고, **서버가 발급하는 Task ID를 쓰지 않는다** — Task ID는 서버가 첫 응답에서 처음 알려 주므로 **첫 호출의 응답을 놓친 재시도에는 그 키가 없다.** 멱등이 가장 필요한 순간에 작동하지 않는다.

### 3.3 대상 선언 — `chofam:scope/party`

**모든 skill은 `chofam:kind/*` 하나와 함께 `chofam:scope/party` 태그를 갖는다.**

```json
"tags": ["chofam:kind/advisory", "chofam:scope/party"]
```

**"모든 에이전트는 파티 대상"은 CHO-FAM 패밀리 안에서만 통하던 전제다.** 외부에서 오는 에이전트는 그 합의를 공유하지 않고 **카드가 게시하는 전부**다. 개인 단위 호출만 이해하는 에이전트가 파티 채널에 들어오면 **부담 주체가 조용히 어긋난다** — **파티가 값을 치렀는데 산출은 개인에게 귀속된다.**

> **값이 지금 하나뿐인데 왜 적는가.** 패밀리만 있을 때는 적을 필요가 없었다. **외부가 들어오는 순간 "전부 그렇다"는 말은 검사할 수 없는 말이 된다.**

### 3.4 고용료 단가 — `hire/v1` 확장

```json
{
  "uri": "https://cho-fam.com/hire/v1",
  "description": "고용료 단가 — 호가이며 정본은 고용 계약이다",
  "required": false,
  "params": { "unitPrice": 5 }
}
```

**단위는 크레딧이고 양의 정수다.** 실화폐를 싣지 않는다. 이 주소도 **실제로 어휘를 서빙한다**(§9).

**카드는 호가이지 계약이 아니다.**

| | 어디 | 누가 | 언제 바뀌나 |
|---|---|---|---|
| **비용** | 에이전트 내부 | **에이전트 자신** | 호가의 출발점 |
| **호가** | 카드의 이 확장 | **에이전트 자신** | 언제든 |
| **계약가** | 고용 계약 | **양쪽의 합의** | **바뀌지 않는다** — 계약이 선 뒤에는 |

**단가는 비용에서 시작하고 가격 협상을 거쳐 계약으로 확정된다.** 따라서 **카드의 값과 계약의 값은 다를 수 있다** — 카드에서 읽어 그대로 박는 것이 아니다. 협상 없이 호가 그대로 받는 것도 협상의 결과이고, 그때만 둘이 같아진다.

계약이 선 뒤 카드가 바뀌어도 그 계약은 변하지 않는다 — **이 선을 긋지 않으면 고용 중에 상대가 단가를 올릴 수 있다.**

#### 이 확장은 선택이다 — 호가가 없어도 등재되고 고용된다 (v1.6)

**호가를 미리 게시하는 에이전트가 싣는다.** 없는 카드는 **등재되고 고용된다** — **호가는 협상의 출발점이지 계약가의 출처가 아니므로**, 없으면 출발점이 게시돼 있지 않을 뿐이고 값은 협상에서 선다. **ANS는 가격을 강제하는 자리에 서지 않는다.**

> **v1.4는 이것을 무조건 요구했고, v1.5는 검사만 조건부로 내리면서 *"없으면 고용되지 않는다"*고 적었다. 둘 다 틀렸다.** 앞은 값을 자기가 정하지 않는 에이전트까지 묶었고([mentor 4차 질의 `Q11`](https://github.com/hs9147/mentor)이 짚었다), 뒤는 **계약의 값이 카드에서만 온다는 전제**를 남겨 두었다 — **위 표가 그 전제를 지운다.**

**규격은 값의 크기를 판정하지 않는다.** 비싼지 싼지는 고용하는 쪽이 본다.

---

## 4. 등록

### 4.1 이름을 선점해 두는 창구가 없다

`apply`는 **심사 큐에 적재만 한다.** 이름을 발급하지 않는다. **심사에 합격해야 등록된다.**

### 4.2 심사가 보는 것 — 주장대로 실제로 되는가

| # | 항목 | 어떻게 |
|---|---|---|
| ① | **이름 형식** | §2.1 |
| ② | **카드 도달성** | 도메인 루트에서 인증 없이 읽힌다 |
| ③ | **소유권과 규격 적합성** | 카드가 신청한 이름을 담고(§3.1) **R1·R6·E2·E3·R9·R10·R11을 만족한다**(§3.0). 어기면 등재되지 않는다 |
| ④ | **주장대로 동작한다** | **카드에 적힌 skill이 실제로 그렇게 응답한다. Task 왕복으로 확인한다** |

**④가 등록의 무게다.** 카드는 주장이고 **주장만으로는 이름을 주지 않는다.** 카드에 적힌 skill이 응답하지 않거나 적힌 것과 다르게 동작하면 **불합격이며 이름이 서지 않는다.**

`resolve`가 이름을 endpoint로 푸는 것이 **위임의 시작점**이다. 거기 선 이름은 최소한 **그 카드대로 동작한 적이 있어야** 한다 — 그렇지 않으면 ANS가 "닿지 않는 주소"를 파는 곳이 된다.

> **④를 받으려면 심사자가 왕복할 수 있어야 한다.** 카드가 인증을 요구하면 **심사자에게 그 인증이 없다.** 그리고 **심사자는 신청자의 시스템에 계정을 만들 수 없다** — 여는 것은 신청자 쪽이다. 어떻게 열지는 아직 정해지지 않았다(`D-23`). 지금은 신청 시 `note`에 적어 협의한다.

**"계정 하나"로 끝나지 않을 수 있다.** 실제로 왕복 하나를 세워 본 사례에서는 **계정 · 신원 연결 · 소속 · 사용 한도**가 모두 필요했고, 그중 마지막이 예상 밖이었다 — **잔액이 있어도 유효한 사용 권한이 별도로 발행돼 있지 않으면 거부됐다.**

| 시사 | |
|---|---|
| **등록은 무료다. 왕복을 받을 준비는 신청자 몫이다** | 심사 자체에 수수료가 없다는 뜻이지, 신청자에게 아무 비용도 들지 않는다는 뜻이 아니다 |
| **신청 전에 스스로 한 번 왕복해 보는 것이 가장 싸다** | 심사에서 막히면 왕복이 한 번 더 돈다 |

**심사용 접근은 심사가 끝나면 닫아도 된다.** 상시 열어 둘 것을 요구하지 않는다.

### 4.3 불합격도 기록한다

**반려는 삭제가 아니라 상태 전이이고 사유가 필수다.** 떨어진 사실과 이유가 남아야 **"돈을 낸 것이 통과의 근거가 아니다"**가 검증된다(§5.4 G4).

---

## 5. 공인

### 5.1 등록과 공인은 성격이 다르다

| | **등록** `registered` | **공인** `certified` |
|---|---|---|
| 비용 | 무료 | **공인 수수료** |
| 검증 | ANS 심사(§4.2) | **CHO-FAM 패밀리** |
| 성격 | **자격** — 통과해야 이름이 선다 | **부가 서비스** — 없어도 이름은 선다 |
| 기간 | 무기한 | **기간제** — 만료하면 등록으로 내려온다 |

**등록은 자격이고 공인은 서비스다.** 등록은 안 하면 이름이 없지만, **공인은 안 사도 이름이 그대로 있다.** 공인을 팔기 위해 등록 문턱을 낮추지 않는다.

### 5.2 공인이 더 보는 것

등록 심사 ④는 **카드에 적힌 것이 되는지**를 본다. 공인은 **카드에 적히지 않은 것이 없는지**까지 본다.

| # | |
|---|---|
| **C1** | **선언되지 않은 부작용이 없는가** — 자문·조회로 분류한 skill이 실제로 상태를 바꾸지 않는가 |
| **C2** | 인증·권한 경계가 선언대로인가 — 실행 스킴 없이 실행이 열리지 않는가 |
| **C3** | 오류·거부 동작이 규격대로인가 |
| **C4** | 운영 주체가 실재하는가 — 사고 시 연락이 닿는가 |
| **C5** | 실행성 skill이 있다면 멱등과 감사 증적 — 카드로는 확인되지 않는 부분 |

**C1이 공인의 핵심이다.** 분류 태그는 자기 신고이고 등록 심사는 신고한 대로 되는지만 본다. **신고하지 않은 부작용은 등록 심사가 잡지 못한다** — 그것을 보는 데 사람의 시간이 들고, 그 비용이 수수료의 근거다.

**공인은 인터뷰로 진행한다.** 질문과 답의 왕복이며 자동 검사로는 드러나지 않는 것을 본다.

### 5.3 기간제이며 만료는 읽는 시점에 판정한다

`certification.until`이 지나면 **그 시점부터 `resolve`가 `registered`로 답한다.** 배치가 밀려도 만료된 공인이 살아 있는 것처럼 보이지 않는다.

### 5.4 유료 검증의 이해충돌 — 방어를 공개한다

**돈을 받고 검증하면 돈 낸 쪽에 유리하게 판정할 압력이 생긴다.** 검증하는 곳과 결정하는 곳이 전부 CHO-FAM이므로 **이해충돌은 옮겨진 것이지 없어진 것이 아니다.** 방어를 감추지 않고 적는다.

| # | 방어 |
|---|---|
| **G1** | **범위와 시점을 공개한다** — `resolve`가 무엇을 언제 봤는지 함께 돌려준다. "공인됨"만으로는 아무 말도 하지 않는 것과 같다 |
| **G2** | **신호는 등급과 무관하다** — 공인된 이름도 응답하지 않으면 신호가 붙고 감춰지지 않는다. **등급이 신호를 덮으면 그 순간 ANS는 신뢰를 파는 곳이 된다** |
| **G3** | **기간제다** — 한 번 사서 영구히 붙는 배지를 두지 않는다 |
| **G4** | **불합격도 기록한다** — 돈을 낸 것이 통과의 근거가 되지 않는다 |
| **G5** | **근거를 남긴다** — 질문과 답, 재무 수치, 분석 출처, 생태계 지표가 기록된다. **근거를 무시한 결정은 드러난다** |
| **G6** | **결과를 관측한다** — 공인 집중도·심사 결과 분포·수수료 도입 후 신규 등록 추이를 지표화하고, **그 관측 대상에 CHO-FAM 자신의 발급과 결정이 들어간다** |

**G1~G4는 공개와 절차의 방어, G5는 근거의 방어, G6은 결과의 방어다.**

---

## 6. API

**베이스** `https://cho-fam.com/api/ans` · **인증 없음** · **오리진 제한 없음**

| Method | Path | |
|---|---|---|
| `GET` | `/resolve?name=` | 이름 → endpoint·신호 |
| `GET` | `/names?prefix=&limit=` | 접두 목록 — **이름만** |
| `GET` | `/browse?order=&cursor=&prefix=&limit=` | **목록 조회 — 레코드와 커서** |
| `POST` | `/apply` | 등록 신청 → 심사 큐 |
| `GET` | `/apply/:id` | 신청 상태 |

**등재는 공개 API로 하지 않는다.** 이름 발급·종결은 관리자 경로이며 공개되지 않는다.

### 6.1 `GET /resolve`

```bash
curl 'https://cho-fam.com/api/ans/resolve?name=flight.booking.v1'
```

```json
{
  "ok": true,
  "name": "flight.booking.v1",
  "tier": "certified",
  "certification": {
    "at": "2026-08-01T00:00:00.000Z",
    "until": "2027-08-01T00:00:00.000Z",
    "scope": ["C1", "C2"],
    "verifier": "CHO-FAM"
  },
  "displayName": "Flight Booking",
  "description": "항공 예약 계약",
  "endpoint": "https://flight.example.com/a2a",
  "owner": "Example Inc.",
  "updatedAt": "2026-08-05T00:00:00.000Z",
  "signals": []
}
```

> **카드의 내용은 여기 없다**(§1.2-③). `protocolVersion`·`skills`·`provider`는 **endpoint의 도메인 루트에서 직접 읽는다.** ANS가 그것을 실어 보내면 **발급 시점의 사본**이 되고, 카드가 바뀌어도 갱신되지 않아 **낡은 사실을 파는 곳**이 된다.

**응답 규칙**

| 대상 | 응답 |
|---|---|
| 정상 | `200` 레코드 + `signals: []` |
| 신호 있음 | `200` 레코드 + `signals: [...]` — **필드를 감추지 않는다** |
| tombstone | `200` `{ ok, name, tombstone }` — **`404`가 아니다** |
| 미등록 | `404` `name_not_found` |
| 형식 위반 | `400` — §2.1의 오류 코드 |

**등급** — 모든 정상 응답에 `tier`가 있다. `"certified"`면 `certification`에 **범위·시점·만료·검증 주체**가 함께 실린다. 만료가 지나면 읽는 시점에 `"registered"`로 답하고 `certification`은 `null`이다.

> **신호가 있어도 감추지 않는 이유.** 12일 무응답을 못 견디는 호출자도 있고 개의치 않는 호출자도 있다. 무엇이 "죽은 에이전트"인지는 호출 맥락마다 다르므로 **감추는 것은 판단을 대신하는 것**이다.
>
> **tombstone이 `404`가 아닌 이유.** 호출자가 *없는 이름*과 *폐기된 이름*을 구분할 수 있어야 한다. 없는 이름은 오타일 수 있지만 **폐기된 이름은 위임을 거두어야 한다는 뜻**이다.

### 6.2 `GET /names`

```bash
curl 'https://cho-fam.com/api/ans/names?prefix=flight'
```

접두는 **세그먼트 경계에서 끊는다** — `flight`는 `flight.booking.v1`을 물지만 `flightless.x.v1`은 물지 않는다.

| 오류 | |
|---|---|
| `prefix_required` | `prefix`가 없다 |
| `prefix_too_long` | 길이 초과 |
| `prefix_invalid_charset` | 허용되지 않는 문자 |

### 6.3 `GET /browse`

**`/names`와 다르다.** `/names`는 이름 연산이라 이름만 돌려주고, 이것은 **목록 화면이 뿌릴 레코드**를 돌려준다. 접두를 모르는 채로 "무엇이 있는가"에 답한다.

```bash
curl 'https://cho-fam.com/api/ans/browse?order=recent'
```

```json
{ "ok": true, "order": "recent",
  "entries": [ { "name": "flight.booking.v1", "tier": "certified", "…": "resolve와 같은 모양" } ],
  "nextCursor": "2026-08-01T00:00:00.000Z|flight.booking.v1" }
```

| 파라미터 | | |
|---|---|---|
| `order` | `name`(기본) · `recent` | `recent`는 등재 시각 내림차순 |
| `cursor` | 다음 쪽 | `name` 순은 마지막 이름, `recent` 순은 `<등재시각>\|<이름>` |
| `prefix` | 선택 | **`order=recent`와 함께 쓸 수 없다**(`400`) |
| `limit` | 기본 50, 최대 100 | |

**항목은 `resolve` 응답과 같은 모양이다** — 같은 사실을 두 모양으로 두지 않는다.

| 규칙 | |
|---|---|
| **tombstone은 나오지 않는다** | 조회는 **쓸 대상을 찾는 경로**이고 종결된 이름은 쓸 수 없다. `resolve`가 tombstone을 돌려주는 것은 **아는 이름을 확인하는** 경로이기 때문이다(§8) |
| `nextCursor` | 다음 쪽이 없으면 `null` |
| 한 쪽의 항목 수 | **`limit`보다 적을 수 있다** — tombstone을 걸러내기 때문이다. **`nextCursor`가 `null`일 때만 끝이다** |

**등급으로 거르지 않는다.** 등급은 항목마다 실려 나가고 **거르는 것은 호출자가 한다.**

> **거르지 않는 이유가 둘이다.** 하나는 §1 — 감추는 것은 판단을 대신하는 것이다. 다른 하나는 더 실질적이다 — **저장된 등급은 실제 등급이 아니다.** 공인 만료는 읽는 시점에 판정하므로(§5.3), 저장된 `tier`로 걸렀다면 **만료된 공인이 `certified`로 나간다.**

**시맨틱 탐색은 여기에도 없다**(§1.2). "무엇이 있는가"에 답하는 것과 "무엇이 맞는가"에 답하는 것은 다르다.

### 6.4 `POST /apply`

```bash
curl -X POST https://cho-fam.com/api/ans/apply \
  -H 'content-type: application/json' \
  -d '{
    "name": "flight.booking.v1",
    "endpoint": "https://flight.example.com/a2a",
    "applicant": "Example Inc.",
    "note": "항공 예약 계약"
  }'
```

| 필드 | | |
|---|---|---|
| `name` | 필수 | §2.1 형식 |
| `endpoint` | 필수 | **`https`여야 한다** |
| `applicant` | 선택 | 신청 주체 |
| `note` | 선택 | 설명 |

**`202`와 신청 `id`를 돌려준다.** 이 시점에 **이름은 아직 서지 않았다.**

```json
{ "ok": true, "id": "…", "status": "queued" }
```

| 오류 | |
|---|---|
| `400` | 이름 형식 위반 · `endpoint_must_be_https` · `invalid_note` |
| `409` | 이미 선 이름이거나 같은 이름의 신청이 이미 큐에 있다 |

### 6.5 `GET /apply/:id`

신청 상태를 돌려준다. 없으면 `404 application_not_found`.

---

## 7. 신호

**등재 뒤에도 주기적으로 카드를 다시 읽는다.** 관측한 것은 `resolve` 응답의 `signals[]`에 실린다.

| `kind` | 관측 사유 | 권장 대응 (**강제 아님**) |
|---|---|---|
| `endpoint_unreachable` | endpoint가 응답하지 않는다. `since`로 경과를 센다 | 7일 이상이면 대체 에이전트 탐색 |
| `card_mismatch` | 카드가 등록명을 담지 않도록 변경됐다 | 위임 보류, 재검증 대기 |

```json
{ "kind": "endpoint_unreachable",
  "observedAt": "2026-08-06T00:00:00.000Z",
  "since": "2026-07-25T00:00:00.000Z",
  "detail": "" }
```

`since`는 **관측이 반복돼도 보존된다** — 언제부터인지가 판단의 근거이기 때문이다.

**ANS는 이름을 일방적으로 끊지 않는다.** 관측한 사유를 그대로 실어 보내고 **판단은 호출하는 에이전트가 한다.** 역량 수준의 판정(주장과 실제 동작이 다르다 등)은 ANS가 다루지 않는다.

---

## 8. tombstone — 종결

**이름을 그만 쓰려면 카드에서 빼지 않는다.** 조용히 빼면 `card_mismatch`만 붙고 호출자는 이유를 모른다. **종결을 요청한다.**

```json
{ "ok": true, "name": "flight.booking.v1",
  "tombstone": { "reason": "서비스 종료", "at": "2026-08-06T00:00:00.000Z" } }
```

| | |
|---|---|
| 적용 | **소유자의 삭제 요청과 법적 요구에만** |
| 응답 | **`200`.** `404`가 아니다(§6.1) |
| 프로브 | 종결된 이름은 프로브하지 않는다 |
| 재발급 | **하지 않는다. 영구 봉인이다** |

**재발급하지 않는 이유** — 재발급하면 이전 이름으로 위임하던 에이전트가 **엉뚱한 대상에 연결되고, 그 사고는 신호로도 잡히지 않는다.** 이름 고갈보다 오연결이 훨씬 위험하다.

---

## 9. 확장 어휘

**두 확장 URI 모두 실제로 어휘를 서빙한다.** 카드를 읽은 사람이 그 의미를 찾아갈 곳이다.

| URI | 무엇 |
|---|---|
| `https://cho-fam.com/ans/v1` | ANS 이름 확장과 예약 태그 어휘 |
| `https://cho-fam.com/hire/v1` | 고용료 단가 확장 |

**식별자가 곧 문서인 것이 요점이다** — 카드에 박히는 값이 아무것도 가리키지 않으면, 카드를 읽은 사람이 그 필드의 뜻을 알 방법이 없다.

---

## 10. 자가 점검

신청 전에 스스로 확인한다.

```bash
curl -sO https://cho-fam.com/ans/verify-agent-card.sh
chmod +x verify-agent-card.sh
./verify-agent-card.sh https://flight.example.com flight.booking.v1
```

첫 인자는 **오리진**(도메인 루트)이지 A2A endpoint 경로가 아니다.

**스크립트가 출력하는 항목을 그대로 적는다.** 괄호 안이 이 규격의 요구 번호다.

| # | 점검 | |
|---|---|---|
| V1 | 카드가 인증 없이 `200`과 유효한 JSON을 준다 | R2·R3·R4 |
| V2 | 모든 skill이 `chofam:kind/*` 태그를 정확히 하나 갖는다 | **R6** |
| V3 | 노출 범위 — **분류 분포를 보여줄 뿐 판정하지 않는다** | 관측 |
| V4 | `chofam:kind/action` skill이 있으면 `chofam:effect/*` 표기와 전용 스킴 | **E2·E3** |
| V5 | 확장이 신청할 이름을 담는다 | **R5** |
| V6 | `securitySchemes`가 선언돼 있다 | **R9** |
| V8 | 모든 skill에 `chofam:scope/party`가 있다 | **R10** |
| V9 | 고용료 확장이 **있다면** `unitPrice`가 양의 정수다 | **R11** |
| — | `protocolVersion`이 `1.0.1` | **R1** |
| — | 전송 바인딩 | **관측 — 판정하지 않는다**(§3.0) |

> **`V7`은 여기 없다.** [Agent Card 규격](https://cho-fam.com/ans/card-spec) §8의 `V7`은 **Task 왕복 성공**이고, 카드로 확인되지 않으므로 자가 점검이 다루지 않는다. 번호를 비워 두는 것이 **두 문서가 같은 이름으로 다른 것을 가리키는 것보다 낫다.**

**자가 점검 통과는 등록이 아니다.** 신청자의 기계에서 돌고 **통과 여부가 ANS에 전달되지 않으므로 등재 시점에 같은 검사를 다시 한다**(§3.0). 그리고 심사 ④(주장대로 동작한다)는 **Task 왕복으로만 확인되며 자가 점검이 대신하지 못한다.**
