---
title: 자체 호스팅 서버에서 API 호출하기
url: https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api
description: 공식 SDK, cURL 또는 ant CLI가 자체 호스팅 Claude Managed Agents 서버를 가리키도록 설정하고, 세션을 시작한 다음 에이전트의 응답을 읽습니다. 그런 다음 필요하면 직접 운영하는 워커를 연결합니다.
---

<Note>
  자체 호스팅 Claude Managed Agents에 대한 접근 권한을 요청하려면 Anthropic 계정 팀에 문의하세요.
</Note>

자체 호스팅 Claude Managed Agents 서버는 Anthropic의 호스팅 [Managed Agents](https://platform.claude.com/docs/ko/managed-agents/overview) 서비스와 동일한 API를 제공합니다. 코드에서는 공식 SDK, cURL 또는 `ant` CLI로 이 서버를 호출합니다. 달라지는 점은 두 가지입니다. 기본 URL이 사용자 서버의 주소가 되고, API 키가 Claude API 키 대신 [서버 API 키](https://platform.claude.com/docs/ko/managed-agents/self-hosted#the-three-credentials)가 됩니다.

브라우저에서 에이전트를 생성하고 세션을 실행하려면 [사용자 서버의 콘솔 사용하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/console)를 참조하세요.

## 사전 요구 사항

* **실행 중인 서버:** [자체 호스팅 Claude Managed Agents 시작하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/quickstart) 또는 [Kubernetes에 배포하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/kubernetes)로 서버를 시작하세요.
* **서버에 대한 접근 권한:** 다른 사람이 서버를 운영하는 경우 서버 주소와 인증 방법을 문의하세요. 해당 서버에서 “single sign-on”(싱글 사인온)을 사용한다면 [개발자가 터미널에서 로그인하도록 허용](https://platform.claude.com/docs/ko/managed-agents/self-hosted/configure#let-developers-log-in-from-a-terminal)을 참조하세요.
* **`ant` CLI:** [CLI 빠른 시작](https://platform.claude.com/docs/ko/cli-sdks-libraries/cli/quickstart#installation)에 나온 대로 최신 버전을 설치하세요.

## 클라이언트가 서버를 가리키도록 설정하기

SDK와 `ant` CLI는 `ANTHROPIC_BASE_URL`에서 서버 주소를, `ANTHROPIC_API_KEY`에서 API 키를 읽습니다. 이 서버용으로 유지하는 터미널에서 두 변수를 모두 설정하세요.

[빠른 시작 스택](https://platform.claude.com/docs/ko/managed-agents/self-hosted/quickstart)의 경우 `.env` 파일이 있는 디렉터리에서 다음 명령을 실행하세요:

```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8400
export ANTHROPIC_API_KEY=$(grep '^CMA_SERVER__API_KEY=' .env | cut -d= -f2)
```

Kubernetes에서는 [세션 실행](https://platform.claude.com/docs/ko/managed-agents/self-hosted/kubernetes#run-a-session)에 나온 대로 두 변수를 설정하고, 포트 포워딩을 계속 실행한 상태로 두세요.

해당 터미널에서는 `ant beta:agents list`와 같은 모든 `ant` 명령이 사용자의 서버에 연결됩니다. Claude Code와 `docker compose`는 같은 변수를 읽으므로 그 터미널에서 실행하지 마세요.

## 세션 시작하기

세션에는 에이전트의 ID와 환경의 ID가 필요합니다:

* **에이전트:** 클라이언트가 서버를 가리키도록 설정한 상태에서 [에이전트 생성하기](https://platform.claude.com/docs/ko/managed-agents/agent-setup#create-an-agent)에 나온 대로 에이전트를 생성하세요. 원시 HTTP 요청을 보내는 경우 URL의 `https://api.anthropic.com`을 `$ANTHROPIC_BASE_URL`로 바꾸세요.
* **환경:** 빠른 시작 스택은 `compose-sandbox`라는 환경을 생성하고, Helm 차트는 `default`라는 환경을 생성합니다. `ant beta:environments list`가 각 환경의 이름과 ID를 출력합니다. 직접 운영하는 워커의 경우, [자체 워커 실행하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api#point-a-worker-at-your-server)에서 관리자 CLI가 출력한 ID입니다.

다음 예시 ID 대신 두 ID를 셸에 설정하세요:

```bash
export AGENT_ID=agent_011CYm1BLqPXpQRk5khsSXrs
export ENVIRONMENT_ID=env_01595EKxaaTTGwwY3kyXdtbs
```

다음 예제는 세션을 생성하고 같은 요청에서 첫 번째 메시지를 보냅니다:

<CodeGroup>
  ```bash cURL
  curl -sS --fail-with-body "$ANTHROPIC_BASE_URL/v1/sessions" \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2026-10-21" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    -H "content-type: application/json" \
    -d '{
      "agent": "'"$AGENT_ID"'",
      "environment_id": "'"$ENVIRONMENT_ID"'",
      "initial_events": [
        {
          "type": "user.message",
          "content": [{"type": "text", "text": "List the files in the working directory."}]
        }
      ]
    }'
  ```

  ```bash CLI
  ant beta:sessions create --agent "$AGENT_ID" --environment-id "$ENVIRONMENT_ID" \
    --initial-event '{type: user.message, content: [{type: text, text: "List the files in the working directory."}]}'
  ```

  ```python Python
  import os

  from anthropic import Anthropic

  client = Anthropic()

  session = client.beta.sessions.create(
      agent=os.environ["AGENT_ID"],
      environment_id=os.environ["ENVIRONMENT_ID"],
      initial_events=[
          {
              "type": "user.message",
              "content": [
                  {"type": "text", "text": "List the files in the working directory."}
              ],
          },
      ],
  )
  print(session.id)
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";

  const client = new Anthropic();

  const session = await client.beta.sessions.create({
    agent: process.env["AGENT_ID"]!,
    environment_id: process.env["ENVIRONMENT_ID"]!,
    initial_events: [
      {
        type: "user.message",
        content: [{ type: "text", text: "List the files in the working directory." }]
      }
    ]
  });
  console.log(session.id);
  ```

  ```csharp C#
  using System;
  using Anthropic;
  using Anthropic.Models.Beta.Agents;
  using Anthropic.Models.Beta.Sessions;
  using Anthropic.Models.Beta.Sessions.Events;

  AnthropicClient client = new();

  var session = await client.Beta.Sessions.Create(new()
  {
      Agent = Environment.GetEnvironmentVariable("AGENT_ID")!,
      EnvironmentID = Environment.GetEnvironmentVariable("ENVIRONMENT_ID")!,
      InitialEvents =
      [
          new BetaManagedAgentsUserMessageEventParams
          {
              Content =
              [
                  new BetaManagedAgentsTextBlock
                  {
                      Text = "List the files in the working directory.",
                  },
              ],
          },
      ],
  });
  Console.WriteLine(session.ID);
  ```

  ```go Go
  package main

  import (
  	"context"
  	"fmt"
  	"os"

  	"github.com/anthropics/anthropic-sdk-go/v2"
  )

  func main() {
  	client := anthropic.NewClient()

  	session, err := client.Beta.Sessions.New(context.Background(), anthropic.BetaSessionNewParams{
  		Agent: anthropic.BetaSessionNewParamsAgentUnion{
  			OfString: anthropic.String(os.Getenv("AGENT_ID")),
  		},
  		EnvironmentID: os.Getenv("ENVIRONMENT_ID"),
  		InitialEvents: []anthropic.BetaSessionNewParamsInitialEventUnion{{
  			OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
  				Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  					OfText: &anthropic.BetaManagedAgentsTextBlockParam{
  						Text: "List the files in the working directory.",
  					},
  				}},
  			},
  		}},
  	})
  	if err != nil {
  		panic(err)
  	}
  	fmt.Println(session.ID)
  }
  ```

  ```java Java
  import com.anthropic.client.AnthropicClient;
  import com.anthropic.client.okhttp.AnthropicOkHttpClient;
  import com.anthropic.models.beta.sessions.SessionCreateParams;
  import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams;

  void main() {
      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      var session = client.beta().sessions().create(SessionCreateParams.builder()
          .agent(System.getenv("AGENT_ID"))
          .environmentId(System.getenv("ENVIRONMENT_ID"))
          .addInitialEvent(BetaManagedAgentsUserMessageEventParams.builder()
              .addTextContent("List the files in the working directory.")
              .build())
          .build());
      IO.println(session.id());
  }
  ```

  ```php PHP
  <?php

  use Anthropic\Client;

  $client = new Client();

  $session = $client->beta->sessions->create(
      agent: (string) getenv('AGENT_ID'),
      environmentID: (string) getenv('ENVIRONMENT_ID'),
      initialEvents: [
          [
              'type' => 'user.message',
              'content' => [['type' => 'text', 'text' => 'List the files in the working directory.']],
          ],
      ],
  );
  echo $session->id, "\n";
  ```

  ```ruby Ruby
  require "anthropic"

  client = Anthropic::Client.new

  session = client.beta.sessions.create(
    agent: ENV.fetch("AGENT_ID"),
    environment_id: ENV.fetch("ENVIRONMENT_ID"),
    initial_events: [
      {
        type: :"user.message",
        content: [{type: :text, text: "List the files in the working directory."}]
      }
    ]
  )
  puts session.id
  ```
</CodeGroup>

응답에는 `sesn_`으로 시작하는 세션 ID가 포함됩니다. 에이전트는 즉시 작업을 시작합니다. `bash`와 같은 에이전트의 내장 도구는 해당 환경을 처리하는 워커에서 실행됩니다.

세션을 지켜보려면 브라우저에서 [사용자 서버의 콘솔](https://platform.claude.com/docs/ko/managed-agents/self-hosted/console)을 열고 **Sessions**를 선택한 다음 해당 세션을 선택하세요. 이 페이지에서처럼 서버 API 키로 시작한 세션은 관리자에게만 표시됩니다.

대신 터미널에서 세션을 따라가려면 세션 ID를 [`ant beta:sessions connect`](https://platform.claude.com/docs/ko/cli-sdks-libraries/cli/sessions-connect)에 전달하세요:

```bash
ant beta:sessions connect sesn_011CZkZAtmR3yMPDzynEDxu7
```

에이전트 버전 고정 등 세션으로 할 수 있는 그 밖의 작업은 [세션 시작하기](https://platform.claude.com/docs/ko/managed-agents/sessions)를 참조하세요.

## 메시지 보내기 및 응답 읽기

다음 예시 ID 대신 세션 ID를 셸에 설정하세요:

```bash
export SESSION_ID=sesn_011CZkZAtmR3yMPDzynEDxu7
```

다음 예제를 실행하기 전에 세션이 유휴 상태가 될 때까지 기다리세요. 예제는 턴이 끝나면 읽기를 중지합니다. 이전 턴이 아직 실행 중이면, 그 턴이 끝나면서 메시지에 대한 응답이 도착하기 전에 예제가 중지될 수 있습니다. 세션의 상태는 콘솔과 `ant beta:sessions connect` 모두에서 확인할 수 있습니다.

다음 예제는 메시지를 보내고 에이전트의 응답을 출력합니다. 스트림은 열린 이후에 발생하는 이벤트만 전달하므로, 예제에서는 먼저 세션의 이벤트 스트림을 엽니다:

<CodeGroup exclude="shell">
  <CodeGroupItem language="cURL">
    이 워크플로에는 단일 셸 명령보다 더 많은 로직이 필요합니다. [Anthropic SDK](https://platform.claude.com/docs/ko/cli-sdks-libraries/overview)를 사용하여 실행하세요.
  </CodeGroupItem>

  <CodeGroupItem language="CLI">
    이 워크플로에는 단일 셸 명령보다 더 많은 로직이 필요합니다. 터미널에서 메시지를 보내고 응답을 읽으려면 [`ant beta:sessions connect`](https://platform.claude.com/docs/ko/cli-sdks-libraries/cli/sessions-connect)를 사용하세요.
  </CodeGroupItem>

  ```python Python
  import os

  from anthropic import Anthropic

  client = Anthropic()
  session_id = os.environ["SESSION_ID"]

  with client.beta.sessions.events.stream(session_id) as stream:
      client.beta.sessions.events.send(
          session_id,
          events=[
              {
                  "type": "user.message",
                  "content": [
                      {"type": "text", "text": "Summarize what you did in one sentence."}
                  ],
              },
          ],
      )

      for event in stream:
          match event.type:
              case "agent.message":
                  for block in event.content:
                      if block.type == "text":
                          print(block.text, end="")
              case "session.error":
                  error_message = event.error.message if event.error else "unknown"
                  print(f"\n[Error: {error_message}]")
              case "session.status_idle":
                  if event.stop_reason.type != "requires_action":
                      break
              case "session.status_terminated":
                  break
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";

  const client = new Anthropic();
  const sessionId = process.env["SESSION_ID"]!;

  const stream = await client.beta.sessions.events.stream(sessionId);
  await client.beta.sessions.events.send(sessionId, {
    events: [
      {
        type: "user.message",
        content: [{ type: "text", text: "Summarize what you did in one sentence." }]
      }
    ]
  });

  events: for await (const event of stream) {
    switch (event.type) {
      case "agent.message":
        for (const block of event.content) {
          if (block.type === "text") {
            process.stdout.write(block.text);
          }
        }
        break;
      case "session.error":
        console.log(`\n[Error: ${event.error?.message ?? "unknown"}]`);
        break;
      case "session.status_idle":
        if (event.stop_reason.type !== "requires_action") {
          break events;
        }
        break;
      case "session.status_terminated":
        break events;
    }
  }
  ```

  ```csharp C#
  using System;
  using Anthropic;
  using Anthropic.Models.Beta.Sessions.Events;

  AnthropicClient client = new();
  var sessionId = Environment.GetEnvironmentVariable("SESSION_ID")!;

  using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(sessionId);
  await client.Beta.Sessions.Events.Send(sessionId, new()
  {
      Events =
      [
          new BetaManagedAgentsUserMessageEventParams
          {
              Content = [new BetaManagedAgentsTextBlock { Text = "Summarize what you did in one sentence." }],
          },
      ],
  });

  await foreach (var streamEvent in stream.Enumerate())
  {
      if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
      {
          foreach (var block in message.Content)
          {
              if (block.Value is BetaManagedAgentsTextBlock textBlock)
              {
                  Console.Write(textBlock.Text);
              }
          }
      }
      else if (streamEvent.Value is BetaManagedAgentsSessionErrorEvent error)
      {
          Console.WriteLine($"\n[Error: {error.Error?.Message ?? "unknown"}]");
      }
      else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent idle
          && idle.StopReason?.Value is not BetaManagedAgentsSessionRequiresAction)
      {
          break;
      }
      else if (streamEvent.Value is BetaManagedAgentsSessionStatusTerminatedEvent)
      {
          break;
      }
  }
  ```

  ```go Go
  package main

  import (
  	"cmp"
  	"context"
  	"fmt"
  	"os"

  	"github.com/anthropics/anthropic-sdk-go/v2"
  )

  func main() {
  	client := anthropic.NewClient()
  	ctx := context.Background()
  	sessionID := os.Getenv("SESSION_ID")

  	stream := client.Beta.Sessions.Events.StreamEvents(ctx, sessionID, anthropic.BetaSessionEventStreamParams{})
  	defer stream.Close()

  	if _, err := client.Beta.Sessions.Events.Send(ctx, sessionID, anthropic.BetaSessionEventSendParams{
  		Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
  			OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
  				Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
  					OfText: &anthropic.BetaManagedAgentsTextBlockParam{
  						Text: "Summarize what you did in one sentence.",
  					},
  				}},
  			},
  		}},
  	}); err != nil {
  		panic(err)
  	}

  events:
  	for stream.Next() {
  		switch event := stream.Current().AsAny().(type) {
  		case anthropic.BetaManagedAgentsAgentMessageEvent:
  			for _, block := range event.Content {
  				if block.Type == "text" {
  					fmt.Print(block.Text)
  				}
  			}
  		case anthropic.BetaManagedAgentsSessionErrorEvent:
  			fmt.Printf("\n[Error: %s]\n", cmp.Or(event.Error.Message, "unknown"))
  		case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
  			if _, paused := event.StopReason.AsAny().(anthropic.BetaManagedAgentsSessionRequiresAction); !paused {
  				break events
  			}
  		case anthropic.BetaManagedAgentsSessionStatusTerminatedEvent:
  			break events
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
  }
  ```

  ```java Java
  import com.anthropic.client.AnthropicClient;
  import com.anthropic.client.okhttp.AnthropicOkHttpClient;
  import com.anthropic.core.JsonObject;
  import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams;
  import com.anthropic.models.beta.sessions.events.EventSendParams;

  void main() {
      AnthropicClient client = AnthropicOkHttpClient.fromEnv();
      var sessionId = System.getenv("SESSION_ID");

      try (var stream = client.beta().sessions().events().streamStreaming(sessionId)) {
          client.beta().sessions().events().send(
              sessionId,
              EventSendParams.builder()
                  .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
                      .addTextContent("Summarize what you did in one sentence.")
                      .build())
                  .build()
          );

          events:
          for (var event : stream) {
              switch (event.type().value()) {
                  case AGENT_MESSAGE -> {
                      for (var block : event.asAgentMessage().content()) {
                          block.text().ifPresent(textBlock -> IO.print(textBlock.text()));
                      }
                  }
                  case SESSION_ERROR -> {
                      var errorMessage =
                          event.asSessionError().error()._json().orElse(null) instanceof JsonObject json
                              ? json.values().get("message").asStringOrThrow()
                              : "unknown";
                      IO.println("\n[Error: " + errorMessage + "]");
                  }
                  case SESSION_STATUS_IDLE -> {
                      if (!event.asSessionStatusIdle().stopReason().isRequiresAction()) {
                          break events;
                      }
                  }
                  case SESSION_STATUS_TERMINATED -> {
                      break events;
                  }
                  default -> {}
              }
          }
      }
  }
  ```

  ```php PHP
  <?php

  use Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent;
  use Anthropic\Beta\Sessions\Events\ManagedAgentsSessionErrorEvent;
  use Anthropic\Beta\Sessions\Events\ManagedAgentsSessionRequiresAction;
  use Anthropic\Beta\Sessions\Events\ManagedAgentsSessionStatusIdleEvent;
  use Anthropic\Beta\Sessions\Events\ManagedAgentsSessionStatusTerminatedEvent;
  use Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock;
  use Anthropic\Client;

  $client = new Client();
  $sessionId = (string) getenv('SESSION_ID');

  $stream = $client->beta->sessions->events->streamStream($sessionId);
  $client->beta->sessions->events->send(
      $sessionId,
      events: [
          [
              'type' => 'user.message',
              'content' => [['type' => 'text', 'text' => 'Summarize what you did in one sentence.']],
          ],
      ],
  );

  foreach ($stream as $event) {
      switch (true) {
          case $event instanceof ManagedAgentsAgentMessageEvent:
              foreach ($event->content as $block) {
                  if ($block instanceof ManagedAgentsTextBlock) {
                      echo $block->text;
                  }
              }
              break;
          case $event instanceof ManagedAgentsSessionErrorEvent:
              printf("\n[Error: %s]\n", $event->error->message);
              break;
          case $event instanceof ManagedAgentsSessionStatusIdleEvent:
              if (!($event->stopReason instanceof ManagedAgentsSessionRequiresAction)) {
                  break 2; // Break foreach loop
              }
              break;
          case $event instanceof ManagedAgentsSessionStatusTerminatedEvent:
              break 2; // Break foreach loop
      }
  }
  $stream->close();
  ```

  ```ruby Ruby
  require "anthropic"

  client = Anthropic::Client.new
  session_id = ENV.fetch("SESSION_ID")

  stream = client.beta.sessions.events.stream_events(session_id)

  client.beta.sessions.events.send_(
    session_id,
    events: [{
      type: "user.message",
      content: [{type: "text", text: "Summarize what you did in one sentence."}]
    }]
  )

  stream.each do |event|
    case event
    when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
      event.content.each do |block|
        print block.text if block.is_a?(Anthropic::Beta::Sessions::BetaManagedAgentsTextBlock)
      end
    when Anthropic::Beta::Sessions::BetaManagedAgentsSessionErrorEvent
      puts "\n[Error: #{event.error&.message || "unknown"}]"
    when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
      break unless event.stop_reason.is_a?(Anthropic::Beta::Sessions::BetaManagedAgentsSessionRequiresAction)
    when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusTerminatedEvent
      break
    else
      # 다른 이벤트 유형은 무시합니다
    end
  end
  ```
</CodeGroup>

루프는 `agent.message` 텍스트를 출력하는 것 외에 세 가지 이벤트를 처리합니다:

* **`session.status_idle`:** 이벤트의 `stop_reason.type`이 `requires_action`이 아니면 루프가 끝납니다.
* **`session.status_terminated`:** 루프가 끝납니다.
* **`session.error`:** 서버가 실패한 모델 호출을 재시도하고 턴을 마칠 수 있으므로 루프는 오류 메시지를 출력하고 계속 읽습니다.

`requires_action`은 세션이 도구 호출을 위해 일시 중지되었음을 의미합니다. 일시 중지를 끝내는 방법은 호출에 따라 다릅니다:

* **커스텀 도구 호출 또는 도구 호출 확인 요청:** 이에 응답할 수 있는 것은 사용자의 코드뿐입니다. 에이전트가 둘 중 하나를 사용한다면 [세션을 일시 중지시키는 도구 호출에 응답하기](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming#answer-tool-calls-that-pause-the-session)를 참조하세요.
* **`bash`와 같이 워커가 실행하는 도구에 대한 호출:** 이 호출은 일반적으로 세션을 일시 중지하지 않습니다. 약 3분 동안 어떤 워커도 호출을 가져가지 않으면, 세션은 워커가 결과를 반환할 때까지 일시 중지됩니다. 이 경우 코드에서 응답할 것은 없습니다. 스트림을 열어 두면 워커가 호출을 실행할 때 턴이 재개됩니다.

세션이 보내는 그 밖의 이벤트, 그리고 하나도 놓치지 않고 다시 연결하는 방법은 [세션 이벤트 스트림](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming)을 참조하세요.

## 세션 안팎으로 데이터 이동하기

데이터는 워커의 작업 디렉터리를 통해 세션에 전달되고 세션에서 나옵니다.

빠른 시작 스택에서는 각 세션의 컨테이너가 세션 저장소의 클론을 제외하면 빈 `/workspace` 디렉터리로 시작합니다. 기본적으로 턴이 끝나고 약 1분 후에 컨테이너가 종료되며, Docker가 해당 디렉터리와 함께 컨테이너를 삭제합니다. 세션이 보존해야 하는 모든 것은 그 전에 업로드나 `git push`와 같은 도구 호출을 통해 밖으로 내보내야 합니다.

컨테이너는 시작된 후 약 1시간이 지나면 턴 도중이라도 종료됩니다. 이는 어떤 설정으로도 바꿀 수 없습니다. 턴은 보통 1\~2분 후 새 컨테이너에서 계속되며, 클라이언트는 이 전환에 대한 이벤트를 받지 않습니다. 이 전환에는 두 가지 비용이 따릅니다:

* **명령:** 이전 컨테이너에서 실행 중이던 모든 것이 중지됩니다. 새 컨테이너는 중단된 도구 호출을 처음부터 다시 실행하지만, 에이전트가 백그라운드에서 실행 중으로 남겨 둔 명령은 아무것도 다시 시작하지 않습니다.
* **파일:** 새 컨테이너의 `/workspace`는 세션 저장소의 새 클론을 제외하면 다시 비어 있습니다.

긴 턴에서는 에이전트가 작업을 진행하면서 결과물을 업로드하거나 푸시하도록 하고, 두 번 실행되면 문제를 일으키는 명령은 피하세요.

Kubernetes에서는 세션이 하나의 파드를 최대 약 1시간 동안 유지하며, 턴은 같은 방식으로 계속됩니다. 다른 점은 두 가지입니다. 새 파드는 `/workspace`에서 세션의 파일을 찾으며, 이전 파드가 중지된 후 약 6분 후에 시작됩니다.

직접 시작하는 워커의 경우, 입력 파일이 필요한 메시지를 보내기 전에 해당 파일을 작업 디렉터리에 넣으세요. 이후 출력 파일을 그곳에서 읽으세요.

세션마다 컨테이너를 직접 시작하는 경우, 세션의 입력 파일이 들어 있는 디렉터리를 컨테이너의 작업 디렉터리로 마운트하세요. 컨테이너가 종료된 후 그 디렉터리에서 출력 파일을 수집하세요. [세션용 파일 준비](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#stage-files-for-a-session)를 참조하세요.

## 호스팅 가이드에서 더 많은 예제 찾기

호스팅 서비스의 가이드에서는 API의 [핵심 개념](https://platform.claude.com/docs/ko/managed-agents/overview#core-concepts)을 설명하고 각 기능의 예제를 제공합니다. 이 가이드들의 SDK 및 CLI 예제는 `ANTHROPIC_BASE_URL`과 `ANTHROPIC_API_KEY`가 설정된 터미널에서 실행하면 서버를 호출합니다. 이 가이드들의 cURL 예제는 `https://api.anthropic.com`을 지정하므로, 실행하기 전에 해당 주소를 `$ANTHROPIC_BASE_URL`로 바꾸세요.

일부 기능은 먼저 서버에 설정이 필요하거나 예제를 변경해야 합니다:

| 작업                         | 호스팅 가이드                                                                                                                                             | 사용자 서버에서                                                                                                                                                                                                                                        |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 에이전트의 모델, 시스템 프롬프트 및 버전 설정 | [에이전트 정의하기](https://platform.claude.com/docs/ko/managed-agents/agent-setup)                                                                         | 일부 필드는 [추론 공급자에 따라 달라집니다](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences#differences-that-depend-on-your-inference-provider).                                                                                       |
| 내장 도구 선택 또는 커스텀 도구 추가      | [도구](https://platform.claude.com/docs/ko/managed-agents/tools)                                                                                      | 웹 도구를 사용하려면 [웹 도구에는 서버 설정이 필요합니다](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences#web-tools-need-a-server-setting)에 설명된 대로 먼저 웹 도구를 켜세요. 어떤 웹 도구가 있는지는 추론 공급자에 따라 다릅니다.                                              |
| MCP 서버 연결 및 토큰 저장          | [MCP 커넥터](https://platform.claude.com/docs/ko/managed-agents/mcp-connector) 및 [볼트로 인증하기](https://platform.claude.com/docs/ko/managed-agents/vaults) | MCP 서버의 호스트를 [MCP 서버 허용 목록](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences#mcp-servers-need-an-allowlist-entry)에 추가하세요. 토큰은 `static_bearer` 자격 증명으로 저장하세요.                                                          |
| 에이전트에 스킬 부여                | [스킬](https://platform.claude.com/docs/ko/managed-agents/skills)                                                                                     | 서버가 zip 아카이브를 거부하므로 각 스킬을 개별 파일로 업로드하세요. 해당 가이드 예제의 [사전 빌드된 스킬](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences#features-that-arent-available)은 사용할 수 없습니다.                                                          |
| 세션 간 메모리 유지                | [에이전트 메모리 사용하기](https://platform.claude.com/docs/ko/managed-agents/memory)                                                                          | 직접 워커를 실행하는 경우 [자체 호스팅 샌드박스의 메모리 저장소](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-memory)에 나온 대로 워커에 쓰기 가능한 `/mnt/memory` 디렉터리를 제공하세요.                                                                             |
| 다른 에이전트에 작업 넘기기            | [멀티에이전트 오케스트레이션](https://platform.claude.com/docs/ko/managed-agents/multiagent-orchestration)                                                       | 해당 가이드 예제의 `multiagent_20261001` 블록을 [`coordinator` 블록](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences#features-that-behave-differently)으로 바꾸세요.                                                                    |
| Git 저장소에서 작업하기             | [GitHub 액세스하기](https://platform.claude.com/docs/ko/managed-agents/github)                                                                           | [Git 저장소에는 허용 목록 항목이 필요합니다](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences#git-repositories-need-an-allowlist-entry)에 설명된 대로 서버 설정에 Git 호스트를 등록하세요. 해당 가이드의 예제 에이전트에는 MCP 서버 허용 목록에 `api.githubcopilot.com`도 필요합니다. |
| 채점자가 루브릭에 따라 작업 확인         | [결과 정의하기](https://platform.claude.com/docs/ko/managed-agents/define-outcomes)                                                                       | 루브릭을 텍스트로 보내세요. Files API를 사용할 수 없으므로 작업 디렉터리에서 [출력 파일을 수집하세요](https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api#move-data-in-and-out-of-a-session).                                                                  |
| 세션 따라가기 및 도구 호출에 응답하기      | [세션 이벤트 스트림](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming)                                                               | 해당 가이드의 첫 번째 예제는 일시 중지가 발생하면 스트림을 닫으므로, 대신 [메시지 보내기 및 응답 읽기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api#send-a-message-and-read-the-reply)의 루프에서 시작하세요.                                                        |

[호스팅 서비스와의 차이점](https://platform.claude.com/docs/ko/managed-agents/self-hosted/differences)에 전체 목록이 있습니다. 기존 애플리케이션을 옮기기 전에 확인하세요.

## 자체 워커 실행하기

[빠른 시작 스택](https://platform.claude.com/docs/ko/managed-agents/self-hosted/quickstart)은 환경을 생성하고 해당 환경의 워커를 실행합니다. [Kubernetes에 배포하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/kubernetes)의 Helm 차트도 마찬가지입니다. 이러한 워커만 필요하다면 이 섹션을 건너뛰세요.

워커는 [자체 호스팅 샌드박스](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes)에서와 같은 방식으로 실행합니다. 즉, [자체 호스팅 워커 배포](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers)에 나온 방식대로 동일한 `ant beta:worker` 명령과 SDK 워커를 사용합니다. 예외는 웹훅으로 트리거되는 워커로, 사용자 서버는 웹훅을 보내지 않기 때문입니다.

### 환경 및 환경 키 생성하기

Claude Console이 아닌 관리자 CLI로 환경과 환경 키를 생성하세요. CLI인 `cma-server-admin`은 릴리스 아카이브의 최상위에 있으므로, 그 위치에서 명령을 실행하세요. `-base-url`에는 워커가 서버에 접근할 주소를 전달하세요:

* **빠른 시작 스택을 실행하는 머신의 워커:** `http://127.0.0.1:8400`
* **다른 머신의 워커:** [서버 앞에 배치한 TLS 프록시](https://platform.claude.com/docs/ko/managed-agents/self-hosted/kubernetes#put-tls-in-front-of-the-server)의 `https` 주소

관리자 CLI는 같은 주소로 서버를 호출하므로, 해당 주소에 접근할 수 있는 머신에서 실행하세요.

```bash
echo "$ANTHROPIC_API_KEY" | ./cma-server-admin bootstrap \
  -base-url http://127.0.0.1:8400 -name my-env -key-file -
```

이 명령은 워커 설정, 워커를 시작하는 명령, 그리고 키의 ID와 만료일을 출력합니다:

```text wrap
export ANTHROPIC_BASE_URL='http://127.0.0.1:8400'
export ANTHROPIC_ENVIRONMENT_ID='env_...'
export ANTHROPIC_ENVIRONMENT_KEY='sk-ant-oat01-...'
# then: histchars= HISTFILE=/dev/null ant beta:worker poll --workdir /workspace
# key id: 'etok_...'  expires: '...'
```

이 명령은 키를 한 번만 표시하므로 키와 키 ID, 만료일을 시크릿 매니저에 저장하세요. 키는 365일이 지나면 작동을 멈춥니다. 그 전에 [자체 워커의 환경 키 교체하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/kubernetes#replace-an-environment-key-for-your-own-workers)를 따르세요.

워커를 실행하는 머신에는 서버 API 키를 두지 마세요. 보호해야 할 것이 전혀 없는 컨테이너나 VM에서 워커를 실행하세요. 그 밖의 보안 책임은 [사용자의 보안 책임](https://platform.claude.com/docs/ko/managed-agents/self-hosted#your-security-responsibilities)을 참조하세요.

### 로컬 실험 실행하기

워커를 시작하는 셸에서 세 개의 `export` 줄을 실행하세요. 그런 다음 출력된 명령이 아닌 다음 명령으로 워커를 시작하세요. 워커는 작업 디렉터리를 생성하지 않으므로 첫 번째 명령이 워커의 작업 디렉터리를 생성합니다:

```bash
mkdir -p "$HOME/cma-workspace" "$HOME/.config/cma"
cat > "$HOME/.config/cma/worker.inputrc" <<'EOF'
$include /etc/inputrc
set disable-completion on
EOF
histchars= HISTFILE=/dev/null INPUTRC="$HOME/.config/cma/worker.inputrc" \
  ant beta:worker poll --workdir "$HOME/cma-workspace"
```

`histchars`, `INPUTRC`, `HISTFILE`은 워커의 `bash` 도구 뒤에서 동작하는 셸을 설정하여, 셸이 각 명령을 에이전트가 작성한 그대로 실행하도록 합니다:

* **`histchars=`:** 셸이 `!`를 명령 기록에 대한 참조로 취급하지 않습니다.
* **`INPUTRC`:** 이 파일은 자동 완성을 끕니다. 그렇지 않으면 자동 완성이 명령의 각 탭 문자를 소비합니다. 에이전트의 파일 도구가 이 파일을 다시 쓸 수 있으므로, 파일을 작업 디렉터리 밖에 두세요.
* **`HISTFILE=/dev/null`:** 셸이 기록 파일을 읽지도 쓰지도 않습니다.

이 워커는 다른 용도로 사용하지 마세요. 이 워커는 환경 키를 보유하고 모든 세션의 도구를 하나의 프로세스에서 실행합니다. 또한 세션의 저장소를 클론하지도 않습니다. 세션은 저장소 없이 실행되며, 클라이언트에는 오류가 전달되지 않습니다.

### 프로덕션에서 워커 실행하기

로컬 실험 외의 경우에는 [세션당 샌드박스 하나 실행하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#run-one-sandbox-per-session)에 나온 대로 세션마다 컨테이너를 하나씩 시작하고 그 안에서 `ant beta:worker run`을 실행하세요. 컨테이너 이미지에는 [자체 샌드박스 이미지 빌드하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api#build-your-own-sandbox-image)에 나열된 항목을 갖추고, 해당 설정에서 다음 세 가지를 변경하세요:

* **환경 키:** 세션의 컨테이너에 `ANTHROPIC_ENVIRONMENT_KEY`를 전달하지 마세요. 서버는 이 키를 환경의 모든 세션에 대해 허용하지만, 컨테이너에는 한 세션에 대한 접근 권한만 필요합니다.
* **작업 항목의 시크릿:** 서버는 이 시크릿을 해당 한 세션에 대해서만 허용하므로, 키 대신 이 시크릿을 전달하세요. [작업 항목의 시크릿 전달](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#forward-the-work-items-secret)에 코드가 시크릿을 받는 위치가 나와 있습니다. 해당 섹션에서 `ANTHROPIC_WORK_SECRET`을 설정하는 곳에서, 컨테이너의 사용자만 읽을 수 있는 파일에 시크릿을 넣으세요. 예를 들어 시크릿을 컨테이너의 표준 입력으로 파이프하고, 컨테이너가 워커를 시작하기 전에 파일을 쓰도록 하세요. 그런 다음 `ANTHROPIC_WORK_SECRET_FILE`을 해당 파일의 경로로 설정하세요.
* **서버 주소:** `ANTHROPIC_BASE_URL`을 컨테이너가 접근할 수 있는 주소로 설정하세요. `http://127.0.0.1:8400`은 그런 주소가 아닙니다.

SDK 워커의 경우 두 가지 사항이 추가로 적용됩니다:

* **버전:** [SDK, CLI 및 라이브러리](https://platform.claude.com/docs/ko/cli-sdks-libraries/overview)에 설명된 대로 최신 버전의 SDK로 워커를 빌드하세요. 이 버전의 워커는 승인이 필요한 도구 호출을 실행하기 전에 승인을 기다리며, 정책이 거부하는 호출은 절대 실행하지 않습니다. 이전 버전으로 빌드한 워커는 모든 도구 호출을 도착하는 즉시 실행합니다.
* **샘플:** [샌드박스 내부에서 SDK 워커 실행](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#run-the-sdk-worker-inside-the-sandbox)의 샘플은 환경 키를 읽으므로, 여기서는 그대로 작동하지 않습니다.

### 자체 샌드박스 이미지 빌드하기

자체 이미지에는 다음 패키지와 설정이 필요합니다:

* **패키지:** Git 2.38 이상, `curl`, `jq`, 그리고 워커의 `grep` 도구가 검색에 사용하는 ripgrep.

* **셸 설정:** 로컬 실험에서와 같은 `histchars`, `HISTFILE`, `INPUTRC`, 그리고 `TERMINFO`. Dockerfile에서 `USER` 줄보다 앞에 다음 줄을 넣으세요:

  ```dockerfile
  ENV histchars=
  RUN printf '\nset disable-completion on\n' >> /etc/inputrc
  ENV INPUTRC=/etc/inputrc TERMINFO=/etc/terminfo HISTFILE=/dev/null
  RUN test -r /etc/terminfo/d/dumb
  ```

  `TERMINFO`에는 워커의 `bash`가 읽는 터미널 설명인 `d/dumb`가 들어 있는 디렉터리를 지정해야 합니다. `bash`가 설치된 Alpine에서는 표시된 대로 `/etc/terminfo`입니다. Debian 또는 Ubuntu 등 다른 기본 이미지에서는 `/lib/terminfo` 또는 `/usr/share/terminfo` 중 `d/dumb`가 있는 쪽입니다. 해당 디렉터리를 `TERMINFO` 설정과 `RUN test` 줄에 작성하세요. `RUN test` 줄은 파일이 없으면 빌드를 중지합니다.

이미지에는 워커가 시작되기 전에 실행되는 아카이브의 스크립트 두 개도 필요합니다:

1. `image-build/with-repositories.sh`를 `/usr/local/bin/cma-with-repositories`로 복사하세요. 이 스크립트는 한 세션의 저장소를 클론합니다.
2. `image-build/with-egress.sh`를 `/usr/local/bin/cma-with-egress`로 복사하세요. 서버에서 [“egress gateway”(이그레스 게이트웨이)](https://platform.claude.com/docs/ko/managed-agents/self-hosted/agent-identity#environment-variable-credentials)를 실행하는 경우, 이 스크립트는 세션의 도구가 이그레스 게이트웨이를 가리키도록 합니다.
3. 두 파일을 모두 실행 가능하게 만드세요.
4. `cma-with-egress cma-with-repositories ant beta:worker run ...`처럼 워커 명령 앞에 두 스크립트를 붙이세요. 이미지가 아카이브의 `kubernetes/sandbox/worker.sh`를 실행한다면 이 단계를 건너뛰세요. 해당 스크립트가 이 방식으로 워커를 시작하기 때문입니다.

`cma-with-repositories` 없이 시작한 워커는 저장소 없이 세션을 실행합니다. 클라이언트에는 오류가 전달되지 않습니다.

### 호스팅 서비스에서 워커 옮기기

이미 Anthropic의 호스팅 서비스용 워커를 실행하고 있다면, 같은 워커가 대신 사용자 서버를 처리하도록 할 수 있습니다. 다음 세 가지를 수행하세요:

* 작업을 폴링하는 프로세스에 관리자 CLI가 출력한 세 가지 `export` 값을 전달하세요.
* 로컬 실험에서처럼 `histchars`, `HISTFILE`, `INPUTRC`를 설정한 상태로 해당 프로세스를 시작하세요.
* [프로덕션에서 워커 실행하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api#run-workers-in-production)에 설명된 변경 사항을 적용하고, 각 세션의 이미지를 [자체 샌드박스 이미지 빌드하기](https://platform.claude.com/docs/ko/managed-agents/self-hosted/call-the-api#build-your-own-sandbox-image)와 대조해 확인하세요.

## 다음 단계

* [개발자가 터미널에서 로그인하도록 허용](https://platform.claude.com/docs/ko/managed-agents/self-hosted/configure#let-developers-log-in-from-a-terminal): 싱글 사인온을 설정하여 개발자가 `ANTHROPIC_API_KEY`가 없는 터미널에서 자신의 ID로 `ant` CLI를 사용하도록 하세요.
* [에이전트 ID 및 자격 증명](https://platform.claude.com/docs/ko/managed-agents/self-hosted/agent-identity): 외부 시스템이 에이전트를 신뢰하도록 하고, 볼트 자격 증명을 시크릿 매니저에 보관합니다.
* [추론 공급자](https://platform.claude.com/docs/ko/managed-agents/self-hosted/inference-provider): 모델 호출을 클라우드 공급자나 자체 모델 게이트웨이로 보냅니다.
