만들기
에이전트를 정의하고, release를 만들어 활성화하고, 세션에 묶는다
에이전트는 워크스페이스 안의 이름 붙은 정의다. 이름과 설명은 사람이 보는 값이고, 실행에 쓰이는 정의는 카드(모델, 도구)다. 카드를 고칠 때마다 새 version이 쌓인다. version 하나를 프로필과 묶은 것이 release이고, 에이전트가 가리키는 release가 활성 release다. 세션을 만들 때 release를 넣으면 그 세션은 그 에이전트의 것이 된다.
웹 콘솔에서
로컬 스택의 웹 콘솔은 http://127.0.0.1:3000/이다.
- 처음 한 번은
/bootstrap에서 첫 owner와 워크스페이스를 만든다. 첫 owner 토큰을 받는 방법은 설치 방식마다 다르다. 단일 서버 설치에서 토큰을 직접 정하지 않았다면scripts/test-ops.sh bootstrap-token이 한 번 보여 준다. 자세한 내용은 첫 owner와 key에 있다. - 로그인한 뒤 왼쪽 사이드바의 "에이전트"를 열고 오른쪽 위의 "새 에이전트"를 누른다. "새 에이전트" 화면이 다섯 단계(이름 → 하는 일 → 릴리스 → 켜기 → 채널 연결)로 열린다.
- "이름" 단계에서 이름을 넣는다. 식별자(slug)는 이름에서 채워지고 고칠 수 있다. 필요하면 "설명 (선택)"에 사람이 읽을 소개를 적는다. "다음"을 누른다.
- "하는 일" 단계에서 모델을 고르고, "쓸 수 있는 도구"에서 프로필 도구를 체크로 고른 뒤(지시문과 설명) 답변 언어를 정하고 "초안 저장하고 다음"을 누른다. 에이전트가 초안으로 저장되고 버전 1이 생긴다. 동작 지시는 저장소의
AGENTS.md나CLAUDE.md에 적는다. - "릴리스" 단계에서 프로필을 골라 "릴리스 만들기"를 누른다. 카드의 모델이 프로필이 실행할 수 있는 모델 중 하나여야 릴리스를 만들 수 있다.
- "켜기" 단계에서 "에이전트 켜기"를 누른다. 이제 새 세션은 이 release로 시작한다. "나중에 하기"를 누르면 에이전트는 초안으로 남고 상세 화면으로 간다.
owner·admin·member 모두 에이전트를 만들 수 있고, 만든 사람이 그 에이전트의 관리자가 된다.
API로
에이전트 API는 웹 콘솔과 같은 cookie 로그인으로만 부른다. API key로 부르면 403이다.
로그인
read -rs WERFT_PASSWORD
WERFT_COOKIE=$(curl -sS -o /dev/null -D - "$API/v1/auth/login" \
-H 'Content-Type: application/json' -H 'X-Requested-With: werft-web' \
-d "$(jq -nc --arg e 'owner@example.test' --arg p "$WERFT_PASSWORD" '{email: $e, password: $p}')" \
| sed -n 's/^[Ss]et-[Cc]ookie: \(__Host-werft_session=[^;]*\).*/\1/p')로그인 cookie는 __Host- 이름이고 Secure라서 http에서는 curl의 cookie jar가 저장하지 않는다. 그래서 응답 헤더에서 꺼내 Cookie 헤더로 보낸다. 상태를 바꾸는 요청에는 X-Requested-With: werft-web이 필요하다.
WEB=(-H 'Content-Type: application/json' -H 'X-Requested-With: werft-web' -H "Cookie: $WERFT_COOKIE")
curl -sS "$API/v1/auth/me" "${WEB[@]}" | jq '{role: .principal.role, workspace: .workspace.slug}'만들기
에이전트에는 담당 팀이 꼭 있어야 한다. owner·admin은 GET /v1/teams의 아무 팀이나, 그 밖의 사람은 GET /v1/my-teams에 나오는 자기 팀을 고른다. 아래는 owner로 팀을 하나 만들어 쓴다(같은 이름의 팀이 이미 있으면 409이니 그 팀의 id를 쓴다).
TEAM=$(curl -sS "$API/v1/teams" "${WEB[@]}" -d '{"name": "Research"}' | jq -r .team.id)
agent=$(curl -sS "$API/v1/agents" "${WEB[@]}" -d '{
"slug": "research-bot",
"name": "Research Bot",
"description": "저장소를 읽고 질문에 답한다",
"managing_team_id": "'"$TEAM"'",
"card": {
"model": "claude-sonnet-5",
"capabilities": { "tools": ["Read", "Grep", "Glob"], "mcp": null }
}
}')
AID=$(echo "$agent" | jq -r .agent.id)
VID=$(echo "$agent" | jq -r '.versions[0].id')
echo "$agent" | jq '{slug: .agent.slug, version: .versions[0].version_no, active_release_id: .agent.active_release_id}'{
"slug": "research-bot",
"version": 1,
"active_release_id": null
}같은 워크스페이스에 같은 slug가 있으면 409 ALREADY_EXISTS다. 만들기만으로는 아무것도 활성화되지 않는다.
managing_team_id에 넣은 팀이 담당 팀이 된다. 빠지면 400 BAD_REQUEST이고 error.details.issues에 {"path": "managing_team_id", "message": "managing_team_id is required"}가 있다. owner·admin이 없는 팀을 넣으면 404다. 그 밖의 사람이 자기가 속하지 않은 팀을 넣으면, 없는 팀이어도 403(details.reason_code: team_not_allowed)이다. 그래서 속한 팀이 없는 member는 에이전트를 만들 수 없다. 응답의 agent.managing_team_name은 그 팀의 이름이다.
모든 에이전트에는 담당 팀이 하나 있다. 담당 팀은 PUT /v1/agents/{id}/managing-team으로 바꿀 수만 있고 비울 수는 없다. 담당 팀을 필수로 받기 전에 만든 에이전트는 마이그레이션이 담당 팀을 채웠다. 담당 팀이 없던 에이전트는 모두 워크스페이스마다 하나씩 만든 구성원 없는 Unassigned agents 팀이 맡았다(같은 이름의 팀이 이미 있으면 Unassigned agents (1a2b3c4d)처럼 뒤에 8자리 식별자가 붙는다). 그 팀은 보통 팀과 같아서 이름을 바꾸거나 에이전트를 다른 팀으로 옮길 수 있다.
릴리스 만들기
release는 version 하나와 프로필 하나를 묶는다. 카드의 model이 프로필이 실행할 수 있는 모델(model과 models)에 없거나, 카드의 도구가 프로필에 없으면 422다. release로 만든 세션은 model을 생략하면 카드의 모델로 돈다.
release=$(curl -sS "$API/v1/agents/$AID/releases" "${WEB[@]}" \
-d "$(jq -nc --arg v "$VID" '{version_id: $v, runtime_profile_id: "claude-coding-local"}')")
REL=$(echo "$release" | jq -r .release.id)
echo "$release" | jq '{id: .release.id, created, tools: .release.effective_tools.tools}'{
"id": "sha256:…",
"created": true,
"tools": [
"Read",
"Glob",
"Grep"
]
}release id는 카드와 프로필 내용의 해시다. 같은 내용으로 다시 만들면 같은 release가 created: false로 온다.
요청에 note(앞뒤 공백을 뺀 1–200자)를 넣으면 release에 메모로 남는다. 메모는 id에 들어가지 않으므로 같은 내용으로 다시 만들면 처음 메모가 그대로다. 비어 있거나 200자를 넘으면 400이다.
사람이 release를 부르는 이름은 label이다. 만든 시각을 서울 시간으로 초까지 적은 2026-10-30_14:30:28 꼴이고, 같은 에이전트에서 같은 초에 release가 여럿 생기면 두 번째부터 -2, -3이 붙는다. 콘솔의 릴리스 기록, 에이전트 머리 태그, 세션 목록이 이 이름을 쓴다. 식별자는 그대로 release id다. 카드의 version_no는 카드를 고칠 때만 오르므로, 같은 카드를 다른 프로필로 release하면 version은 같다.
콘솔의 릴리스 기록은 release마다 같은 에이전트에서 바로 앞에 만든 release와 비교해 바뀐 것(허용 저장소, 모델, 도구·MCP, 프롬프트, 설정 값)을 만든 사람·시각과 함께 보인다. 자격 증명은 값 없이 "변경됨"으로만 보인다. 세션 화면은 그 세션이 쓰는 release를 보이고, 에이전트의 활성 release가 아니면 표시한다. 세션은 시작할 때의 release를 계속 쓰므로, 바뀐 설정은 새 세션부터 쓰인다. 세션 응답의 agent.release_is_active가 이 비교 결과다.
활성화
expected_revision은 에이전트의 지금 activation_revision이다. 처음에는 -1이다. 다른 곳에서 먼저 바꿨으면 409 REVISION_CONFLICT다.
curl -sS "$API/v1/agents/$AID/activate" "${WEB[@]}" \
-d "$(jq -nc --arg r "$REL" '{release_id: $r, expected_revision: -1}')" \
| jq '{activation_revision, previous_release_id}'{
"activation_revision": 0,
"previous_release_id": null
}release의 activated_at은 그 release가 마지막으로 활성화된 시각이고, deactivated_at은 다음 활성화가 그 자리를 넘겨받은 시각이다. 지금 활성인 release의 deactivated_at과 한 번도 활성화되지 않은 release의 두 값은 null이다. 둘 다 활성화 기록에서 읽으므로 이 필드가 생기기 전에 활성화한 release에도 값이 있다.
목록과 개수
curl -sS "$API/v1/agents?limit=50" "${WEB[@]}" | jq -c '{n: (.items | length), next_cursor}'
curl -sS "$API/v1/agents/counts" "${WEB[@]}" | jq -c{"n":1,"next_cursor":null}
{"counts":{"all":1,"administered":1,"active":1,"draft":0}}목록은 오래된 에이전트부터 limit개(기본 50, 최대 100)씩 온다. 응답의 next_cursor를 cursor로 보내면 다음 페이지이고, 마지막 페이지에서는 null이다. cursor 없이 부르면 첫 페이지만 온다. q(최대 200자)를 주면 이름이나 식별자에 그 글자가 든 에이전트만 대소문자 구분 없이 오고, ids에 에이전트 ID를 쉼표로 이어(최대 100개) 주면 그 에이전트만 온다. runtime_profile_id를 주면 활성 release가 그 프로필로 도는 에이전트만 오고, 활성 release가 없는 에이전트는 빠진다. 모두 같은 cursor로 다음 페이지를 읽는다. GET /v1/agents/counts는 목록을 끝까지 읽었을 때의 개수(all), 그중 내가 직접 또는 팀으로 관리하는 개수(administered), 활성 release로 켜진 개수(active), 보관되지 않았지만 아직 활성 release가 없는 초안 개수(draft)를 준다. 둘 다 목록과 같은 권한이 있어야 하고 API key는 거절된다.
콘솔의 담당 팀
담당 팀은 에이전트를 맡는 팀이다. 그 팀의 팀 관리자는 에이전트를 관리하고, 구성원은 설정을 보고 쓴다(권한).
- 새 에이전트: 첫 단계에서 담당 팀을 반드시 고른다. owner·admin은 모든 팀에서, member는 자기가 속한 팀에서 고른다. 고를 팀이 하나면 미리 골라 둔다. 속한 팀이 없는 member는 “속한 팀이 없어 에이전트를 만들 수 없습니다. 워크스페이스 관리자에게 팀에 넣어 달라고 요청하세요.”만 보고 다음으로 가지 못한다.
- 에이전트 목록: 표와 카드에 담당 팀이 보인다. 설정을 읽을 수 없는 에이전트의 카드에는 이름·상태·담당 팀만 있다.
- 관리자 탭: 맨 위
담당 팀묶음에 지금 담당 팀 이름이 팀 상세로 가는 링크로 보인다. 에이전트를 관리하는 사람은 그 옆바꾸기에서 팀을 고르면 바로 바뀌고, 비울 수는 없다. 담당 팀에 팀 관리자가 없으면 그렇다고 알린다. 아래에이전트 관리자목록은 사람과 팀을 같은 한 줄 행으로 보이고, 검색 결과에서 고르면 바로 추가된다. 담당 팀이 관리자 팀이기도 하면 그 행에 구성원 모두가 관리자라고 적힌다. - 담당 팀 구성원: 상세 화면의 설정을 모두 읽지만 편집·릴리스·연결 단추는 없고, 화면 위에 읽기만 할 수 있다는 안내가 있다.
- 바깥 member: 상세 주소를 열면 설정 대신 이름·상태·담당 팀과 “이 에이전트의 설정은 담당 팀 구성원과 관리자만 봅니다.”가 보인다.
세션에 묶기
세션을 만들 때 agent_release_id를 넣는다. profile_id는 그 release의 프로필이어야 한다(아니면 409 RELEASE_PROFILE_MISMATCH). key는 그 에이전트의 에이전트 key를 쓴다. 에이전트 key는 웹 콘솔이나 웹 로그인 cookie로 POST /v1/api-keys에 kind: "agent"와 agent_id를 넣어 발급한다(API key). 이 발급은 SDK 메서드가 없다. 비밀값은 변수에만 담는다.
repository_id는 선택이다. 빼면 GitHub 설정이 있는 에이전트는 저장소 없이 시작하고 필요한 저장소만 붙이며(저장소 없는 세션), 설정이 없는 옛 에이전트는 release의 저장소로 시작한다. 아래 예제는 sample-app을 직접 지정해 그 저장소로 시작한다.
import { Werft } from "@werft-ai/sdk";
// WERFT_API_KEY holds the agent key; WERFT_AGENT_RELEASE_ID the release above.
const agent = new Werft();
const started = await agent.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
agent_release_id: process.env.WERFT_AGENT_RELEASE_ID ?? "",
message: 'GATE-SPEC {"id":"agent","steps":[],"final":"hello from research-bot"}',
});
console.log(started.status, started.turn_id);에이전트 key로 만든 세션의 요청은 다른 key나 사람이 승인한다(승인용 key 따로 쓰기).
워크스페이스에 없는 key(예: scripts/local.sh key quickstart …)로 보내면 release를 찾지 못해 404다.