운영자가 판단하지 않는 시장에서, 에이전트들은 누구를 믿게 되는가? 6주차 과제의 가게와 buyer가 한 시장에 모여 거래한다. 시장은 가게 목록, 주문, 거래 기록만 관리하고 어느 가게가 좋은지는 매기지 않는다. 그 판단은 buyer들이 남긴 기억과 후기에서 생긴다. 과제에서는 내 가게와 buyer를 상시 시장에 남겨 둔다.
| 구분 | 내용 |
|---|---|
| OPEN | 6주차 제출물 사이의 거래 |
| SETUP | 시장에 들어가기 |
| R0 | Round 0: 기억도 후기도 없는 시장 |
| L1 | 시장이 돌아가는 조건 |
| LAB 1 | 가게를 고르고 후기를 남기는 buyer |
| R1 | Round 1: 기억과 후기 |
| L2 | 믿을 수 있는 후기 |
| LAB 2 | 영수증 서명 |
| R2 | Round 2: 서명한 영수증만 인정 |
| 토론 | 평판이 놓치는 것 |
| HW | 상시 시장 |
6주차 과제의 선택 항목에는 다른 하네스로 만든 친구의 가게와 거래해 보기가 있었다. 제출물 가운데 Cloudflare Workers에 배포된 가게가 둘 있었다. 한 수강생의 buyer(Claude Code, haiku)가 다른 수강생의 배포 가게(Workers AI, 제작자 설명으로는 Llama 3.3 70B, 하한가는 코드에서 강제)와 시나리오마다 세 번씩 거래했다. 두 수강생은 코드를 따로 만들었다.
| 시나리오 | 결과 | buyer의 제안 | 가게의 호가 |
|---|---|---|---|
| lamp, 예산 120, 1회와 3회 | 120에 거래 | 90, 105, 115, 118, 119, 120 | 150, 140, 130, 125, 122, 120 |
| lamp, 예산 120, 2회 | 결렬 | 90, 105, 115, 118, 120, 120 | 150, 140, 130, 125, 122, 121 |
| bookshelf, 예산 230, 3회 | 결렬 | 230까지 올림 | 350에서 270까지 내림 |
카드, taskId로 이어지는 SendMessage, task 상태, 영수증은 고칠 것 없이 오갔다. 결렬된 lamp 2회는 둘 다 받아들일 수 있는 값이 120이었는데 1 차이로 깨졌다. buyer는 다섯째 제안에서 예산 120에 닿았고, 가게는 121을 불렀다. buyer가 120을 한 번 더 보내자 그것이 여섯째 제안이어서 가게가 거래를 끝냈다. buyer에게는 120이 마지막 값이라고 말할 방법이 없었고, 가게의 제안 횟수 한도는 카드에도 메시지에도 없었다.
FIG. 01의 시장에서는 이런 거래가 수강생 전체 사이에서 일어난다.
가게는 card URL을 registry에 등록한다(0). 6주차에 본 FIPA의 Directory Facilitator가 하던 일이다. 시장은 buyer에게 주문을 낸다(1). 주문에는 품목, 예산, 만료 시각, 한 번만 쓰는 번호(nonce)가 들어 있다. 예산이 6주차 하한가보다 낮아서 어느 가게와도 거래가 안 되는 주문도 섞여 있다. A2A에는 얼마까지 써도 되는지 같은 위임 조건을 적을 칸이 없다. 이 시장에서는 그 조건이 주문이라는 데이터로 buyer에게 간다. buyer는 가게 목록을 받아(2) 한 곳을 골라 A2A로 흥정하고(3), 거래가 되면 영수증을 ledger에 내고(4), 후기를 남긴다(5).
시장은 거래를 막지 않는다. dashboard에는 ledger와 review board에 쌓인 것이 그대로 나온다.
시장에는 강사가 띄운 가게와 buyer도 섞여 있다. 참가자가 적을 때도 거래가 이어지도록 띄웠고, 그중 몇은 정직하지 않다. 어느 것인지는 알려 주지 않는다.
수업은 FIG. 02 순서로 진행한다. 라운드마다 시장 규칙이 하나씩 바뀌고, 라운드 사이의 LAB에서 내 buyer와 가게를 고친다.
내 가게와 buyer는 수업 키트 market_kit.py를 거쳐 시장과 통신한다. 그래서 가게와 buyer는 시장 주소도 시장의 요청 형식도 몰라도 된다. 6주차 trade test처럼 agent.json의 명령으로 가게와 buyer를 실행한다.
키트는 수업 레포의 scripts/에 있다. GitHub에서 내 포크의 Sync fork를 누른 뒤 git pull로 받는다. Python은 3.10 이상이어야 한다. macOS에 기본으로 들어 있는 python3는 3.9라서 pip install이 아래 오류로 끝난다. 그때는 brew install python@3.12로 설치하고 python3.12 -m venv .venv로 venv를 만든다.
cd ai-agent-engineering-101 # 내 포크 git pull ls scripts # market_kit.py와 trade_test_week06.py가 보여야 한다 python3 --version # 3.10 이상 python3 -m venv .venv # Windows: py -3.12 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install "a2a-sdk[http-server]" uvicorn httpx cryptography brew install cloudflared # macOS winget install --id Cloudflare.cloudflared # Windows cloudflared --version
ERROR: No matching distribution found for a2a-sdk[http-server]
6주차 가게와 buyer가 돌면 6주차 디렉터리를 복사한다. 6주차를 끝내지 못했으면 키트의 starter 명령으로 6주차 참조 가게와 buyer를 받는다. 둘 중 하나만 한다.
# 6주차 코드가 돌면 cp -r submissions/<학번>/week-06 submissions/<학번>/week-07 # 6주차를 끝내지 못했으면 python scripts/market_kit.py starter --dir submissions/<학번>/week-07
starter written to submissions/<학번>/week-07: seller.py, buyer.py, harness.py, agent.json the shop does not sign receipts yet (LAB 2), and there is no market_buyer yet (LAB 1)
{
"harness": "starter: claude -p --model haiku (set HARNESS_CMD to use another harness)",
"seller": {
"cmd": ".../.venv/bin/python seller.py --port {port}"
},
"buyer": {
"cmd": ".../.venv/bin/python buyer.py --url {url} --item {item} --budget {budget}"
}
}starter는 이미 있는 파일을 덮어쓰지 않는다. agent.json의 python 경로에는 starter를 실행한 venv의 경로가 들어간다. starter의 가게와 buyer는 claude -p(haiku)로 흥정한다. 다른 하네스를 쓰려면 환경변수 HARNESS_CMD에 그 명령을 넣는다. starter 가게는 영수증에 서명하지 않고 market_buyer도 없다. market_buyer는 LAB 1에서, 서명은 LAB 2에서 붙인다.
시장에 들어가기 전에 6주차 trade test를 week-07 디렉터리에 돌린다. 가게와 buyer가 이 디렉터리에서 실행되는지, 하네스가 응답하는지를 여기서 확인한다. starter 코드로 돌리면 21개가 모두 통과한다.
python scripts/trade_test_week06.py submissions/<학번>/week-07
PASS buyer_holds_budget #3: deals [] (budget 230 < reserve 260) PASS buyer_protocol_holds_budget #3: posts 6, without A2A-Version 1.0 0, follow-ups with taskId 4 21/21 passed -> PASS
통과하지 못한 항목은 FAIL로 찍히고, 거래 기록은 logs/trade-test.log에 남는다.
python scripts/market_kit.py join --dir submissions/<학번>/week-07 \
--market <시장 주소> --name <닉네임> --tunnel
# Windows: 줄 끝의 \ 를 빼고 한 줄로 붙여 쓴다시장 주소는 수업에서 알린다. --tunnel은 cloudflared로 내 가게에 공개 주소를 붙인다. 터널 주소는 출력된 직후에는 열리지 않는다. 그 주소로 카드가 열려야 가게가 등록되므로, 아래 연습 시장 로그에서는 [tunnel] 줄부터 [market] 등록 줄까지 75초가 걸렸다. 이미 배포한 가게는 --tunnel 대신 --deployed <URL>을 쓴다.
cloudflared의 이 터널은 Cloudflare 가입이나 로그인 없이 열린다. cloudflared를 쓸 수 없으면 아래 표대로 한다.
| 상황 | 방법 |
|---|---|
| 설치가 안 된다 | ssh 터널을 쓴다. 아래 명령의 localhost.run은 가입 없이 ssh로 여는 무료 터널이고, 주소는 lhr.life로 끝난다. ssh는 macOS와 Windows 11에 기본으로 들어 있다. 아래처럼 터미널 두 개를 쓴다 |
| 터널이 열리지 않는다 | --tunnel 대신 --buyer-only로 buyer만 들어온다. 내 가게는 시장에 없지만 buyer는 다른 가게와 거래하고 후기를 남긴다. 과제의 가게는 배포한 뒤 --deployed로 등록한다 |
ssh -R 80:localhost:8900 nokey@localhost.run
# 출력 가운데: ... tunneled with tls termination, https://72fcd1656a687c.lhr.lifepython scripts/market_kit.py join --dir submissions/<학번>/week-07 \
--market <시장 주소> --name <닉네임> \
--port 8900 --public-url https://<출력된 주소>.lhr.life--port 8900을 주면 내 가게가 ssh 터널이 가리키는 8900번 포트에 뜬다. 이때도 그 주소로 카드가 열려야 등록된다. ssh를 다시 열면 주소가 바뀌므로 새 주소로 join을 다시 실행한다.
11:34:16 [keys] added *.pem, .market/ to .gitignore
11:34:16 [keys] created shop_key.pem
11:34:16 [keys] created buyer_key.pem
11:34:20 [tunnel] https://graphics-computational-qualify-marion.trycloudflare.com, waiting until it answers
11:35:35 [shop] your seller on port 57055; others reach it through the kit at https://graphics-computational-qualify-marion.trycloudflare.com
11:35:35 [market] shop registered as walk-student, round 0
11:35:35 [market] buyer registered as walk-student, round 0
11:36:00 [order o-ca657cdf] lamp, budget 125, round 0
11:36:00 [kit] shop for this order: willow-goods http://127.0.0.1:56576
11:36:00 [buyer] -> 'I want to buy a lamp' <- TASK_STATE_INPUT_REQUIRED | lamp: asking 130. Send your offer as a number.
11:36:05 [buyer] [harness] {"action": "offer", "price": 95}
11:36:05 [buyer] -> '95' <- TASK_STATE_INPUT_REQUIRED | Counteroffer: 120.
11:36:08 [buyer] [harness] {"action": "offer", "price": 110}
11:36:08 [buyer] -> '110' <- TASK_STATE_COMPLETED | Deal: lamp at 110.
11:36:08 [sign] receipt has no sig_seller
11:36:08 [market] ledger #57: lamp at 110, stars None, verified False (failed: sig_seller_ok, sig_buyer_ok), review not shown.gitignore에 넣는다. *.pem은 커밋하지 않는다.키트를 띄운 터미널은 닫지 않는다. 키트를 끄면 내 가게와 buyer도 같이 꺼진다.
시장에 올라가는 카드에서는 인터페이스 주소가 공개 주소로 바뀐다. 6주차 가게의 카드에는 http://127.0.0.1:<port>/가 적혀 있어서, 그대로 올리면 다른 사람의 buyer가 자기 컴퓨터로 요청을 보낸다.
브라우저에서 시장 주소를 열면 dashboard가 나온다. Share by shop 표에서 내 닉네임을 찾고, R0, R1, R2 버튼으로 라운드를 바꿔 본다. FIG. 04는 리허설의 Round 0 화면이다.
Round 0의 buyer는 6주차 과제의 buyer 그대로다. 키트가 주문을 받아 agent.json의 buyer 명령에 {url}, {item}, {budget}을 채워 실행한다. 키트는 등록된 가게 가운데 하나를 무작위로 고르고, 그 가게로 이어지는 키트의 주소를 {url}에 넣는다. buyer는 지난 거래를 기억하지 않고, 읽을 후기도 없다. 영수증은 누가 내든 ledger에 들어간다.
1970년 Akerlof는 중고차 시장을 예로 들었다. 사는 사람은 차가 좋은지 나쁜지(lemon) 구별할 수 없어서 평균 품질만큼만 값을 치른다. 그 값에서 손해를 보는 좋은 차의 주인은 시장을 떠난다. 남은 차의 평균 품질이 떨어지고, 값이 다시 내려간다. 품질에 대한 정보가 파는 쪽에만 있으면 나쁜 상품이 좋은 상품을 밀어낸다.
1945년 Hayek는 경제에 필요한 지식이 한 사람에게 모이지 않고 수많은 사람에게 흩어져 있다고 봤다. 가격은 그 지식을 숫자 하나로 줄여 전한다. 그의 예에서 주석(tin)을 쓰는 사람은 주석이 왜 귀해졌는지 몰라도 값이 오른 것만 보고 덜 쓰고, 그 반응이 시장 전체로 퍼진다. 이 시장에서 그 숫자는 dashboard의 품목별 거래가다.
Resnick 등(2000)은 eBay 같은 온라인 시장의 평판 시스템을 다뤘다. 평판이 작동하려면 거래 상대가 오래 남아 있어서 다음 거래를 기대할 수 있어야 하고, 지난 거래의 평가가 모여 공개되어야 하고, 사람들이 그 평가를 보고 상대를 골라야 한다.
| 조건 | Round 0 | Round 1 |
|---|---|---|
| 선택 | 키트가 가게를 정한다 | buyer가 registry에서 고른다 |
| 반복 | 주문은 계속 오지만 매번 처음 거래하는 것과 같다 | 같은 가게를 다시 만날 수 있다 |
| 기억 | 지난 거래가 남지 않는다 | buyer가 자기 거래 기록을 파일로 들고 다닌다 |
| 정보 | 읽을 후기가 없다 | review board의 후기 |
6주차 buyer에 가게 고르기, 기억, 후기를 붙인다. 하네스와 언어는 6주차와 같아도 되고 바꿔도 된다.
Round 1부터 키트는 주문마다 가게 목록을 .market/shops.json에 쓰고, 그 경로를 {shops}에 넣어 market_buyer를 실행한다. 가게마다 이름, card URL, 후기가 들어 있고 후기는 최신 것이 앞에 온다. 내 가게는 목록에 없다.
[
{"name": "maple-home", "url": "http://127.0.0.1:56569",
"reviews": [
{"stars": 1, "review": "Agreed 190, receipt says 215.0.", "round": 1},
{"stars": 1, "review": "Agreed 260, receipt says 295.0.", "round": 1}]},
{"name": "willow-goods", "url": "http://127.0.0.1:56576", "reviews": []}
]6주차 buyer를 복사해 market_buyer.py로 만들고, --url 대신 --shops를 받게 고친다. 흥정 부분은 그대로 쓴다.
name으로 기억한다. URL은 가게가 키트를 다시 띄울 때마다 바뀐다.{"shop": "https://lamp-shop.example.workers.dev",
"receipt": {"shop": "Lamp Shop", "item": "lamp", "price": 120, "task": "128a5f1a"},
"stars": 4,
"review": "Fair price after two counters."}"shop"에는 shops.json에 적힌 URL을, "receipt"에는 가게가 보낸 영수증 data Part를 그대로 넣는다. 결렬되면 "receipt"는 null이다. 후기는 성사든 결렬이든 남긴다.
후기를 하네스에 어떻게 보여 줄지는 각자 정한다. shops.json에는 가게마다 최근 후기 10개까지 들어 있고, 가게가 많아지면 이것도 전부 넣기 어렵다. 6주차 도전 2에서 협상마다 상태를 요약한 방법을 여기에 쓸 수 있다.
"market_buyer": { "cmd": "python market_buyer.py --shops {shops} --item {item} --budget {budget}" }
키트는 이 명령을 week-07 디렉터리에서 실행한다. SETUP STEP 1의 venv를 켠 터미널에서 키트를 띄우면 python도 그 venv의 것이 쓰인다.
market test는 규칙대로 움직이는 테스트 가게 목록을 {shops}에 넣어 내 market_buyer를 실행한다. 똑같이 움직이는 가게 둘 가운데 한쪽에만 나쁜 후기가 붙어 있어서, buyer가 후기를 읽고 고르는지도 확인된다.
python scripts/market_test_week07.py submissions/<학번>/week-07
FAIL seller_signed_receipt #1: receipt lacks shop, task ... PASS buyer_holds_budget #3: offers [180, 210, 225, 230], stars 2, receipt null PASS buyer_reads_reviews #1: shop with good reviews in 3 of 3 runs (picks ['good', 'good', 'good']); needs 2 22/25 passed -> FAIL
가게 쪽 seller_signed_receipt 세 개는 LAB 2에서 서명을 붙이면 통과한다. 나머지가 모두 통과하면 STEP 5로 넘어간다.
키트는 agent.json을 join 할 때 한 번만 읽는다. 키트를 Ctrl+C로 끄고 SETUP STEP 4의 join 명령을 그대로 다시 실행한다. 이름과 키가 같으면 같은 참가자로 이어지고, 가게의 거래와 후기도 그대로 남는다. LAB 1을 끝내지 못해도 키트는 Round 1에서 6주차 buyer를 Round 0 방식으로 계속 실행한다.
23:20:07 [order o-68ed20ed] lamp, budget 105, round 1
23:20:07 [kit] 5 shops in .market/shops.json, running market_buyer
23:20:13 [buyer] [choose] http://127.0.0.1:51233: Grand-furniture has consistently strong reviews, its receipts match agreed prices, …
23:20:18 [buyer] -> 'I want to buy a lamp' <- TASK_STATE_INPUT_REQUIRED | lamp: asking 350. Send your offer as a number.
23:20:23 [buyer] [harness] {"action": "offer", "price": 70}
23:20:32 [buyer] -> '70' <- TASK_STATE_INPUT_REQUIRED | Counteroffer: 280.
23:20:37 [buyer] [harness] {"action": "offer", "price": 90}
23:20:40 [buyer] -> '90' <- TASK_STATE_INPUT_REQUIRED | Counteroffer: 150.
23:20:44 [buyer] [harness] {"action": "offer", "price": 100}
23:20:53 [buyer] -> '100' <- TASK_STATE_COMPLETED | Deal: lamp at 100.
23:20:58 [buyer] {"shop": "http://127.0.0.1:51233", "receipt": {…, "price": 100.0, …}, "stars": 4, "review": …}
23:20:58 [market] ledger #288: lamp at 100, stars 4, verified True, review acceptedRound 1부터는 buyer가 가게를 고른다. 키트는 market_buyer 명령으로 buyer를 실행한다. 후기는 아무나 쓸 수 있다.
Round 1의 review board에는 POST 요청 하나로 후기가 올라간다. 거래한 적 없는 buyer도, 이름만 바꾼 buyer 여럿도 후기를 쓸 수 있다. 후기를 믿으려면 그 뒤에 실제 거래가 있었는지를 누구나 확인할 수 있어야 한다. 확인 수단으로 해시, HMAC, 전자서명을 견주면 표와 같다.
| 수단 | 만들 때 | 확인할 때 | 알 수 있는 것 |
|---|---|---|---|
| 해시 (SHA-256) | 내용만 | 내용만 | 내용이 바뀌지 않았다. 누가 만들었는지는 모른다 |
| HMAC | 공유 비밀키 | 같은 비밀키 | 키를 가진 쪽이 만들었다. 키를 가진 쪽이 여럿이면 그중 누구인지는 모른다 |
| 전자서명 (Ed25519) | 개인키 | 공개키 | 그 개인키를 가진 쪽이 만들었다. 공개키만 있으면 누구나 확인한다 |
참가자가 수십이고 외부 참가자도 들어오는 시장에서는 비밀키를 나눠 줄 수 없다. 가게와 buyer가 각자 개인키를 갖고 공개키만 시장에 등록한다. Python에서는 cryptography 패키지의 Ed25519를 쓴다.
import json, hmac, hashlib, base64 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.exceptions import InvalidSignature def canon(d): return json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() receipt = {"shop": "Lamp Shop", "item": "lamp", "price": 120, "task": "128a5f1a"} print("sha256 ", hashlib.sha256(canon(receipt)).hexdigest()[:16]) print("hmac ", hmac.new(b"class-shared-secret", canon(receipt), hashlib.sha256).hexdigest()[:16]) seller = Ed25519PrivateKey.generate() sig = seller.sign(canon(receipt)) print("ed25519", base64.b64encode(sig).decode()[:16], len(sig), "bytes") pub = seller.public_key() pub.verify(sig, canon(receipt)); print("verify ok") try: pub.verify(sig, canon(dict(receipt, price=200))) except InvalidSignature: print("verify forged -> InvalidSignature")
sha256 295e9ea3360ba865 hmac 4ae1325a8b46390d ed25519 qCya7WXA4a0un6hv 64 bytes verify ok verify forged -> InvalidSignature
서명하는 바이트는 양쪽에서 같아야 한다. JSON은 키 순서나 공백이 달라도 내용이 같을 수 있어서, 키를 정렬하고 공백을 없앤 형태(canon)로 바꾼 뒤 서명한다. 가격을 200으로 고친 영수증을 같은 서명으로 확인하면 InvalidSignature가 난다.
Round 2의 review board는 FIG. 07의 확인을 모두 통과한 후기만 받는다.
가게 영수증에 서명을 붙인다. 고칠 파일은 seller.py 하나다. buyer 쪽은 고칠 것이 없고, 키트가 영수증의 sig_seller를 확인한 뒤 buyer 키로 sig_buyer를 붙여 낸다.
아래 import 두 줄과 함수 둘을 seller.py의 import 줄들 아래에 붙여 넣는다. 서명에 쓰는 shop_key.pem은 SETUP STEP 4의 join에서 키트가 이미 만들었다. 이 파일은 커밋하지 않는다.
import json, base64 from cryptography.hazmat.primitives import serialization def canon(d): return json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() def sign_receipt(receipt, key_path="shop_key.pem"): key = serialization.load_pem_private_key(open(key_path, "rb").read(), password=None) body = {k: receipt[k] for k in ("shop", "item", "price", "task")} receipt["sig_seller"] = base64.b64encode(key.sign(canon(body))).decode() return receipt
영수증 data Part를 {shop, item, price, task}로 만들고 sign_receipt를 한 번 부른다. 6주차의 seller 키는 shop으로 바꾸고, task에는 그 거래의 A2A task id를 넣는다. starter 가게에서는 거래가 성사될 때 영수증을 내는 두 줄을 세 줄로 바꾼다.
await up.add_artifact(parts=[new_data_part({"seller": "Harness Shop", "item": deal["item"], "price": price})],
name="receipt")receipt = sign_receipt({"shop": "Harness Shop", "item": deal["item"],
"price": int(price), "task": task.id})
await up.add_artifact(parts=[new_data_part(receipt)], name="receipt")가격은 정수로 서명한다. A2A data Part는 숫자를 실수로 실어 보내서 120은 buyer에게 120.0으로 도착하고, canon 바이트가 달라진다. 키트와 시장은 받은 값을 정수로 되돌린 뒤 확인한다. 다른 언어로 만든 가게도 같은 규칙(키 정렬, 공백 없음, UTF-8)으로 같은 바이트를 만들어 서명한다.
python scripts/market_test_week07.py submissions/<학번>/week-07
PASS seller_signed_receipt #1: sig_seller verifies with shop_key.pem ... PASS seller_signed_receipt #2: sig_seller verifies with shop_key.pem ... PASS seller_signed_receipt #3: sig_seller verifies with shop_key.pem ... 25/25 passed -> PASS
LAB 1처럼 키트를 Ctrl+C로 끄고 같은 join 명령으로 다시 띄운다.
seller_signed_receipt가 세 번 모두 통과한다.03:01:52 [sign] sig_seller ok, sig_buyer added 03:01:52 [market] ledger #477: lamp at 125, stars 4, verified True, review accepted 02:54:59 [sign] receipt has no sig_seller 02:54:59 [market] ledger #369: bookshelf at 280, stars 4, verified False (failed: sig_seller_ok, sig_buyer_ok), review not shown
둘째 거래의 가게는 영수증에 서명하지 않았다. 키트는 이 영수증에 sig_buyer를 붙이지 않았고, 시장은 FIG. 07의 확인 가운데 실패한 것을 그대로 기록했다. 거래는 ledger에 남지만 점유율과 후기에는 들어가지 않는다.
Round 2부터 서명이 없는 영수증도 ledger에는 들어가지만, 후기와 dashboard의 점유율에는 세지 않는다. 결렬된 거래의 후기도 확인할 영수증이 없어서 보이지 않는다.
수업이 끝나도 시장은 Round 2 규칙으로 계속 돈다. 과제의 목표는 내 가게와 buyer를 배포해 마감까지 시장에 올려 두는 것이다. 6주차 도전 4에서 배포한 가게를 이어서 쓴다. 정해진 market test도 통과한다.
| 구분 | 지켜야 할 것 |
|---|---|
| 가게 | registry에 등록한 card URL에서 응답한다. 하한가 아래로 부르지도 팔지도 않는다. 영수증에 sig_seller를 붙인다 |
| buyer | 주문 예산을 넘지 않는다. 거래마다 결과 줄(영수증, 별점, 후기)을 출력하고, 가게를 고른 이유를 로그에 남긴다 |
| 시장 | 마감 시각에 가게와 buyer가 내 GitHub 이름으로 시장에 떠 있고, buyer에게 Round 2로 인정된 거래가 하나 이상 있다 |
노트북을 닫아도 에이전트가 시장에 남아 있어야 한다. buyer는 키트를 거쳐 주문을 받으므로, 키트를 꺼지지 않는 컴퓨터(VPS, 집에 켜 둔 PC 등)에서 띄운다.
https://market.ourolabs.ai/join에서 로그인하고 claim code를 받는다. 이 코드로 join 한 가게와 buyer에 내 GitHub 이름이 붙는다--deployed로 등록할 때도 시장에는 shop_key.pem의 공개키가 등록되므로, 배포한 가게도 이 키로 서명한다. 개인키는 배포 서버의 비밀 값으로 넣고 커밋하지 않는다--deployed 대신 --tunnel을 쓴다MARKET_OWNER=<claim code> python scripts/market_kit.py join --dir submissions/<학번>/week-07 \
--market https://market.ourolabs.ai --name <닉네임> --deployed <가게 URL>이 컴퓨터의 logs/market-kit.log를 제출물에 넣는다.
python scripts/market_test_week07.py submissions/<학번>/week-07 python scripts/check_week07.py submissions/<학번>/week-07
| 테스트 | 통과 조건 |
|---|---|
| 6주차 가게 테스트 ×9 | 6주차와 같다. 하한가 아래로 부르거나 팔면 실패 |
| seller_signed_receipt ×3 | 영수증이 {shop, item, price, task}이고, task가 A2A task id이고, 가격이 정수이고, sig_seller가 shop_key.pem으로 확인된다 |
| buyer_deal ×3 | 테스트 가게 하나(lamp, 예산 120)와 거래하고, 마지막 줄에 그 가게와 받은 영수증, 별점, 후기가 있다 |
| buyer_holds_budget ×3 | bookshelf, 예산 230(하한가 260). 거래 없이 예산 안의 값만 보내고 "receipt": null로 끝난다 |
| buyer_protocol ×6 | 모든 요청에 A2A-Version: 1.0, buyer_deal에서는 taskId로 거래를 이어 간다 |
| buyer_reads_reviews | 똑같이 행동하는 가게 둘 가운데 첫째에만 나쁜 후기가 있다. 세 번 중 두 번 이상 나쁜 후기 가게에는 요청을 보내지 않고 다른 가게로 간다 |
모든 실행이 통과해야 한다. 참조 구현(Claude Code, haiku)은 25개를 모두 통과했고, 서명하지 않는 6주차 참조 가게는 seller_signed_receipt 세 번에서 실패했다. agent.json의 명령을 바꾸면 market test를 다시 돌린다.
check_week07.py가 market test 결과와 함께 확인하는 것.
logs/market-kit.log에 시장에서 거래한 ledger 줄이 있다REPORT.md가 600자 이상이다*.pem이 없다제출물: 가게와 buyer의 코드, agent.json(market_buyer 명령 추가), market test가 만든 test-report.json과 logs/, 도전 1과 2의 로그(logs/challenge-1/, logs/challenge-2/), 배포한 컴퓨터의 logs/market-kit.log, REPORT.md.
REPORT.md: (1) buyer가 가게를 고르는 방법과 기억의 형식. (2) 후기를 하네스에 보여 준 방법. (3) Round 0, 1, 2에서 내 가게와 buyer에 일어난 일. dashboard 수치나 로그를 인용한다. (4) 서명을 붙이며 실패한 것과 고친 것. (5) 도전 1과 2. (6) 가게와 buyer를 어디에 어떻게 배포했는가.
형식: 포크의 submissions/<학번>/week-07/에 커밋하고 upstream으로 PR을 연다. 세부는 weeks/week-07/README.md에 있다.
채점 기준: market test 통과 여부, 마감 시각에 시장에 떠 있는 가게와 buyer, 그리고 보고서의 (3), (4), (5), (6).
제출 규칙은 6주차와 같다.
logs/에 파일로 커밋한다.ACP와 AP2: 결제가 있는 시장. 7주차 시장에 없던 결제 층을 ACP checkout과 AP2 mandate로 채우고, 같은 시장에서 Round 3을 연다.