펜리젠트 헤더

Mac mini M4 클러스터에 Kimi K2.5를 배포하고 Penligent.ai를 호출합니다: 최소한의 로컬 우선 에이전트 해커 튜토리얼

이 튜토리얼에서는 실제 재현 가능한 "경로 A" 아키텍처귀하의 에이전트 오케스트레이션, 도구 실행, 증거 저장 및 감사 로그가 로컬에서 실행됩니다.Mac mini M4 클러스터추론은 공식 Kimi(Moonshot) K2.5 API를 통해 실행됩니다. 를 통해 OpenAI 호환 인터페이스. Moonshot의 자체 문서에서는 다음과 같이 명시적으로 지원합니다. OpenAI SDK 호환성 를 표시하고 표준 POST /v1/chat/완성하기 워크플로. (문샷 AI)

여기서 의도는 다음과 같습니다. 공인 보안 테스트만 (테스트할 수 있는 명시적인 권한이 있는 자체 시스템, 실험실 또는 대상). 목표는 "무기화"가 아닙니다. 최소, 제어 가능, 감사 가능 에이전트 파이프라인을 통해 고객이 사용할 수 있는 로컬 배포로 발전시킬 수 있습니다.

1) Mac mini M4에서 경로 A가 "절대적으로 가능한" 경로인 이유

K2.5의 공식 API는 OpenAI 호환 시맨틱으로 안정적이고 공급업체가 유지 관리하는 추론 레이어를 제공하며, 로컬 플랫폼을 구축하여 다음과 같은 작업을 수행할 수 있습니다. 모델은 셸 명령을 직접 실행하지 않습니다.. 대신 모델은 화이트리스트 도구 (함수)를 호출하고 모든 도구 호출은 로컬 정책 엔진 에서 실행되고 샌드박스 작업자.

Moonshot은 또한 호환 가능한 인터페이스(다음을 포함)를 나열하는 자세한 'OpenAI에서 마이그레이션' 지침을 제공합니다. /v1/chat/complaints) 및 플랫폼 엔드포인트를 통한 Kimi 모델 사용 예시. (문샷 AI)

루트 A가 "절대적으로 실현 가능한" 이유

2) 하드웨어 계획: 몇 대의 Mac mini M4가 필요한가요?

경로 A에서는 다음과 같습니다. not 로컬에서 K2.5 가중치를 호스팅하는 것이므로 결정적인 요소는 다음과 같습니다. 도구 실행 처리량 (워커, 샌드박스, 보고서 생성), GPU 추론 용량이 아닙니다.

Apple의 공식 사양에는 통합 메모리 상한이 다른 Mac mini(M4)와 Mac mini(M4 Pro) 구성이 나와 있지만, 실제로는 메모리가 높을수록 더 많은 컨테이너를 동시에 실행하고 로그/인덱스를 상주 상태로 유지하는 데 도움이 됩니다. (GitHub)

프로덕션과 같은 최소한의 설정을 위해:

  • 2대(최소 실행 가능): 하나의 "컨트롤 플레인" 노드(게이트웨이 + 오케스트레이터 + 대기열 + 통합 가시성)와 하나의 "워커" 노드(샌드박스 실행 + 펜리전트 통합).
  • 머신 3대(데모 및 병렬 처리에 권장): 하나의 제어 평면, 두 명의 작업자. 이렇게 하면 '병렬 에이전트 계획'과 '병렬 스캔'이 고객 앞에서 안정적으로 느껴집니다.

처리량에 따라 크기를 조정하는 경우, 간단한 엔지니어링 휴리스틱을 사용하여 평균 작업 런타임을 추정하세요. T예상되는 일일 작업 N를 클릭하고 원하는 완료 기간 W. 대략 다음과 비례하는 작업자 용량을 원할 것입니다. (T × N) / W를 클릭한 다음 로그 전송 및 보고서 생성을 위한 헤드룸을 추가합니다.

하드웨어 계획: 몇 개의 Mac mini M4 노드?

3) 참조 아키텍처(로컬 우선, 모델-비에이아이)

6가지 구성 요소를 구축하게 됩니다:

  1. 모델 게이트웨이 (로컬): (a) 내부 호출자를 인증하고, (b) 아웃바운드 프롬프트를 수정/필터링하고, (c) Moonshot Kimi 엔드포인트로 전달하고, (d) 감사 메타데이터를 기록하는 작은 OpenAI 호환 프록시입니다.
  2. 에이전트 오케스트레이터 (로컬): 명령을 직접 실행하지 않고 도구만 호출하는 '에이전트 루프'(계획 → 도구 호출 → 관찰 → 계속)입니다.
  3. 정책 엔진 (로컬): 위험한 행동 유형에 대한 허용 목록 대상, 속도 제한, 최대 걸음 수 및 하드 차단.
  4. 샌드박스 작업자 (로컬): 제어된 컨테이너 환경에서 각 작업 실행, Penligent 호출, 증거 수집 및 아티팩트 보고.
  5. 증거 및 보고서 저장 (로컬): 로컬 디스크, NAS 또는 MinIO; 모든 작업은 추적 ID와 변경 불가능한 아티팩트 포인터를 받습니다.
  6. 감사 로그 (로컬): 모든 모델 요청 및 도구 실행 단계에 대한 추가 전용 레코드입니다.

Moonshot의 플랫폼 문서는 OpenAI 호환성을 강조하고 표준 Kimi API 사용법을 보여 주며, 이를 추론의 기반으로 활용하게 됩니다. (문샷 AI)

레퍼런스 아키텍처 개요

4) 클러스터 설정: Mac mini M4의 k3s(2~3노드)

당신 can 로 모든 것을 할 수 있지만, k3s는 워커 확장, 네임스페이스 격리, 롤링 업데이트, 서비스 검색 등 실제 배포에 필요한 '클러스터 형태'를 제공합니다.

4.1 각 Mac의 사전 요구 사항

명령줄 도구와 기본 종속성을 설치합니다:

xcode-select --install
/bin/bash -c "$(curl -fsSL )"
brew install git jq wget python@3.11

컨테이너의 경우 Docker 데스크톱 팀에 익숙한 경우 콜리마 보다 서버와 유사한 런타임을 원한다면. 중요한 것은 컨테이너를 안정적으로 빌드하고 실행할 수 있다는 것입니다.

4.2 제어 노드에 k3s 서버 설치하기

켜짐 m4-control:

curl -sfL  | sh -s - server --write-kubeconfig-mode 644
sudo kubectl get nodes -o wide
sudo cat /var/lib/rancher/k3s/server/node-token

4.3 워커 노드 조인

켜짐 m4-worker-1 (및 m4-worker-2 노드가 3개인 경우):

export K3S_URL="https://:6443"
export K3S_TOKEN=""
curl -sfL  | sh -s - 에이전트

다시 켜기 m4-control:

sudo kubectl get nodes -o wide

Mac mini M4의 k3s 클러스터 설정

5) Kimi K2.5 API 기본 사항

Moonshot의 문서에서 이를 확인할 수 있습니다:

  • 플랫폼 API는 다음과 같은 HTTP 엔드포인트를 사용합니다. 무기명 토큰 인증.
  • 그것은 OpenAI SDK와 호환 를 사용하세요.
  • 표준 채팅 완료 엔드포인트는 다음과 같습니다. POST /v1/chat/완성하기.
  • 이 문서에는 'Kimi API 사용 시작하기' 가이드와 Kimi 모델에 대한 예제가 포함되어 있습니다. (문샷 AI)

또한 K2.5에는 모델 카드/커뮤니티 자산(예: '생각하기' 대 '즉시' 사용 패턴)에 공개적으로 참조되는 예시와 스크립트가 있습니다. (포옹하는 얼굴)

모델 이름을 구성할 수 있도록 게이트웨이를 구조화하여 하드코딩 가정을 피할 수 있습니다.

Kimi K2.5 API 기본 사항(OpenAI 호환)

6) 리포지토리 구조 구축

리포지토리 폴더를 만듭니다:

local-agentic-hacker/
  deploy/
    00-namespace.yaml
    01-secrets.yaml
    02-configmap.yaml
    10-gateway.yaml
    20-worker.yaml
    30-오케스트레이터.yaml
  services/
    게이트웨이/
      app.py
      요구사항.txt
      도커파일
    orchestrator/
      app.py
      requirements.txt
      도커파일
    worker/
      app.py
      penligent_connector.py
      requirements.txt
      도커파일
  configs/
    policy.yaml
    tools_schema.json

이 레이아웃은 '실행되는 것'(서비스)과 '배포되는 방법'(배포)을 깔끔하게 분리합니다.

리파지토리 구조 및 결과물 레이아웃

7) 모델 게이트웨이(로컬): Kimi에 대한 OpenAI 호환 프록시

게이트웨이는 의도적으로 작습니다. 게이트웨이의 역할은 다음을 유지하는 것입니다. 기타 모든 로컬 서비스 인터넷 키가 직접 필요하지 않도록 하고 일관된 로깅과 선택적 삭제를 시행합니다.

Moonshot에 따르면 Kimi API는 OpenAI와 호환되므로 이 접근 방식은 다음과 같이 깔끔하게 매핑됩니다. /v1/chat/complaints. (문샷 AI)

7.1 services/gateway/app.py

가져오기
가져오기 시간
import Any, Dict, Optional을 입력할 때

import httpx
에서 FastAPI, 헤더, HTTPException, 요청을 가져옵니다.

MOONSHOT_BASE_URL = os.environ.get("MOONSHOT_BASE_URL", "")
MOONSHOT_API_KEY = os.environ.get("MOONSHOT_API_KEY", "")

# 자체 클러스터 서비스에 대한 내부 인증
GATEWAY_INTERNAL_KEY = os.environ.get("GATEWAY_INTERNAL_KEY", "change-me")

app = FastAPI(title="로컬 모델 게이트웨이(Moonshot API를 통한 Kimi K2.5)")

def redact(text: str) -> str:
    # 최소 예제: 정규식 규칙으로 확장할 수 있습니다.
    if not isinstance(text, str):
        반환 ""
    # 로그에 비밀을 유출하지 마세요.
    return text[:2000]

app.post("/v1/chat/completions")
async def chat_completions(req: 요청, 권한 부여: Optional[str] = 헤더(기본값=없음)):
    # 내부 발신자 인증
    if authorization != f"Bearer {GATEWAY_INTERNAL_KEY}":
        HTTPException(status_code=401, detail="권한 없음") 발생

    MOONSHOT_API_KEY가 아닌 경우:
        raise HTTPException(status_code=500, detail="MOONSHOT_API_KEY가 설정되지 않았습니다")

    페이로드: Dict[str, Any] = await req.json()

    # 감사 메타데이터(기본적으로 원시 프롬프트 없음)
    audit = {
        "ts": int(time.time()),
        "model": payload.get("model"),
        "messages_count": len(payload.get("messages", [])),
        "has_tools": bool(payload.get("tools")),
    }
    print("[GATEWAY_AUDIT]", audit)

    headers = {
        "Authorization": f"Bearer {MOONSHOT_API_KEY}",
        "Content-Type": "application/json",
    }

    # 선택 사항: 업스트림 전송 전 최소한의 리댁션
    # 예시: 사용자 콘텐츠 필드 잘라내기
    messages = payload.get("messages", [])
    메시지의 m에 대해
        m에 "content"가 있고 isinstance(m["content"], str):
            m["content"] = redact(m["content"])
    payload["messages"] = 메시지

    클라이언트로서 httpx.AsyncClient(timeout=180)를 사용하여 비동기화합니다:
        r = await client.post(f"{MOONSHOT_BASE_URL}/chat/completions", headers=headers, json=payload)

    if r.status_code >= 400:
        HTTPException(status_code=r.status_code, detail=r.text[:800])을 발생시킵니다.

    반환 r.json()

7.2 요구 사항 및 도커파일

서비스/게이트웨이/요구 사항.txt:

fastapi==0.115.0
uvicorn==0.30.6
httpx==0.27.2

서비스/게이트웨이/도커파일:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

모델 게이트웨이: 프록시 + 리댁션 + 감사

8) 정책 + 도구: 설계를 통해 에이전트를 제어 가능하게 만들기

핵심 원칙은 다음과 같습니다: 모델이 직접 '작업'을 수행할 수 없습니다.. 도구 호출만 요청할 수 있습니다. 정책에서 도구를 호출할 수 있는지 여부와 매개 변수를 결정합니다.

8.1 configs/policy.yaml

허용 목록:
  - ""
  - ""

제한:
  default_max_requests: 800
  default_max_duration_sec: 1800
  기본_속도_제한_RPS: 3.0

hard_blocks:
  - "dos"
  - "credential_stuffing"
  - "mass_scan_outside_scope"
  - "DATA_EXFILTRATION"

human_in_the_loop:
  REQUIRE_CONFIRM_FOR:
    - "high_impact"

8.2 configs/tools_schema.json

이것은 OpenAI 스타일의 도구 스키마입니다. Kimi는 Moonshot 문서에 설명된 대로 도구 호출을 지원하므로 구조는 OpenAI 호환 API에서 기대할 수 있는 것과 동일합니다. (문샷 AI)

[
  {
    "type": "function",
    "function": {
      "name": "create_pentest_task",
      "설명": "로컬 작업자/펜리전트 커넥터에서 새 승인된 작업을 만듭니다. 대상이 허용 목록에 있어야 합니다.",
      "매개변수": {
        "type": "객체",
        "속성": {
          "target": { "type": "문자열" },
          "scope": { "type": "문자열" },
          "notes": { "type": "문자열" },
          "limits": {
            "type": "object",
            "속성": {
              "max_requests": { "type": "정수" },
              "max_duration_sec": { "type": "정수" },
              "rate_limit_rps": { "type": "숫자" }
            },
            "required": ["max_requests", "max_duration_sec", "rate_limit_rps"]
          }
        },
        "required": ["target", "scope", "limits"] }
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "run_pentest_task",
      "설명": "샌드박스 워커에서 작업을 실행합니다.",
      "매개변수": {
        "type": "객체",
        "속성": {
          "task_id": { "type": "문자열" }
        },
        "필수": ["task_id"]
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "fetch_task_result",
      "설명": "최종 결과와 아티팩트 포인터를 가져옵니다.",
      "매개변수": {
        "type": "객체",
        "속성": {
          "task_id": { "type": "문자열" }
        },
        "필수": ["task_id"]
      }
    }
  }
]

정책 및 도구 스키마: 에이전트를 제어 가능하게 만들기

9) 오케스트레이터: 안전하고 감사할 수 있는 최소한의 '에이전트 루프'

오케스트레이터는 최대 단계, 대상 허용 목록, 속도 제한 및 도구 화이트리스트와 같은 런타임 제약 조건을 적용하는 곳입니다.

Moonshot의 문서에는 OpenAI 호환 인터페이스와 도구 호출 지침이 나와 있으며, 여기서는 표준 도구 호출 흐름을 사용하겠습니다. (문샷 AI)

9.1 서비스/오케스트레이터/앱.py

import json
import os
import time
from typing import Any, Dict, Optional

import httpx
import yaml
from fastapi import FastAPI, HTTPException

GATEWAY_URL = os.environ.get("GATEWAY_URL", "<http://model-gateway:8080/v1/chat/completions>")
GATEWAY_KEY = os.environ.get("GATEWAY_INTERNAL_KEY", "change-me")

WORKER_URL = os.environ.get("WORKER_URL", "<http://worker:8081>")
MODEL_NAME = os.environ.get("MODEL_NAME", "kimi-k2.5")  # set to your actual model name in Moonshot

POLICY_PATH = os.environ.get("POLICY_PATH", "/configs/policy.yaml")
TOOLS_PATH = os.environ.get("TOOLS_PATH", "/configs/tools_schema.json")

app = FastAPI(title="Local Agent Orchestrator")

def load_policy() -> Dict[str, Any]:
    with open(POLICY_PATH, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

def load_tools() -> Any:
    with open(TOOLS_PATH, "r", encoding="utf-8") as f:
        return json.load(f)

def is_allowed_target(target: str, policy: Dict[str, Any]) -> bool:
    for a in policy.get("allowlist", []):
        if target.startswith(a):
            return True
    return False

async def call_model(messages: Any, tools: Any) -> Dict[str, Any]:
    payload = {
        "model": MODEL_NAME,
        "messages": messages,
        "tools": tools,
        "tool_choice": "auto"
    }
    headers = {"Authorization": f"Bearer {GATEWAY_KEY}"}
    async with httpx.AsyncClient(timeout=180) as client:
        r = await client.post(GATEWAY_URL, headers=headers, json=payload)
    if r.status_code >= 400:
        raise HTTPException(status_code=500, detail=f"Model gateway error: {r.text[:500]}")
    return r.json()

async def call_worker(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
    async with httpx.AsyncClient(timeout=1800) as client:
        r = await client.post(f"{WORKER_URL}{path}", json=body)
    if r.status_code >= 400:
        raise HTTPException(status_code=500, detail=f"Worker error: {r.text[:500]}")
    return r.json()

@app.post("/run")
async def run_agent(input: Dict[str, Any]):
    policy = load_policy()
    tools = load_tools()

    target = input.get("target", "")
    goal = input.get("goal", "Run an authorized security test and produce a report.")
    context = input.get("context", "")

    if not is_allowed_target(target, policy):
        raise HTTPException(status_code=400, detail="Target not in allowlist.")

    messages = [
        {
            "role": "system",
            "content": (
                "You are an orchestrator for AUTHORIZED security testing only. "
                "Stay within scope and rate limits. "
                "Do NOT output exploit payloads or instructions. "
                "Use tools only via provided functions, and focus on safe verification, evidence collection, and reporting."
            )
        },
        {
            "role": "user",
            "content": f"Target: {target}\\nGoal: {goal}\\nContext: {context}\\nProceed using tools."
        }
    ]

    max_steps = 10
    trace = {
        "trace_id": f"trace-{int(time.time())}",
        "start_ts": int(time.time()),
        "target": target,
        "steps": []
    }

    task_id: Optional[str] = None

    for step in range(max_steps):
        resp = await call_model(messages, tools)
        msg = resp["choices"][0]["message"]
        trace["steps"].append({"step": step, "assistant": msg})

        # If no tool calls, return the final text
        if "tool_calls" not in msg:
            return {"trace": trace, "final": msg.get("content", ""), "task_id": task_id}

        for tc in msg["tool_calls"]:
            fn = tc["function"]["name"]
            args = json.loads(tc["function"]["arguments"])

            # Local hard enforcement
            if fn == "create_pentest_task":
                if not is_allowed_target(args.get("target", ""), policy):
                    raise HTTPException(status_code=400, detail="Tool target not allowed.")

                limits = args.get("limits") or {}
                limits.setdefault("max_requests", policy["limits"]["default_max_requests"])
                limits.setdefault("max_duration_sec", policy["limits"]["default_max_duration_sec"])
                limits.setdefault("rate_limit_rps", policy["limits"]["default_rate_limit_rps"])
                args["limits"] = limits

                w = await call_worker("/create", args)
                task_id = w["task_id"]
                tool_result = w

            elif fn == "run_pentest_task":
                if not task_id:
                    raise HTTPException(status_code=400, detail="No task_id yet.")
                tool_result = await call_worker("/run", {"task_id": task_id})

            elif fn == "fetch_task_result":
                if not task_id:
                    raise HTTPException(status_code=400, detail="No task_id yet.")
                tool_result = await call_worker("/result", {"task_id": task_id})

            else:
                raise HTTPException(status_code=400, detail=f"Unknown tool: {fn}")

            # Feed tool output back to the model
            messages.append(msg)
            messages.append({
                "role": "tool",
                "tool_call_id": tc["id"],
                "content": json.dumps(tool_result, ensure_ascii=False)
            })

        # Optional early stop if result contains report pointers
        if task_id and any("report" in (m.get("content","") if isinstance(m, dict) else "") for m in messages[-3:]):
            break

    return {"trace": trace, "task_id": task_id}

서비스/오케스트레이터/요구 사항.txt:

fastapi==0.115.0
uvicorn==0.30.6
httpx==0.27.2
PyYAML==6.0.2

서비스/오케스트레이터/도커파일:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8082"]

오케스트레이터: 안전 에이전트 루프

10) 워커: 로컬 샌드박스 실행 + 펜리전트 커넥터

작업자는 작업 생성, 작업 실행, 증거 수집, 보고서 포인터 작성 등 '실행 단계'의 책임을 시뮬레이션합니다.

앞뒤 설명이 없는 완전한 튜토리얼을 요청하셨으므로 아래 커넥터는 의도적으로 다음과 같이 구성되었습니다. 실제 펜리전트 API를 연결하기 전에도 엔드투엔드로 실행됩니다.를 입력하면 파이프라인을 증명하는 데모 보고서 아티팩트가 생성됩니다. 그런 다음 내부 함수 하나를 실제 Penligent 통합으로 대체할 수 있으며 다른 모든 기능은 동일하게 유지됩니다.

10.1 services/worker/app.py

가져오기
가져오기 시간
import uuid
입력에서 import Any, Dict

에서 FastAPI, HTTPException 가져오기

from penligent_connector import run_with_penligent

app = FastAPI(title="샌드박스 워커")

TASKS: Dict[str, Dict[str, Any]] = {}
EVIDENCE_DIR = os.environ.get("EVIDENCE_DIR", "/evidence")

@app.post("/create")
async def create_task(body: Dict[str, Any]):
    task_id = str(uuid.uuid4())
    TASKS[task_id] = {
        "task_id": task_id,
        "status": "created",
        "created_ts": int(time.time()),
        "input": body,
        "결과": None,
    }
    반환 {"task_id": task_id, "status": "created"}

@app.post("/run")
async def run_task(body: Dict[str, Any]):
    task_id = body.get("task_id", "")
    task_id가 TASKS에 없는 경우:
        raise HTTPException(status_code=404, detail="작업을 찾을 수 없음")

    task = TASKS[task_id]
    task["status"] in ("running", "done"):
        반환 {"task_id": task_id, "status": task["status"]}

    task["status"] = "running"

    inp = task["input"]
    result = await run_with_penligent(inp, evidence_dir=EVIDENCE_DIR)

    task["결과"] = 결과
    task["status"] = "done"
    task["done_ts"] = int(time.time())

    반환 {"task_id": task_id, "status": "done", "summary": result.get("summary", {})}

@app.post("/result")
async def fetch_result(body: Dict[str, Any]):
    task_id = body.get("task_id", "")
    task_id가 TASKS에 없는 경우:
        raise HTTPException(status_code=404, detail="작업을 찾을 수 없음")

    task = TASKS[task_id]
    if task["status"] != "done":
        반환 {"task_id": task_id, "status": task["status"]}

    반환 {"task_id": task_id, "status": "done", "result": task["result"]}

10.2 services/worker/penligent_connector.py

json 가져오기
import os
import time
입력에서 import Any, Dict

httpx 가져오기

PENLIGENT_BASE_URL = os.environ.get("PENLIGENT_BASE_URL", "")
PENLIGENT_API_KEY = os.environ.get("PENLIGENT_API_KEY", "")

async def run_with_penligent(task: Dict[str, Any], evidence_dir: str) -> Dict[str, Any]:
    target = task["target"]
    scope = task.get("scope", "")
    limits = task.get("limits", {})
    notes = task.get("notes", "")

    ts = int(time.time())
    evidence_path = os.path.join(evidence_dir, f"task-{ts}")
    os.makedirs(evidence_path, exist_ok=True)

    # 실제 펜리전트 API가 구성된 경우, 이를 호출합니다.
    PENLIGENT_BASE_URL 및 PENLIGENT_API_KEY인 경우:
        headers = {"Authorization": f"Bearer {PENLIGENT_API_KEY}"}
        페이로드 = {
            "target": 대상,
            "scope": 범위,
            "limits": 제한
            "notes": notes
        }

        클라이언트로서 httpx.AsyncClient(timeout=1800)를 사용하여 비동기화합니다:
            r = await client.post(f"{PENLIGENT_BASE_URL}/api/task/run", headers=headers, json=payload)

        if r.status_code >= 400:
            반환 {
                "요약": {"target": target, "error": r.text[:500]},
                "증거_경로": 증거_경로
            }

        data = r.json()

        report_path = os.path.join(evidence_path, "penligent_report.json")
        open(report_path, "w", encoding="utf-8")를 f로 사용합니다:
            json.dump(data, f, ensure_ascii=False, indent=2)

        반환 {
            "요약": {
                "target": target,
                "issues_count": data.get("issues_count", 0),
                "highlights": data.get("highlights", [])[:10]
            },
            "report_path": report_path,
            "증거_경로": 증거_경로
        }

    # 그렇지 않으면 데모 아티팩트를 생성하여 파이프라인을 검증합니다.
    report_path = os.path.join(evidence_path, "report.md")
    content = (
        f"# 데모 보고서(파이프라인 유효성 검사)\\n\\n"
        f"대상: {대상}\\n\\n"
        f"Scope: {scope}\\n\\n"
        f"Notes: {notes}\\n\\n"
        f"Limits: {json.dumps(limits)}\\n\\n"
        f"## 요약\\n\\n"
        f"이것은 로컬 우선 에이전트 파이프라인을 검증하기 위한 자리 표시자 보고서입니다.\\n"
        f"PENLIGENT_BASE_URL/PENLIGENT_API_KEY를 바꾸고 실제 펜리전트 호출을 구현하세요.\\n"
    )
    열다(report_path, "w", encoding="utf-8")를 f로 대체합니다:
        f.write(content)

    반환 {
        "summary": {"target": target, "issues_count": 0, "하이라이트": ["데모 전용"]},
        "report_path": report_path,
        "증거_경로": 증거_경로
    }

서비스/근로자/요구 사항.txt:

fastapi==0.115.0
uvicorn==0.30.6
httpx==0.27.2

서비스/워커/도커파일:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py penligent_connector.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8081"]

워커 샌드박스 + 펜리전트 커넥터

11) k3s에 배포: 네임스페이스, 시크릿, 컨피그맵, 서비스

11.1 네임스페이스

배포/00-네임스페이스.yaml:

apiVersion: v1
종류: 네임스페이스
메타데이터:
  이름: 에이전트-로컬

신청하세요:

kubectl apply -f deploy/00-namespace.yaml

11.2 비밀(문샷 API 키 + 내부 게이트웨이 키)

Moonshot의 문서 쇼 권한 부여: 무기명 $MOONSHOT_API_KEY Kimi API 요청에 대한 사용 패턴을 확인합니다. (문샷 AI)

배포/01-secrets.yaml:

apiVersion: v1
종류: 비밀
메타데이터:
  이름: moonshot-secret
  네임스페이스: 에이전트-로컬
유형: 불투명
stringData:
  api_key: "replace_with_your_moonshot_api_key"
---
apiVersion: v1
종류: 비밀
메타데이터:
  이름: 내부 비밀
  네임스페이스: 에이전트-로컬
유형: 불투명
stringData:
  게이트웨이_키: "replace_with_a_long_random_internal_key"

신청하세요:

kubectl apply -f deploy/01-secrets.yaml

11.3 컨피그맵(정책 + 도구 스키마)

배포/02-configmap.yaml:

apiVersion: v1
종류: 컨피그맵
메타데이터:
  이름: 에이전트-컨피그
  네임스페이스: 에이전트-로컬
데이터:
  policy.yaml: |
    허용 목록
      - ""
    제한
      default_max_requests: 800
      default_max_duration_sec: 1800
      기본_속도_제한_RPS: 3.0
    hard_blocks:
      - "dos"
      - "credential_stuffing"
      - "mass_scan_outside_scope"
      - "DATA_EXFILTRATION"
    human_in_the_loop:
      REQUIRE_CONFIRM_FOR:
        - "high_impact"
  tools_schema.json: |
    [
      {
        "type": "function",
        "function": {
          "name": "create_pentest_task",
          "설명": "로컬 작업자/펜리전트 커넥터에서 새 승인된 작업을 만듭니다. 대상이 허용 목록에 있어야 합니다.",
          "매개변수": {
            "type": "객체",
            "속성": {
              "target": { "type": "문자열" },
              "scope": { "type": "문자열" },
              "notes": { "type": "문자열" },
              "limits": {
                "type": "object",
                "속성": {
                  "max_requests": { "type": "정수" },
                  "max_duration_sec": { "type": "정수" },
                  "rate_limit_rps": { "type": "숫자" }
                },
                "required": ["max_requests", "max_duration_sec", "rate_limit_rps"]
              }
            },
            "required": ["target", "scope", "limits"] }
          }
        }
      },
      {
        "type": "function",
        "function": {
          "name": "run_pentest_task",
          "설명": "샌드박스 워커에서 작업을 실행합니다.",
          "매개변수": {
            "type": "객체",
            "속성": { "task_id": { "type": "문자열" } },
            "required": ["task_id"]
          }
        }
      },
      {
        "type": "function",
        "function": {
          "name": "fetch_task_result",
          "설명": "최종 결과와 아티팩트 포인터를 가져옵니다.",
          "매개변수": {
            "type": "객체",
            "속성": { "task_id": { "type": "문자열" } },
            "required": ["task_id"]
          }
        }
      }
    ]

신청하세요:

kubectl apply -f deploy/02-configmap.yaml

11.4 게이트웨이 배포

배포/10-gateway.yaml:

apiVersion: apps/v1
종류: 배포
메타데이터:
  이름: 모델-게이트웨이
  네임스페이스: 에이전트-로컬
spec:
  replicas: 1
  선택기:
    matchLabels:
      앱: 모델-게이트웨이
  템플릿:
    메타데이터:
      labels:
        앱: 모델-게이트웨이
    spec:
      컨테이너:
        - 이름: 모델-게이트웨이
          이미지: yourrepo/model-gateway:최신
          포트:
            - 컨테이너포트: 8080
          env:
            - 이름: MOONSHOT_BASE_URL
              값 ""
            - name: MOONSHOT_API_KEY
              valueFrom:
                secretKeyRef:
                  이름: MOONSHOT-SECRET
                  키: API_KEY
            - 이름: 게이트웨이_인터널_키
              valueFrom:
                secretKeyRef:
                  이름: 내부-비밀
                  키: 게이트웨이_키
---
apiVersion: v1
종류: 서비스
메타데이터:
  이름: 모델-게이트웨이
  네임스페이스: 에이전트-로컬
spec:
  selector:
    앱: 모델-게이트웨이
  포트
    - port: 8080
      대상 포트: 8080

11.5 작업자 배포

배포/20-worker.yaml:

apiVersion: apps/v1
종류: 배포
메타데이터:
  이름: 작업자
  네임스페이스: 에이전트-로컬
spec:
  replicas: 1
  선택기:
    matchLabels:
      app: worker
  템플릿:
    메타데이터:
      labels:
        app: worker
    spec:
      컨테이너:
        - 이름: 워커
          이미지: yourrepo/worker:최신
          포트:
            - 컨테이너포트: 8081
          env:
            - name: EVIDENCE_DIR
              value: "/evidence"
            - 이름: PENLIGENT_BASE_URL
              값: "" 실제 펜리전트 연결 시 설정된 #
            - 이름: PENLIGENT_API_KEY
              값: "" 실제 배포 시 시크릿을 통해 설정된 #
          볼륨 마운트:
            - 이름: 증거
              mountPath: /evidence
      볼륨:
        - 이름: 증거
          emptyDir: {}
---
apiVersion: v1
종류: 서비스
메타데이터:
  name: worker
  네임스페이스: 에이전트-로컬
spec:
  selector:
    앱: 워커
  포트
    - port: 8081
      대상 포트: 8081

11.6 오케스트레이터 배포

배포/30-오케스트레이터.yaml:

apiVersion: apps/v1
종류: 배포
메타데이터:
  이름: 오케스트레이터
  네임스페이스: 에이전트-로컬
spec:
  replicas: 1
  선택기:
    matchLabels:
      앱: 오케스트레이터
  템플릿:
    메타데이터:
      labels:
        앱: 오케스트레이터
    spec:
      컨테이너:
        - 이름: 오케스트레이터
          이미지: yourrepo/orchestrator:최신
          포트:
            - 컨테이너포트: 8082
          env:
            - 이름: GATEWAY_URL
              값 ""
            - name: GATEWAY_INTERNAL_KEY
              valueFrom:
                secretKeyRef:
                  이름: 내부-비밀
                  키: 게이트웨이_키
            - 이름: WORKER_URL
              value: ""
            - 이름: 모델_이름
              value: "kimi-k2.5"
            - name: POLICY_PATH
              value: "/configs/policy.yaml"
            - 이름: TOOLS_PATH
              value: "/configs/tools_schema.json"
          볼륨 마운트:
            - 이름: configs
              마운트경로 /configs
      볼륨:
        - 이름: configs
          configMap:
            이름: 에이전트-컨피그
---
apiVersion: v1
종류: 서비스
메타데이터:
  이름: 오케스트레이터
  네임스페이스: 에이전트-로컬
spec:
  selector:
    앱: 오케스트레이터
  포트
    - port: 8082
      대상 포트: 8082

모두 적용:

kubectl apply -f deploy/10-gateway.yaml
kubectl apply -f deploy/20-worker.yaml
kubectl apply -f deploy/30-orchestrator.yaml
kubectl -n 에이전트-로컬 get pods -o wide

k3s에 배포: 시크릿 / 컨피그맵 / 서비스

12) 로컬 랩 타겟으로 엔드투엔드 검증(권장: OWASP 주스 샵)

네트워크 내부에서 법적 랩 대상을 실행하세요. 예를 들어, 한 노드에서 OWASP Juice Shop을 실행합니다:

도커 실행 -d --이름 주스샵 -p 3000:3000 bkimminich/juice-shop

에서 허용 목록에 대상을 추가합니다. policy.yaml (또는 컨피그맵)에서 확인할 수 있습니다.

그런 다음 오케스트레이터를 포트 포워딩하고 요청을 실행합니다:

kubectl -n 에이전트-로컬 포트 포워드 svc/오케스트레이터 8082:8082

이제 상담원에게 전화하세요:

curl -s  \\
  -H "Content-Type: application/json" \\
  -d '{
    "target": "",
    "목표": "범위 내에서 승인된 보안 평가를 수행하고 보고서 요약을 생성합니다.",
    "context": "안전한 인증만 사용하세요. 익스플로잇 페이로드를 출력하지 마세요."
  }'

아직 펜리전트를 연결하지 않은 경우에도 작업자는 데모 보고서 아티팩트를 생성하여 다음과 같이 증명합니다. 에이전트 루프 + 도구 호출 + 증거 포인터 파이프라인이 작동합니다. 펜리전트를 연결하면 동일한 파이프라인이 실제 결과를 생성합니다.

공인 랩 타겟을 사용한 엔드투엔드 데모

13) 도구 호출의 신뢰성 확보: 배포 게이트로 K2VV(K2 Vendor Verifier)를 사용하세요.

에이전트 시스템은 종종 미묘한 도구 호출 불일치로 인해 실패합니다. 문샷 생성 K2 벤더 검증자 특히 K2 API 구현 및 도구 호출의 품질/일관성을 모니터링하고 개선하기 위한 것입니다. (GitHub)

실제 배포에서는 도구 호출 안정성을 SLO로 취급하세요. 가장 간단한 패턴은 다음과 같습니다:

  • 게이트웨이 + 업스트림에 대해 CI에서 K2VV를 실행하세요.
  • JSON 스키마 유효성, tool_call 서식 오류, 중지 이유 및 "누락된 도구 호출" 회귀를 추적하세요.
  • 점수가 기준선 아래로 떨어지면 차단이 해제됩니다.

이는 진지한 보안 구매자에게 '프로덕션급 에이전트 플랫폼'을 정당화할 수 있는 가장 쉬운 방법 중 하나입니다.

신뢰성 게이트: K2 공급업체 검증기(ToolCall 안정성)

14) 최소한의 시스템에서도 건너뛰어서는 안 되는 보안 제어

제품 브로셔에 '에이전트 해커'를 넣으려는 경우, 첫 번째 고객은 피해를 방지하는 방법을 묻게 될 것입니다. 신뢰할 수 있는 최소한의 이야기는 다음과 같습니다:

시스템에서 엄격한 대상 허용 목록엄격한 도구 허용 목록, 엄격한 요금 제한, 엄격한 최대 걸음 수엄격한 휴먼 인 더 루프 후크 를 사용하여 영향력이 큰 작업을 수행합니다. 이 모델은 명령을 실행하지 않고 도구 호출만 요청하며 실행 전에 로컬에서 검사하기 때문에 이러한 제약 조건을 우회할 수 없습니다.

문샷의 키미 플랫폼은 도구 호출과 긴 컨텍스트를 지원하며, 로컬 제어를 통해 안전하게 작동할 수 있습니다. (문샷 AI)

협상할 수 없는 안전 관리

15) 어디 Penligent.ai "플러그인" 및 이 튜토리얼이 계속 유효한 이유

이 튜토리얼에서 펜리전트는 정확히 하나의 경계에 통합되어 있습니다. run_with_penligent() 함수. 펜리전트 여부:

  • 호스팅하는 내부 HTTP API입니다,
  • 배포하는 로컬 바이너리 또는 도커화된 워커입니다,
  • 하이브리드 '작업 생성 → 폴링 → 보고서 가져오기' 워크플로우입니다,

...오케스트레이터와 게이트웨이는 변경할 필요가 없습니다. 이러한 분리로 인해 빌드 절대적으로 실현 가능 오늘과 상업화 가능 내일.

펜리전트가 연결되는 곳(안정적 통합 경계)

16) 생산성 향상 업그레이드

로컬 우선주의를 유지하면서 '최소한의 것'을 넘어 진화하고 싶다면:

  1. 교체 빈 디렉터리 증거 보관 MinIO (S3 호환)를 사용하여 객체 URL을 아티팩트 포인터로 반환합니다.
  2. 작업 대기열을 추가하여 작업자가 작업을 비동기적으로 실행하고 재시작 후에도 살아남을 수 있도록 합니다(Redis + RQ/Celery 또는 NATS).
  3. 작업자가 허용 목록에 있는 대상에만 도달할 수 있도록 송신 네트워크 정책을 추가하세요.
  4. 구조화된 감사 로그(JSON 라인)와 로그 파이프라인을 추가하세요.
  5. 휴먼 인더루프 작업에 대해 '승인 대기 중인 도구 호출'을 표시하는 UI를 추가합니다.

아키텍처 계약은 변경되지 않습니다. Moonshot API를 통해 모델링하고 로컬에서 제어합니다.

아키텍처 변경 없이 프로덕션 업그레이드

17) 최종 합격 체크리스트

최소한의 로컬 우선 에이전트 보안 플랫폼은 다음과 같은 경우에만 '진짜'입니다:

  • 허용 목록에 없는 대상은 거부됩니다.
  • 이 모델은 임의의 명령을 실행할 수 없습니다(도구 스키마 호출만 가능).
  • 도구 호출은 추적 ID 및 타임스탬프와 함께 기록됩니다.
  • 증거 아티팩트가 생성되고 안정적인 포인터를 갖습니다.
  • 속도 제한 및 최대 걸음 수는 로컬에서 적용됩니다.
  • API 키는 이미지나 로그 내부에 저장되지 않고 Kubernetes 시크릿에만 저장됩니다.
  • 워커가 다시 시작해도 시스템은 여전히 엔드투엔드로 실행됩니다(프로덕션에서는 단일 지점 '메모리 상태'가 없음).

18) 요약

이제 완전하고 재현 가능한 경로 튜토리얼:

  • Mac mini M4 클러스터는 컨트롤 플레인과 워커 플레인을 실행합니다.
  • 로컬 OpenAI 호환 게이트웨이는 공식 Kimi API로 전달합니다(/v1/chat/complaints) 내부 인증 및 감사 로깅을 사용합니다. (문샷 AI)
  • 오케스트레이터는 안전한 도구 호출 에이전트 루프를 구현합니다.
  • 작업자는 로컬에서 작업을 실행하고 단일 커넥터 경계를 통해 Penligent와 통합합니다.
  • K2 벤더 검증기는 툴 호출 신뢰성을 측정하고 시행할 수 있는 실용적인 방법을 제공합니다. (GitHub)

이를 리포지토리에 붙여넣고 세 개의 이미지를 빌드/푸시하면 지금 바로 k3에 배포하고 인증된 랩 타깃을 사용하여 로컬 우선 '에이전트 해커' 파이프라인을 시연하고 준비가 되면 시스템을 다시 작성하지 않고도 실제 Penligent 작업 인터페이스에서 교체할 수 있습니다.

https://platform.moonshot.ai/docs/guide/start-using-kimi-api https://platform.moonshot.ai/docs/api/chat https://huggingface.co/moonshotai/Kimi-K2.5 https://github.com/MoonshotAI/K2-Vendor-Verifier https://platform.moonshot.ai/blog/posts/K2_Vendor_Verifier_Newsletter https://docs.k3s.io/quick-start https://docs.k3s.io/installation https://docs.k3s.io/installation/configuration https://owasp.org/www-project-juice-shop/ https://github.com/juice-shop/juice-shop https://help.owasp-juice.shop/ https://penligent.ai/blog https://penligent.ai/docs https://www.penligent.ai/hackinglabs/the-2026-ultimate-guide-to-ai-penetration-testing-the-era-of-agentic-red-teaming/ https://www.penligent.ai/hackinglabs/the-2026-ultimate-guide-to-ai-penetration-testing-the-era-of-agentic-red-teaming/ https://www.penligent.ai/hackinglabs/the-dawn-of-autonomous-offensive-security-architecture-and-practice-of-agentic-pentesting/ https://penligent.ai/resources/blog/overview-of-penligentais-automated-penetration-testing-tool https://blog.vllm.ai/2025/10/28/Kimi-K2-Accuracy.html

게시물을 공유하세요:
관련 게시물
ko_KRKorean