기능에이전트

프로필 고르기

세션이 어떤 모델·도구·권한 모드로 도는지 정하는 프로필

프로필(profile)은 세션이 도는 방식을 정한다: runtime과 그 버전, 모델, 도구 목록, 권한 모드, 모델 provider, 저장소를 쓰는지. 운영자 프로필은 이 설치의 모든 워크스페이스가 쓸 수 있는 가장 넓은 범위를 정한다. 설치의 DB에 있고 사용자는 id로 고른다. 프로필마다 돌 수 있는 저장소 목록도 그 DB 행에 있다. 새 설치에는 프로필이 없다. 설치 owner가 콘솔의 설정 > 에이전트 연결 > 모델 연결에서 연결을 더하고, 프로필을 만들 때 그 연결을 고른다. 프로필 화면은 연결을 만들거나 바꾸지 않고 고르기만 한다. 연결도 설치의 DB에 있다. 연결 추가 대화상자는 종류를 고르면 그 종류의 공식 엔드포인트(anthropic은 https://api.anthropic.com)를 미리 채운다. 프록시나 호환 서버를 쓸 때만 고치면 되고, 비워서 보내도 같은 주소로 저장된다. 공식 주소가 없는 종류(litellm)는 엔드포인트를 직접 적어야 한다. 엔드포인트는 만든 뒤 바꿀 수 없다. 연결을 사용 중지하면 그 연결로만 모델을 부르는 프로필과 에이전트는 다시 켤 때까지 실행되지 않는다. 확인 창은 조회에 성공한 영향 수(프로필, 활성 릴리스의 에이전트, 끝나지 않은 세션)를 보여 준다. 꺼진 연결은 같은 화면의 다시 켜기로 되살린다. 저장된 키가 없거나 같은 정의의 다른 연결이 켜져 있으면 켤 수 없다. 프로필이 쓰던 연결을 쓸 수 없게 되면 프로필 양식은 첫 연결로 바꾸지 않고 연결을 직접 고르게 한다.

목록 보기

SDK 예제는 SDK 설치에서 만든 werft 클라이언트를, HTTP API 예제는 서버 주소 API와 key KEY 셸 변수를 쓴다.

const profiles = await werft.profiles.list();
for (const profile of profiles.items) {
  console.log(profile.id, profile.model, profile.repository_mode);
}
claude-coding-auto claude-sonnet-5 required
claude-coding-local claude-sonnet-5 required
general claude-sonnet-5 none
필드뜻
model기본 모델
tools세션에서 쓸 수 있는 도구. 밖의 도구는 실행되지 않는다(권한)
deny이 프로필이 막는 내장 도구. 이 설치가 그 runtime 버전에서 지원하는 내장 도구 중 tools에 없는 것이다. 지원 목록이 없는 버전이면 null(도구 차단목록)
billingmetered면 비용을 세션 상한에 센다. none이면 비용을 세지 않는다
repository_moderequired(저장소 필수), optional, none(저장소 없음)

provider 주소와 자격 증명은 목록에 나오지 않는다. 그 값은 worker에도 가지 않는다.

세션에서 고르기

세션을 만들 때 profile_id로 고른다. 프로필이 모델을 여럿 허용하면 model로 그중 하나를 고르고, 빼면 기본 모델로 돈다.

도는 중인 세션의 모델이 허용 목록에서 빠지면 모델 요청이 곧 끊긴다. 세션 이벤트에 error 이벤트(code: MODEL_NOT_ALLOWED, model)가 한 번 남고, 세션 상세 타임라인에 "…이(가) 프로필의 허용 목록에서 제외되어 이 세션의 모델 요청이 차단되었습니다"로 보인다. 허용 목록을 되돌리거나 다른 모델로 새 세션을 연다.

모델이 빠진 사실은 막히는 모든 경로가 어느 모델인지 이름을 넣어 말한다.

  • 도는 중인 턴은 위 error 이벤트다. 프로필이나 모델 연결이 통째로 없어져 막힐 때도 같은 이벤트가 남고, cause가 profile_removed(모델만 빠졌으면 model_removed)이며 model에 세션이 쓰던 모델이 담긴다.
  • 다음 메시지가 새 worker를 띄우다 막히면 세션이 failed가 되고, 같은 error 이벤트(cause: model_removed)와 "모델 … 이(가) 프로필 …의 허용 목록에서 빠졌다"는 사유가 세션 이벤트와 입력 receipt에 남는다. 세션 상세의 attention(CATALOG_MISMATCH)도 빠진 모델 이름을 말한다.
  • 새 세션을 만들 때 허용 목록에 없는 모델이면 422 UNSUPPORTED_CAPABILITY가 모델 이름을 말한다. 요청이 모델을 적지 않아 릴리스의 모델로 도는 경우에도 그 이름이 나온다.
  • 허용 목록에서 빠진 모델의 릴리스를 활성화하거나 그 릴리스로 세션을 만들면 PROFILE_DIGEST_MISMATCH가 "릴리스의 모델이 허용 목록에서 빠졌다"고 모델 이름과 함께 말한다.

프로필 상세는 "이 프로필을 쓰는 에이전트" 위에, 에이전트 개요는 준비 상태 위에 어느 모델이 빠졌는지와 할 일(모델을 목록에 되돌리거나, 상세 설정에서 허용된 모델로 바꿔 저장)을 경고로 보인다.

같은 시리즈의 새 모델로 옮기기

운영자 프로필(설정 문서의 profiles)에 model_families를 적어 두면, 빠진 모델의 세션이 막히는 대신 같은 시리즈의 새 모델로 옮겨 간다. 모델 이름을 풀어 읽어 시리즈를 짐작하지 않고, 적어 둔 값만 쓴다.

"model_families": {
  "claude-sonnet-5-0": { "family": "sonnet", "rank": 0 },
  "claude-sonnet-5-1": { "family": "sonnet", "rank": 1 }
}
  • family가 같은 모델이 한 시리즈이고, rank가 클수록 새 모델이다. 한 family에서 rank가 겹치면 설정이 거절된다.
  • 빠진 모델의 항목도 지우지 않는다. 항목이 없으면 그 모델이 어느 시리즈인지 알 수 없어 지금처럼 막힌다.
  • 후보는 허용 목록에 있는 같은 family에서 rank가 더 높은 모델이고, 여럿이면 가장 가까운(가장 낮은) 하나다. 낮은 rank로 내려가거나 다른 family로 넘어가는 일은 없다. 후보가 없으면 위에서 설명한 대로 막힌다.
  • model_families는 세션의 고정 값(지문)에 들어가지 않는다. 적거나 바꿔도 이미 만든 세션과 릴리스는 그대로다.

옮기는 때는 세션이 다음 턴을 시작하는 claim이다. 턴을 기다리는 세션만 옮기고, 도는 중인 턴은 건드리지 않는다. 그 턴은 위 설명대로 모델 요청이 끊긴다. 옮기면 세션 이벤트에 system 이벤트(subtype: model_migrated, from, to)가 남고 타임라인에 "…에서 …(으)로 옮겼다"로 보인다. 이전 모델로 만든 체크포인트는 옮긴 뒤에도 그대로 이어서 쓴다.

릴리스의 모델이 빠졌는데 후보가 있으면 그 릴리스로 세션을 만들 수 있다. 세션은 후보 모델로 시작하고, 세션을 만드는 것이 릴리스를 바꾸지는 않는다. 요청이 model을 직접 적었다면 그 모델이 허용 목록에 있어야 한다. 프로필 요약(GET /v1/profiles)의 model_successors가 빠진 모델과 옮겨 갈 모델을 보여 주고, 이 경우 에이전트 개요는 막힘 경고 대신 안내를 보인다.

에이전트의 활성 릴리스도 후보 모델로 옮겨진다. 서버가 model_migration 사유의 새 릴리스를 만들어 활성화하고, 감사 receipt에 이전·이후 모델과 automatic: true가 남는다. 사람이 누른 것이 아니라서 행위자는 그 릴리스를 마지막으로 활성화한 사람(owner·admin이어야 한다)이다. 그 사람이 비활성이거나 더는 owner·admin이 아니면 옮기지 않고 에이전트는 막힌 채로 두며 관리자에게 실패를 DM으로 알린다. 옮기고 나면 에이전트 관리자에게 DM이 간다. 에이전트마다 auto_model_migration(PATCH /v1/agents/{id}, 기본 true)을 끄면 릴리스 재작성·세션 이동·새 세션의 후보 사용이 모두 멈추고 이 에이전트는 막힌다. 에이전트 개요의 경고에서도 켜고 끌 수 있다.

이 방식은 운영자 프로필의 모델 목록이 바뀌는 경우를 다룬다. 워크스페이스 프로필 버전은 만든 뒤 모델 목록이 바뀌지 않는다.

처음 막힐 때 Slack DM도 한 번 간다. DM에도 빠진 모델 이름이 들어간다. 받는 사람은 세션을 만든 사람과 에이전트 관리자(직접 지정한 관리자, 관리자 팀 구성원, 담당 팀 lead)이고, 각자 이 에이전트의 Slack 앱이 설치된 팀에 Slack 계정을 연결해 둔 경우만 받는다. 세션 하나에 사람마다 한 번이라, 같은 세션의 다음 시도가 막혀도 다시 오지 않는다. 다른 이유로 끝난 세션에는 가지 않는다.

const created = await werft.sessions.create({
  profile_id: "claude-coding-local",
  model: "claude-sonnet-5",
  repository_id: "sample-app",
  message: 'GATE-SPEC {"id":"profile","steps":[],"final":"ok"}',
});
const sessionId = created.session_id;
console.log((await werft.sessions.get(sessionId)).runtime);
{
  kind: 'claude_agent_sdk',
  version: '0.3.319',
  profile_id: 'claude-coding-local',
  model: 'claude-sonnet-5',
  permission_mode: 'default'
}

카탈로그에 없는 프로필, 그 저장소에서 돌 수 없는 프로필, 프로필이 허용하지 않는 모델은 422다.

에이전트 release를 만들 때는 runtime_profile_id로 고른다. 카드의 model이 그 프로필이 실행할 수 있는 모델 중 하나이고, 카드의 도구가 모두 프로필에 있어야 한다(만들기).

도구 차단목록

프로필 버전은 쓸 도구를 늘어놓는 대신 막을 내장 도구만 deny에 적을 수 있다. 운영자 프로필이면 서버는 버전을 저장할 때 이 설치가 그 runtime 버전에서 지원하는 내장 도구 중 deny에 없는 것을 tools로 저장한다. 워크스페이스 프로필은 아래처럼 바탕의 도구 안에서 계산한다. 세션과 release는 저장된 tools만 본다.

  • 제한 없음은 지원 목록 안에서다. 운영자 프로필의 deny: []는 지원 목록의 내장 도구를 모두 연다. 엔진에 들어 있어도 이 설치가 지원하지 않는 도구는 열리지 않는다. 지원 목록과 도구마다의 실행 위치(boundary)는 owner가 GET /v1/installation-profiles/options의 tool_catalogs로 본다.
  • 실행 위치. local은 worker 안에서, egress는 플랫폼의 외부 접속 프록시를 거쳐 밖으로, provider는 모델 provider 쪽에서 돈다. provider 쪽 도구(WebSearch)는 외부 접속 프록시가 막지 못하고 프로필의 차단만 막는다. 그래서 새 이름의 첫 버전은 provider 쪽 도구를 deny에 넣고 시작하기를 권한다. API는 deny에 적은 것만 막으므로 deny: []는 WebSearch도 연다. 콘솔은 첫 버전에서 provider 쪽 도구를 기본으로 막아 둔다.
  • 새로 생긴 도구는 막힌 채 이어진다. 버전은 저장할 때의 지원 목록으로 tools를 고정하고, 기존 버전·release·세션은 새 도구를 얻지 않는다. 엔진을 올리면 runtime 버전이 바뀌어 새 프로필 버전을 만들게 된다. 이때 이전 버전(운영자 프로필은 previous_id, 워크스페이스 프로필은 같은 이름의 최신 버전)이 따져 보지 않은 도구가 새 tools에 들어가려면 accept_added_tools에 적어야 한다. 그런 도구가 deny에도 accept_added_tools에도 없으면 400(reason_code: added_tool_not_accepted)이다. 이전 버전을 tools로 만들었으면 그 목록 밖의 도구가 모두 여기에 든다.
  • deny는 내장 도구 이름과 mcp__<server>__<tool> 이름을 받는다. MCP 이름은 도구 하나를 가리키는 mcp__<server>__<tool> 모양이어야 한다. 서버 하나나 패턴으로는 막지 못한다. 모양이 아니면 400(mcp_tool_name), 내장 GitHub 서버의 도구면 400(github_server)이다. 이 설치가 목록에 올리지 않은 서버면 400(unknown_mcp_server)이다. 이전 버전이 이미 막고 있던 이름은 서버가 목록에서 빠졌어도 다시 따지지 않는다. 응답에서 MCP 이름은 deny가 아니라 mcp_deny에 정렬되어 나온다. deny에는 내장 도구만 남는다.
  • accept_added_tools는 내장 도구 이름만 받는다. 거기에 MCP 이름을 적으면 400(mcp_tool)이다. deny와 accept_added_tools 모두 Bash(rm:*) 같은 패턴은 400(tool_pattern), 목록에 없는 이름은 400(unknown_tool)이다. 허용 목록(tools)으로 저장해도 이전 버전이 막은 mcp__ 이름은 그대로 이어진다. 활성 release의 프로필이 막은 MCP 도구는 에이전트의 MCP 설정에서도 새로 허용할 수 없다(mcp_tool_denied).
  • MCP 차단은 다음 버전으로 이어지고, unblock_mcp_tools로만 풀린다. 새 버전은 이전 버전이 막은 mcp__ 이름을 그대로 이어받는다. 풀려면 그 이름을 unblock_mcp_tools에 적는다. 이전 버전이 막지 않은 이름이면 400(mcp_unblock_unknown), 같은 이름을 deny에도 적었으면 400(mcp_unblock_conflict), 이전 버전이 없으면 400(mcp_unblock_no_previous)이다. 워크스페이스 프로필(POST /v1/workspace-profiles)의 새 버전은 이전 버전을 previous_id로 가리킨다. 그 사이 같은 이름에 더 새 버전이 생겼으면 409(stale_previous)다.
  • 바탕 프로필이 막은 MCP 이름은 모든 파생 버전이 deny에 직접 적는다. 빠뜨리거나 unblock_mcp_tools로 풀려 하면 400(base_mcp_deny_dropped)이다.
  • tools와 deny 중 하나. 요청은 둘 중 정확히 하나를 보낸다. 둘 다 보내거나 둘 다 빼면 400이다. accept_added_tools는 deny와 함께만 보낸다. tools로 보내면 지금처럼 그 목록 그대로 저장한다. 지원 목록이 없는 runtime 버전은 tools만 받고 deny는 400(no_tool_catalog)이다.
  • 워크스페이스 프로필은 바탕을 넘지 않는다. tools는 바탕 프로필의 tools 중 지원 목록에 있는 도구에서 deny를 뺀 것이다. 그래서 deny: []도 바탕이 쓰지 않는 도구는 열지 않는다. 바탕이 deny로 만든 버전이고 지원 목록이 같으면, 바탕이 막은 도구를 deny에서 빼는 요청은 400(base_denies)이다.
  • 기존 버전은 그대로다. tools로 만든 버전도 목록의 deny에는 지원 목록에서 tools를 뺀 것으로 보인다. 넓히려면 deny로 새 버전을 만든다.

아래 예제는 만들기의 WEB 헤더로 owner가 부른다. 먼저 지원 목록에서 worker 밖으로 나가는 도구를 본다.

curl -sS "$API/v1/installation-profiles/options" "${WEB[@]}" \
  | jq -c '.tool_catalogs[0].tools | map(select(.boundary != "local"))'
[{"name":"WebFetch","boundary":"egress"},{"name":"WebSearch","boundary":"provider"}]

로컬 프로필 다음 버전을 WebSearch만 막아 만든다. 로컬 프로필은 tools로 만든 버전이라 NotebookEdit·WebFetch는 따져 본 적이 없는 도구다.

next=$(jq -nc '{name: "coding-local", previous_id: "claude-coding-local",
  connection_id: "fake-messages", runtime_kind: "claude_agent_sdk", runtime_version: "0.3.319",
  model: "claude-sonnet-5", models: ["claude-sonnet-5"], permission_mode: "default",
  repository_ids: ["sample-app"], deny: ["WebSearch"]}')
curl -sS "$API/v1/installation-profiles" "${WEB[@]}" -d "$next" | jq -c '.error.details'
curl -sS "$API/v1/installation-profiles" "${WEB[@]}" \
  -d "$(echo "$next" | jq -c '.accept_added_tools = ["NotebookEdit", "WebFetch"]')" \
  | jq -c '.profile | {id, tools, deny}'
{"reason_code":"added_tool_not_accepted","field":"accept_added_tools"}
{"id":"op_coding-local_v1","tools":["Read","Edit","Write","Glob","Grep","NotebookEdit","Bash","AskUserQuestion","WebFetch"],"deny":["WebSearch"]}

레퍼런스: POST /v1/installation-profiles

진행 중인 세션의 프로필 바꾸기

sessions.changeProfile(POST /v1/sessions/{id}/profile)로 바꾼다. 돌고 있는 turn은 시작한 프로필로 끝나고, 그다음 입력부터 새 프로필로 돈다. 대화와 작업 트리는 마지막 checkpoint에서 이어진다. 같은 프로필로 바꾸면 아무것도 바뀌지 않고 receipt만 남는다.

import { waitForTurn } from "@werft-ai/sdk";

await waitForTurn(werft, sessionId, created.turn_id);
const { revision } = await werft.sessions.get(sessionId);
const changed = await werft.sessions.changeProfile(sessionId, {
  expected_revision: revision,
  profile_id: "claude-coding-local",
});
console.log(changed.receipt_status);
succeeded

새 프로필이 runtime 종류·버전이나 과금 방식이 다르거나 그 저장소와 짝이 아니면 422 UNSUPPORTED_CAPABILITY다. 에이전트 release로 만든 세션은 release가 프로필을 정하므로 409 RELEASE_PROFILE_MISMATCH다. scope는 sessions:control이다.

워크스페이스 프로필 만들기

워크스페이스 owner와 admin은 운영자 프로필 하나를 바탕으로 이 워크스페이스에서만 쓰는 프로필을 만든다. 콘솔의 워크스페이스 > 프로필에서 프로필 만들기를 누른다.

  • 줄이기만 된다. 모델과 도구는 바탕 프로필에 있는 것 중에서 고르고, 권한 모드는 바탕 프로필과 같거나 사람이 더 많이 확인하는 모드만 고른다. 새 프로필은 바탕 프로필이 auto를 허용하면(바탕이 auto나 bypassPermissions) auto로 시작하고, 프로필 만들기는 그런 바탕 프로필을 먼저 고른다. 바탕이 허용하지 않는 auto·bypassPermissions는 비활성 선택지로 보인다. bypassPermissions 바탕은 운영자가 설치 설정을 켠 설치에만 있다. bypassPermissions를 고르면 위험 안내가 보인다. runtime, provider, 자격 증명, 저장소 짝은 바탕 프로필의 것을 그대로 쓴다.
  • 저장한 버전은 바뀌지 않는다. 수정해서 저장할 때마다 새 버전 id ws_<워크스페이스 slug>_<이름>_v<n>이 생기고, 화면은 저장 전에 그 id를 보여 준다. 이전 버전을 쓰는 release는 새 버전으로 release를 다시 만들어야 바뀐다. 목록은 이름마다 최신 버전 한 줄을 보여 주고, 이전 버전은 이전 버전 N개를 눌러 펼친다.
  • 행을 누르면 상세 화면(/profiles/<id>)이 열린다. 모델·도구·권한 모드·바탕 프로필, 같은 이름의 버전 기록, 활성 release가 그 버전을 쓰는 에이전트가 나온다. 에이전트 목록은 에이전트를 관리할 수 있는 사람에게만 보인다. 소유자·관리자는 사용 가능한 버전의 상세에서 수정으로 지금 값이 채워진 편집 화면을 연다(바탕 프로필 바뀜이면 다시 만들기, 사용 중지됐거나 바탕 프로필이 없는 버전에는 버튼이 없다). 운영자 프로필 상세에서는 이 프로필로 워크스페이스 프로필 만들기로 새 워크스페이스 프로필을 시작한다.
  • 목록(GET /v1/profiles)에는 운영자 프로필(source: "operator")과 이 워크스페이스의 버전(source: "workspace")이 함께 나온다. 콘솔은 운영자 프로필을 고치지 못한다. 운영자 카탈로그는 ws_로 시작하는 id를 쓸 수 없어서 둘이 겹치지 않는다.
  • 운영자가 바탕 프로필을 바꾸면 버전은 바탕 프로필 바뀜, 지우면 바탕 프로필 없음, 차단하면 바탕 프로필 차단됨이 된다. 그 버전으로는 새 release와 세션을 만들 수 없다. 바탕 프로필이 바뀐 버전은 다시 만들기로 새 버전을 만든다.
  • 사용 중지됨 버전은 새 release와 세션이 고를 수 없다. 이미 그 버전을 쓰는 release와 세션은 계속 돈다. 사용 중지는 되돌릴 수 없다.
  • release의 프로필 목록은 운영자 프로필과 워크스페이스 프로필을 나눠 보여 주고, 새 release를 받지 않는 버전은 고를 수 없다.
  • 구성원은 목록만 본다. 만들기·수정·사용 중지는 콘솔에 로그인한 owner·admin만 할 수 있고(POST /v1/workspace-profiles, POST /v1/workspace-profiles/{profile_id}/retire), API key로는 403이다.

auto 규칙과 스크립트 상한은 화면에 자리만 있고 아직 고칠 수 없다.

운영자 프로필 차단하기

owner는 운영자 프로필 하나를 지금 도는 세션까지 멈출 수 있다. 사용 중지(retire)는 새 release와 세션만 막지만, 차단은 그 프로필로 도는 모든 것을 막는다. 웹 로그인 cookie로 owner만 한다. admin·member와 API key는 403이다. 콘솔에서는 운영자 프로필 상세의 차단·차단 해제로 하고, 확인 창을 거친다. 아래 예제는 만들기의 WEB 헤더를 쓴다.

curl -sS -X POST "$API/v1/installation-profiles/claude-coding-local/block" "${WEB[@]}" \
  | jq -c '{state: .profile.installation.state, receipt_id}'
{"state":"blocked","receipt_id":"…"}
  • 프로필은 차단됨(blocked)이 되고, 그 위의 워크스페이스 프로필은 바탕 프로필 차단됨(base_blocked)이 된다.
  • 새 release·release 활성화·세션은 409 PROFILE_BLOCKED로 거절된다. error.details.reason_code는 blocked(고른 프로필이 차단됨)나 base_blocked(그 바탕이 차단됨)이고, error.details.profile_id가 고른 프로필이다. 그 프로필의 기존 세션은 새 메시지를 409 PROFILE_BLOCKED로 거절하고(차단 전에 받은 메시지의 같은 Idempotency-Key 재전송은 원래 응답을 돌려준다), GET /v1/sessions/{id}의 attention이 PROFILE_BLOCKED가 된다. 차단 전에 이미 쌓인 턴은 시작할 때 catalog_mismatch로 끝나고, 지금 도는 턴은 모델·저장소 요청이 1초 남짓 안에 끊긴다.
  • POST /v1/installation-profiles/{profile_id}/unblock으로 풀면 원래 상태로 돌아온다. 끝난 세션과 턴은 저절로 다시 돌지 않는다. 필요한 입력을 다시 보낸다.
  • 이미 그 상태면 아무것도 바뀌지 않고 receipt_id가 null이다.

다음 단계

  • 권한: 도구 목록과 권한 모드가 하는 일
  • 시작: 세션 생성 요청

이 페이지