Metadata-Version: 2.4
Name: xsoft-comply
Version: 1.0.0
Summary: Python SDK for xsoft_comply AI governance platform (AI기본법 대응)
Author-email: xsoft <contact@xsoftcomply.com>
License-Expression: MIT
Project-URL: Homepage, https://xsoftcomply.com
Keywords: ai-governance,compliance,audit,llm
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.1; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.1; extra == "openai-agents"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-timeout; extra == "dev"
Dynamic: license-file

# xsoft_comply Python SDK

[xsoft comply](https://xsoftcomply.com) 공식 Python SDK · PyPI 배포.
AI 기본법 대응 AI 거버넌스 플랫폼의 감사기록을 남기는 클라이언트 라이브러리입니다.

## 설치

### 0) 가상환경(venv) 먼저 — `externally-managed-environment` 에러 예방

Ubuntu 23.04+ · Debian 12+ · Fedora 38+ 등 최신 배포판의 시스템 파이썬은 PEP 668 로 잠겨 있어
`pip install` 이 다음 메시지로 멈춥니다.

```
error: externally-managed-environment
× This environment is externally managed
```

시스템 파이썬을 건드리지 말고 **가상환경에 설치**하세요(권장).

```bash
# 1) venv 만들기 (python3-venv 가 없다면: sudo apt install python3-venv)
python3 -m venv ~/venv

# 2) venv 의 pip 으로 설치 — activate 없이 경로로 직접 불러도 됩니다
~/venv/bin/pip install --upgrade pip

# 3) 실행도 같은 venv 로
~/venv/bin/python -c "import xsoft_comply; print(xsoft_comply.__version__)"
```

셸에 붙여 쓰려면 `source ~/venv/bin/activate` 후 `pip`·`python` 을 그대로 쓰면 됩니다
(빠져나올 때는 `deactivate`).

> venv 를 만들 수 없는 환경(컨테이너 베이스 이미지 등)에서만 마지막 수단으로
> `pip install --break-system-packages ...` 를 씁니다. 시스템 패키지와 충돌할 수 있어 권장하지 않습니다.

### 1) 설치 방법 — PyPI 정식 배포 (표준)

`xsoft-comply` 는 **PyPI 에 정식 배포되어 있습니다**. 아래가 표준 설치 경로입니다.

> **이 배포본 = `1.0.0`** (LICENSE 동봉).
> `pip install xsoft-comply` 는 최신 게시본을 설치합니다.
> 배포 파일의 무결성 확인값(sha256)은 버전별로 릴리스 노트(`RELEASES.md`)에 기재합니다. README 는 배포 파일 안에 포함되므로 자기 버전의 확인값을 여기에 적지 않습니다.

```bash
~/venv/bin/pip install xsoft-comply
```

소스에서 직접(개발·수정 목적) 설치하려면:

```bash
# 저장소 루트에서
~/venv/bin/pip install -e sdk/python
```

### ★ 안전하게 설치하기 (권장)

이 SDK 는 고객사 서버 안에서 실행됩니다. 공급망 공격(타이포스쿼팅·계정 탈취)을
막으려면 이름을 정확히 쓰고 해시를 고정하시기 바랍니다.

```bash
# ① 정확한 배포 이름 = xsoft-comply  (import 이름은 xsoft_comply)
#    비슷한 이름(xsoftcomply · xsoft-compliance 등)은 당사 배포본이 아닙니다.
# ② 해시 고정 = 아래 예시대로 requirements.txt 파일을 직접 만들어 적어두고
#    --require-hashes 로 설치합니다 (이 파일은 패키지에 동봉되지 않습니다).
~/venv/bin/pip install --require-hashes -r requirements.txt
```

아래 내용을 담은 `requirements.txt` 를 **직접 생성**합니다(확인값은 릴리스 노트의 값을 그대로 복사):

```
# <확인값> 자리에는 릴리스 노트(RELEASES.md)에 기재된 해당 버전의 sha256 값을 넣습니다.
xsoft-comply==1.0.0 \
    --hash=sha256:<whl 파일의 확인값> \
    --hash=sha256:<tar.gz 파일의 확인값>
```

각 버전의 배포 파일 sha256 은 **`RELEASES.md`** 에 적어 둡니다.
이 README 자체가 배포 파일 안에 들어가므로 여기에 자기 확인값을 적으면
적는 순간 그 값이 틀려집니다 — 그래서 확인값 기록은 패키지 밖 파일에 둡니다.

참고로, 이벤트의 `occurred_at`(발생 시각)과 `event_id`(이벤트 식별자)는
언제나 SDK 가 스스로 만들며, 이를 바깥에서 덮어쓰는 파라미터는 배포 패키지에 없습니다.

설치 후 확인:

```bash
~/venv/bin/python -c "import xsoft_comply; print(xsoft_comply.__version__)"
```

당사가 지키는 것 — **외부 의존성 0**(표준 라이브러리만), **설치 시 실행되는 코드 없음**
(`setup.py`·`.pth` 없음). `import` 만으로는 네트워크를 열지 않습니다
(`init()` 을 호출해야 백그라운드 전송 작업이 시작됩니다).

외부 의존성 없음(표준 라이브러리만). Python 3.9 이상.

## 빠른 시작

```python
import xsoft_comply as xc

xc.init(
    api_key="xsk_live_...",            # API 키 (대시보드에서 발급)
    endpoint="https://your-domain.com/api/v1",
    log_llm_content=False,             # 기본값 · 원문 미전송
)

with xc.session(agent_id="support-bot") as s:
    s.llm_call(
        model="gpt-4o-mini",
        provider="openai",
        tokens_in=512,
        tokens_out=128,
        latency_ms=820,
    )
    s.tool_call(tool="crm.lookup", status="ok", latency_ms=12)
    s.data_access(resource="customer_profile", scope="read", record_count=1)
    s.decision(decision="escalate", options=3, confidence=0.72)
    s.human_review(human_review="approved", reviewer_role="manager")
    s.action(action="ticket.create", status="ok")
```

세션 컨텍스트를 벗어나면 `session_end` 이벤트가 자동으로 기록 대기열에 추가됩니다.

### 저절로 남는 것과 직접 적는 것

| 구분 | 저절로 남습니다 | 직접 적으셔야 합니다 |
|---|---|---|
| 세션 | `session_start` · `session_end` (세션 컨텍스트가 처리) | — |
| 이벤트 | — | LLM 호출 · 도구 호출 · 데이터 조회 · 업무 처리 · 판단 · 사람 검토 |
| 프레임워크 연동 | LangChain · OpenAI Agents SDK 의 주요 실행 | 판단 · 사람 검토 · 업무별 metadata |

### 이벤트 6종

| 이벤트 | 언제 기록됩니까 | 메서드 |
|---|---|---|
| `session_start` | 세션이 시작될 때 | 세션 컨텍스트가 자동 처리 |
| `llm_call` | LLM 을 호출했을 때 | `s.llm_call(...)` |
| `tool_call` | 외부 도구·API 를 호출했을 때 | `s.tool_call(...)` |
| `data_access` | 데이터를 조회했을 때 | `s.data_access(...)` |
| `action` | 실제 처리·업무를 실행했을 때 | `s.action(...)` · `s.decision(...)` · `s.human_review(...)` |
| `session_end` | 세션이 끝날 때 | 세션 컨텍스트가 자동 처리 |

## 조항 증적 (제31~36조)

`event_type` 은 **6종**입니다 — `session_start` · `llm_call` · `tool_call` · `data_access` ·
`action` · `session_end`.

`decision()` · `human_review()` 는 **메서드로 그대로 사용합니다.** 내부에서
`action` + 조항 증적(제34조 고영향 책무)으로 조립되어 전송됩니다 → 기존 코드는 그대로 동작합니다.
달라지는 것은 «당사 서버가 이 기록을 어떤 조항으로 분류해 보관하는가» 뿐입니다.

```python
with xc.session(agent_id="loan-bot") as s:
    # 한 이벤트에 여러 조항 증적을 담을 수 있습니다(배열)
    s.action("loan.approve", status="ok", evidence=[
        xc.evidence_block(34, supervisor_id="sup-1", decision_basis="심사 기준표 v3"),
        xc.evidence_block(31, ai_generated_label="labeled", label_method="watermark",
                          output_id="out-99"),
    ])

    # 운영 중 기록이 아닌 «선언» 성격의 값도 같은 경로로 보냅니다.
    # 선언 정보도 일반 이벤트 경로로 기록하여 변경 이력을 함께 관리합니다.
    s.evidence(32, source="declared", evidence_key="model-2026",
               model_id="m-100", model_version="1.2",
               risk_management_policy_ref="RM-2026-01")
```

- `evidence_key` 는 같은 선언을 반복 전송해도 하나의 증적으로 관리하기 위한 **중복 방지 키**입니다.
  생략하면 이벤트마다 새 키가 되어 매번 새 증적이 됩니다.
- 수집 항목은 조항별 허용 항목명만 받습니다. 오타·정의되지 않은 이름은 **경고 후 제외(drop)** 됩니다(이벤트 자체는 거부되지 않습니다).
- 값에 원문(prompt/response)이나 판정어("준수/위반")를 넣지 마십시오.

### 값 규칙·형식 제약 — 타입이 틀리면 해당 필드가 저장되지 않습니다 (★ 중요)

각 수집 항목은 정해진 타입이 있습니다. 타입이 맞지 않는 값은 서버가 해당 항목만 제외합니다(drop). 이벤트는 `202`(접수)로 응답되고 제외 사유는 응답에 나타나지 않으므로, 접수에 성공해도 감사 시점에 해당 항목이 비어 있을 수 있습니다. 전송 전 `dry_run=True`·`validate()` 로 확인하시기 바랍니다.

| 타입 | 허용 값 | 맞지 않으면 | 예시 |
|---|---|---|---|
| 텍스트(text) | 아무 문자열(500자 초과 시 잘림) | 숫자·객체도 문자열로 변환 | `supervisor_id="sup-1"` |
| 숫자(numeric) | 숫자·숫자 문자열 | 그 밖의 문자 → **제외** | `confidence=0.68` |
| 정수(bigint) | 정수·정수 문자열 | 소수는 내림 · 숫자 아님 → **제외** | `evidence_document_id=123` |
| 참/거짓(boolean) | `true`/`false`·`"true"`/`"false"` | `1`·`"yes"` → **제외** | `control_applied=true` |
| 날짜(date) | 해석 가능한 날짜(`YYYY-MM-DD` 저장) | 형식 불명 → **제외** | `submission_date="2026-02-10"` |

- `null`·빈 문자열 = 제외. 값이 없으면 넣지 마십시오.
- 산업별(로봇·음성·자율주행·의료·채용·금융) 계측 예시·전송 흐름은 **설치 및 사용 설명서(PDF)** 참조.

### 수집 항목 (조항별)

`evidence_block(조항, **항목)` 또는 `s.evidence(조항, ...)` 에 넣는 **수집 항목**입니다.
조항마다 허용된 항목명이 정해져 있으며, 여기에 없는 이름은 경고 후 저장되지 않습니다(이벤트 자체는 거부하지 않습니다).

> **본 SDK 는 항목의 필수 여부를 판정하지 않습니다.** 어떤 항목을 언제 전송할 것인지는 이용사업자가 정합니다.
> 전송되지 않은 항목은 비어 있는 상태로 보관되며, 이를 미준수로 해석하지 않습니다.
>
> 본 목록은 「인공지능 발전과 신뢰 기반 조성 등에 관한 기본법」 제31조부터 제36조까지 및
> 같은 법 시행령 제23조부터 제29조까지가 요구하는 사실을 수집하기 위한 항목입니다.

**제31조 인공지능 투명성 확보 의무 — 15개**

| 수집 항목 | 타입 | 의미 | 근거 |
|---|---|---|---|
| prior_notice_provided | boolean | 사전 고지 이행 여부 | 법 §31① |
| disclosure_method | text | 고지 방법 | 영 §23①1~4 |
| disclosure_version | text | 적용한 고지 문구의 버전 | 영 §23① |
| ai_generated_label | text | 생성 결과물 표시 이행 여부 | 법 §31② |
| label_method | text | 표시 방법 | 영 §23②1~2 |
| machine_readable_label | text | 기계판독 표시 시 안내 제공 여부 | 영 §23②2 후단 |
| synthetic_media | text | 실제와 구분이 어려운 결과물 해당 여부 | 법 §31③ |
| label_visibility | text | 이용자가 명확히 인식할 수 있는 방식 여부 | 법 §31③ |
| accessibility_method | text | 쉽게 확인할 수 있는 방법 | 영 §23③1 |
| audience_consideration | text | 주된 이용자의 조건 고려 | 영 §23③2 |
| artistic_relaxation | boolean | 예술적·창의적 표현물 완화 적용 여부 | 법 §31③ 후단 |
| exemption_type | text | 적용 예외 유형 | 영 §23④1~3 |
| exemption_basis | text | 예외로 본 근거 | 영 §23④ |
| output_id | text | 대상 결과물 식별 | 법 §31②③ |
| media_type | text | 결과물 매체 종류 | 법 §31③ |

**제32조 인공지능 안전성 확보 의무 — 24개**

| 수집 항목 | 타입 | 의미 | 근거 |
|---|---|---|---|
| training_compute | numeric | 학습 누적 연산량 | 영 §24①1 |
| frontier_technology_status | text | 최첨단 기술 적용 여부 | 영 §24①2 |
| frontier_technology_basis | text | 그 판단의 근거 | 영 §24①2 |
| broad_serious_impact_status | text | 광범위·중대한 영향 우려 여부 | 영 §24①3 |
| broad_serious_impact_basis | text | 그 판단의 근거 | 영 §24①3 |
| model_id | text | 대상 모델 식별 | 법 §32① |
| model_version | text | 모델 버전 | 법 §32① |
| risk_management_policy_ref | text | 위험관리체계 문서 참조 | 법 §32①2 |
| submission_status | text | 이행 결과 제출 여부 | 법 §32② |
| submission_date | date | 제출일 | 법 §32② |
| risk_id | text | 위험 건 식별 | 법 §32①1 |
| risk_signal_source | text | 위험을 식별한 경로 | 법 §32①1 |
| risk_lifecycle_stage | text | 위험이 발생한 수명주기 단계 | 법 §32①1 |
| risk_type | text | 위험 구분 | 법 §32①1 |
| risk_level | text | 위험 평가 결과 | 법 §32①1 |
| risk_action | text | 위험 완화 조치 | 법 §32①1 |
| risk_status | text | 위험 처리 상태 | 법 §32①1 |
| incident_id | text | 안전사고 건 식별 | 법 §32①2 |
| incident_detection_source | text | 사고 감지 경로 | 법 §32①2 |
| incident_type | text | 사고 구분 | 법 §32①2 |
| incident_severity | text | 사고 심각도 | 법 §32①2 |
| incident_action | text | 사고 대응 조치 | 법 §32①2 |
| incident_status | text | 사고 처리 상태 | 법 §32①2 |
| incident_close_reason | text | 사고 종결 사유 | 법 §32①2 |

**제34조 고영향 인공지능과 관련한 사업자의 책무 — 38개**

| 수집 항목 | 타입 | 의미 | 근거 |
|---|---|---|---|
| risk_mgmt_ref | text | 위험관리방안 문서 참조 | 법 §34①1 |
| control_applied | boolean | 통제 실행 여부 | 법 §34①1 |
| control_type | text | 통제 구분 | 법 §34①1 |
| control_outcome | text | 통제 결과 | 법 §34①1 |
| control_version | text | 적용된 통제 규칙의 버전 | 법 §34①1 |
| explanation_policy_ref | text | 설명 방안 문서 참조 | 법 §34①2 |
| decision_basis | text | 최종결과 도출에 활용된 주요 기준 | 법 §34①2 |
| training_data_reference | text | 학습용데이터 개요 참조 | 법 §34①2 |
| explanation_available | boolean | 설명 제공 가능 여부 | 법 §34①2 |
| explanation_delivery_status | text | 설명 전달 결과 | 법 §34①2 |
| user_protection_policy_ref | text | 이용자 보호 방안 문서 참조 | 법 §34①3 |
| user_protection_action | text | 이용자 보호 조치 | 법 §34①3 |
| protection_reason | text | 보호 조치 사유 | 법 §34①3 |
| protection_outcome | text | 보호 조치 결과 | 법 §34①3 |
| oversight_policy_ref | text | 사람 관리·감독 체계 문서 참조 | 법 §34①4 |
| supervisor_id | text | 관리·감독자 식별 | 법 §34①4 |
| supervisor_name | text | 관리·감독자 성명 | 영 §27①4 |
| supervisor_contact | text | 관리·감독자 연락처 | 영 §27①4 |
| supervisor_role | text | 관리·감독자 직책 | 법 §34①4 |
| intervention_trigger | text | 개입 조건 | 법 §34①4 |
| intervention_authority | text | 개입 권한 및 범위 | 법 §34①4 |
| oversight_required | boolean | 사람 검토 필요 여부 | 법 §34①4 |
| oversight_request_status | text | 사람 검토 요청 상태 | 법 §34①4 |
| oversight_handover_status | text | 검토 대상 인계 상태 | 법 §34①4 |
| reviewer_id | text | 검토자 식별 | 법 §34①4 |
| reviewer_role | text | 검토자 직책 | 법 §34①4 |
| review_status | text | 검토 진행 상태 | 법 §34①4 |
| review_result | text | 검토 결과 | 법 §34①4 |
| human_override | text | 사람에 의한 변경 여부 | 법 §34①4 |
| override_reason | text | 변경 사유 | 법 §34①4 |
| evidence_document_id | bigint | 조치 근거 문서 참조 | 법 §34①5 |
| document_version | text | 보관 문서의 버전 | 법 §34①5 |
| retention_status | text | 문서 5년 보관 상태 | 영 §27② |
| publication_status | text | 주요 내용 게시 여부 | 영 §27① |
| publication_url | text | 게시 위치 | 영 §27① |
| committee_matter | text | 위원회 심의·의결 사항 | 법 §34①6 |
| alt_compliance_basis | text | 다른 법령에 따른 조치로 갈음하는 근거 | 법 §34③ · 영 §27⑤ |
| downstream_exemption | boolean | 중대한 기능 변경이 없어 이행한 것으로 보는 경우 | 영 §27③ |

**제33조 · 제35조 · 제36조 — 수집 항목 없음**

| 조항 | 사유 |
|---|---|
| **제33조** 고영향 인공지능의 확인 | 제공 전 사전 검토 및 정부 확인 요청은 호출 시점마다 발생하지 않습니다. 관리 화면 및 문서 등록 경로에서 관리합니다. |
| **제35조** 고영향 인공지능 영향평가 | 영향평가의 내용은 시행령 제28조제1항이 정하는 바에 따라 영향평가서에 포함됩니다. 문서 등록 경로에서 관리합니다. |
| **제36조** 국내대리인 지정 | 국내대리인의 지정 및 신고는 회사 단위의 사실입니다. 회사 설정 화면에서 관리합니다. |

★ 위 세 조항의 항목을 SDK 로 전송하면 허용된 항목명에 없으므로 경고 후 저장되지 않습니다.

## 전송 전 로컬 확인

```python
xc.init(..., dry_run=True)     # ① 네트워크 전송 없음. 전송 데이터·조항 연결·수집 항목·가림 결과를 콘솔에 출력
report = xc.validate(evt)      # ② 조항별 수집 항목 전송 «사실» 집계 (판단 아님)
print(xc.preview())            # ③ 이벤트 1건의 전송 내용을 로컬에서 확인
```

```bash
python -m xsoft_comply.preview   # 설치 확인용 이벤트 1건 출력
```

`validate()` 는 「수집 항목 N개 중 M개가 전송되었다」는 **사실만** 반환합니다.
준수 또는 위반 여부를 판단하지 않습니다(변호사법 제109조).

블록마다 반환하는 값 =

| 키 | 뜻 |
|---|---|
| `field_total` · `field_filled` | 그 조항의 수집 항목 수 · 그중 값이 전송된 수 |
| `field_empty` | 전송되지 않은 수집 항목 이름 |
| `unknown_fields` | 허용된 항목명에 없어 저장되지 않을 이름(오타 확인용) |

★ `field_empty` 에 이름이 많다는 것은 **미준수를 뜻하지 않습니다.** 어떤 항목을 전송할 것인지는
이용사업자가 자신의 시스템에서 정하며, 해당 사실이 발생하지 않은 항목은 비어 있는 것이 정상입니다.

## LangChain 연동

```python
from langchain_openai import ChatOpenAI

handler = xc.langchain_handler()
llm = ChatOpenAI(callbacks=[handler])

with xc.session(agent_id="lc-agent") as s:
    response = llm.invoke("안녕하세요")
```

`langchain` 또는 `langchain-core` 가 설치돼 있으면 `BaseCallbackHandler` 를 상속합니다.
없으면 LangChain 의존성 없이 사용할 수 있는 기본 Python 객체로 동작합니다.

## OpenAI Agents SDK 연동

```python
import asyncio
import xsoft_comply as xc
from agents import Agent, Runner, function_tool

@function_tool
def lookup_order(order_id: str) -> str:
    return f"주문 {order_id} 상태 = 배송중"

agent = Agent(name="order-bot", model="gpt-4o-mini", tools=[lookup_order])
xc.instrument(agent)                     # 에이전트 연동 설정(AgentHooks)

async def main():
    with xc.session(agent_id="order-bot"):
        await Runner.run(
            agent, "주문번호 A-1024 상태 알려줘.",
            hooks=xc.agents_run_hooks(),  # 실행 알림 처리기 등록(작업 인계까지 기록)
        )

asyncio.run(main())
```

기록 항목 = 에이전트 시작·종료, 도구 호출(이름·소요시간), LLM 호출(모델·토큰 수), 작업 인계.
원문(입력·출력)은 전송하지 않고 **길이만** 남깁니다.
두 처리기를 함께 써도 **중복 기록을 방지합니다**(이미 연동된 에이전트는 실행 알림 처리기가 건너뜁니다).

## 이벤트 종류별 로그 토글

```python
xc.init(..., log_events={"tool_call": False, "data_access": False})
```

비활성화한 이벤트 유형에는 순번(seq)을 부여하지 않으므로, 이벤트 순서 검증의 연속성이 유지됩니다.
`session_start` · `session_end` 는 끌 수 없습니다(끄면 실행 흐름 기록 자체가 성립하지 않습니다).

## 정책 기반 실행 차단

```python
rules = [
    {"id": "no-mass-delete", "when": {"tool": "crm.delete_all"},
     "effect": "block", "reason": "대량 삭제는 사람 승인 후에만"},
    {"id": "export-warn", "when_re": {"tool": r"^crm\.export"}, "effect": "warn"},
]
xc.init(..., policy=rules)              # policy_enforce=False 면 기록만 하고 차단하지 않습니다

with xc.session(agent_id="support-bot") as s:
    outcome = s.run_tool("crm.delete_all", delete_everything)
    if outcome.blocked:
        ...                              # 도구를 **실행하지 않은** 상태입니다
```

차단은 예외를 발생시키지 않습니다. `outcome.blocked` 로 알려주고, 감사기록에는
`policy_result="blocked"` · `metadata.policy_rule` 이 남습니다.
`s.check("tool_call", tool="…")` 로 평가만 할 수도 있습니다.

## 전자서명 (ed25519 · 의존성 0)

```python
xc.init(..., sign=True)                 # 키가 없으면 로컬에 생성해 소유자만 읽고 쓸 수 있는 권한(0600)으로 보관을 시도합니다
print(xc.signing_public_key())          # 감사인에게 전달할 공개키(개인키는 반환 경로 없음)
```

서명 대상 = trace_id·seq·event_id·type·occurred_at·라벨 필드·metadata 해시·직전 서명.
보관된 기록만 보고 검증할 수 있고, 값이 한 글자만 달라져도 검증에 실패합니다.
서명은 **백그라운드 전송 작업**에서 수행합니다(호출 스레드의 부담을 늘리지 않습니다).

서명 주체는 **SDK(고객 프로세스 안)** 입니다 — 서버가 대신 서명하는 「서버 서명」 이 아니라
**클라이언트 전자서명(ed25519)** 입니다. 개인키는 고객 서버 밖으로 나가지 않고,
당사는 공개키로 검증만 합니다.

## 원문 보관 (객체스토리지 · 옵션)

```python
xc.init(
    ...,
    log_llm_content=True,
    r2={"endpoint": "https://<account>.r2.cloudflarestorage.com",
        "bucket": "comply-raw", "access_key_id": "...", "secret_access_key": "..."},
)
s.llm_call(model="gpt-4o-mini", content=prompt_and_response)
```

원문은 **고객이 지정한 객체 저장소로 직접** 업로드됩니다(당사 수집 API 를 거치지 않습니다).
이벤트에는 위치(`content_uri`)와 길이·무결성 확인값만 남습니다. 저장소 장애 시에도 이벤트 전송은
별도로 계속 시도하며, 원문은 로컬 임시 보관 파일에도 기록되지 않습니다.

## 보관기간(retention)

```python
xc.init(..., retention_days=3650)       # 기본 1825일(5년) · 추가 보관 옵션 사용 시 그에 맞춰 설정합니다
```

이벤트 부가정보(metadata)에 `_retain_until`(만료일)이 남습니다. 만료분 정리는 운영측 도구가
수행합니다.

`_retain_until` 은 **한국 날짜(KST) 기준**입니다. 이 값은 절대시각이 아니라 사람이 읽는
날짜이며, 한국 시각으로 같은 날 기록된 이벤트는 만료일도 같아야 하기 때문입니다.
`occurred_at`·전자서명 시각 같은 **절대시각은 UTC** 입니다.

## 수동 flush / 종료

```python
xc.flush(timeout=5.0)   # 대기열이 빌 때까지 최대 5초 대기
xc.shutdown()           # 백그라운드 전송 작업 정지
```

프로세스 종료 시 `atexit` 처리기가 자동으로 최대 2초 동안 전송 완료를 시도합니다.

### 서버리스에서는 `flush_on_return` 을 쓰십시오

서버리스는 응답을 반환한 뒤 컨테이너를 멈춥니다. 백그라운드 전송 작업도 함께 멈추므로,
큐에 남아 있던 이벤트가 전송되지 않을 수 있습니다. 오류도 남지 않습니다.

```python
import xsoft_comply as xc

@xc.flush_on_return
def handler(event, context):
    with xc.session(agent_id="my-agent") as s:
        ...
    return {"statusCode": 200}
```

- 핸들러가 값을 돌려주기 **직전에** 큐를 비웁니다.
- flush 가 실패해도 핸들러의 반환값과 예외는 그대로 통과합니다.
- `xc.is_serverless()` 로 현재 환경이 서버리스로 보이는지 확인할 수 있습니다.

---

## 유실 계측 — `dropped` 와 `failed` 를 함께 확인하십시오

중요한 운영 위험은 이벤트 누락입니다. 그래서 전송되지 않은 이벤트를 두 갈래로 나눠 셉니다.
**원인이 다르면 조치도 다르기 때문입니다.**

```python
xc.dropped_events()   # 대기열 초과로 누락된 건수
xc.failed_events()    # 서버 거부로 전송되지 않은 건수
```

| 함수 | 언제 늘어납니까 | 무엇을 확인해야 합니까 |
|---|---|---|
| `dropped_events()` | SDK 내부 대기열이 가득 차 제외했을 때 | `max_queue` 를 키우거나 `flush_interval` 을 줄입니다 |
| `failed_events()` | 서버가 `400`·`401`·`403`·`413` 등으로 거부했을 때 | API 키 · 요금제 · 전송 본문 크기를 확인합니다 |

- 두 값과 **로컬 임시 보관(스풀) 상태를 함께 확인**하십시오. 스풀에 보관 중인 건(`403 PLAN_REQUIRED` · `429` · `5xx`)은 **재전송 대상** 이라 두 카운터에 잡히지 않습니다.
- 세는 단위는 **이벤트 건수** 입니다(전송 묶음 수가 아닙니다). 한 번의 전송 요청에 여러 건이 실립니다.

```python
if xc.dropped_events() or xc.failed_events():
    log.error("감사기록 누락 가능 — 대기열 초과 %d건 · 서버 거부 %d건",
              xc.dropped_events(), xc.failed_events())
```

### 증상별 확인 순서

| 증상 | 먼저 확인할 것 |
|---|---|
| 패키지 import 실패 | 가상환경 경로 · 설치한 이름(`xsoft-comply`) · Python 3.9 이상 |
| 운영 화면에 기록이 없음 | API 키 · `endpoint` · 세션 종료 또는 `flush()` 호출 · 요금제 권한 |
| `dropped_events()` 증가 | 큐 용량(`max_queue`)과 flush 주기 · 일시적인 대량 전송 여부 |
| `failed_events()` 증가 | API 키 · 요금제 · 배치 본문 크기 · 서버 응답 코드 |
| 서버리스에서 마지막 기록이 없음 | 반환 직전 `flush()` 또는 `@xc.flush_on_return` 사용 여부 |

---

## 애플리케이션 영향 최소화 설계

xsoft comply SDK 는 호출 스레드에서 네트워크 전송을 하지 않도록 설계했습니다.
다음 8가지 동작 원칙을 따릅니다.

### 1. 모든 공개 함수는 예외를 호출자에게 전달하지 않습니다

`init()` · `session()` · `s.llm_call()` 등 모든 공개 함수는 최상위 `try/except BaseException` 으로 감쌉니다.
`KeyboardInterrupt` · `SystemExit` 만 다시 발생시킵니다(프로세스 종료 신호를 차단하지 않기 위해).

```python
# SDK 내부 구조 (단순화)
def llm_call(self, ...):
    try:
        evt = build_event(...)
        buffer.put(evt)   # 대기 없이 즉시 반환
    except (KeyboardInterrupt, SystemExit):
        raise
    except BaseException:
        pass              # 오류를 호출자에게 전달하지 않습니다
```

### 2. 호출 스레드는 네트워크 전송을 하지 않습니다

이벤트 기록은 `queue.put_nowait()` 만 실행합니다.
대기열이 가득 차면 가장 오래된 항목을 먼저 제외하고 새 항목을 넣습니다. **대기 없음.**

```
고객 스레드  →  put_nowait(event)  →  [대기열]  →  백그라운드 전송 작업  →  전송 요청
```

### 3. 전송은 백그라운드 스레드(daemon thread) 1개가 담당합니다

`threading.Thread(daemon=True)` 로 생성됩니다.
이 스레드는 Python 인터프리터가 종료될 때 함께 종료되므로 고객 프로세스 종료를 지연시키지 않습니다.

### 4. 전송 대기 제한 시간: 연결 2초 / 응답 3초

`urllib.request` 에 합산 5초 제한 시간을 설정합니다.
`requests` · `httpx` 등 외부 라이브러리 의존성이 없습니다.

### 5. 전송 실패 시 로컬 임시 보관 → 재시도 간격 조정

연결 거부 · 서버 오류 · 시간 초과 등 네트워크 장애 시:
1. 이벤트를 `~/.xsoft_comply/spool/events.jsonl` 에 줄 단위로 추가 기록합니다
2. 재시도 간격을 점차 늘려 대기합니다 (1초 → 2초 → 4초 → 8초 → 최대 30초)
3. 대기 후 재전송을 시도합니다

### 6. 임시 보관 파일 상한 (기본 32 MB)

임시 보관 폴더의 총 크기가 32 MB 를 초과하면 오래된 파일부터 삭제합니다.
디스크 쓰기 실패도 예외를 호출자에게 전달하지 않습니다.

### 7. init() 전에 이벤트를 기록해도 중단되지 않습니다

대기열이 준비되지 않은 상태에서 이벤트 기록은 아무 작업 없이 즉시 반환됩니다.

### 8. 프로세스 종료 시 자동 전송 시도 (최대 2초)

```python
import atexit
atexit.register(_atexit_handler)   # init() 최초 호출 시 1회 등록

def _atexit_handler():
    flush(timeout=2.0)             # 2초 후 반드시 반환
```

프로세스가 정상 종료될 때 대기열에 남은 이벤트의 전송을 최대 2초 동안 시도합니다.
2초 안에 완료되지 않아도 프로세스 종료를 지연시키지 않습니다.

---

## 원문을 당사 수집 API 로 전송하지 않는 기본 설계

`content=` 인자로 원문(prompt/response)을 넘겨도 원문 자체는 당사 수집 API 로 전송되지 않습니다.
대신 `content_len` (길이) 과 `content_sha256_8` (sha256 앞 8자) 만 부가정보에 포함됩니다.

원문을 보관해야 한다면 위 「원문 보관 (객체스토리지 · 옵션)」 을 사용하시면 되고, 그때도 원문은
**고객이 지정한 객체 저장소로 직접 업로드**됩니다 — 당사를 경유하지 않습니다.

다음 부가정보(metadata) 키가 있으면 SDK 에서 자동 제거하고, 제거된 키의 **개수**를 기록합니다:
`prompt`, `response`, `messages`, `content`, `input`, `output` 등 21개.
다만 부가정보의 다른 키에 담긴 값까지 원문인지 판별하지는 못하므로,
부가정보와 조항 증적에는 원문·민감정보를 입력하지 마십시오.

## 개인정보 처리 원칙

### 1. 적용 범위

본 SDK 가 당사 수집 API 로 전송하는 값은 **① 부가정보(metadata)** 와 **② 조항 증적(evidence)**
두 가지이며, 개인정보 처리 방식이 서로 다릅니다.

| 구분 | 무엇인가 | 개인정보 처리 |
|---|---|---|
| **부가정보(metadata)** | 이용자가 이벤트에 자유롭게 덧붙이는 값. 항목을 당사가 정하지 않습니다 | ★ **자동으로 가립니다**(제2항) |
| **조항 증적(evidence)** | 법 제31조부터 제36조까지가 요구하는 값. 항목명과 형식을 당사가 정합니다 | ★ **가리지 않습니다**(제3항) |

### 2. 부가정보(metadata) — 자동 가림 대상

부가정보는 이용자가 무엇이든 담을 수 있는 자리이므로, 개인정보가 의도치 않게 포함될 수
있습니다. 이에 본 SDK 는 전송 전 이용자 서버에서 다음 형식의 값을 자동으로 가립니다.

| 대상 형식 | 처리 결과 |
|---|---|
| 주민등록번호 | `******-*******` |
| 휴대전화번호 | `010-****-****` |
| 전자우편주소 | `u***@example.com` |
| 카드번호(16자리) | `1234-****-****-5678` |

가림이 한 건이라도 발생한 경우 `metadata._pii_masked` 값이 `true` 로 기록됩니다.

본 항의 가림은 **이용자 보호를 위한 예비적 조치**이며, 이용자가 부가정보에 개인정보를
담지 않을 책임을 대신하지 않습니다.

### 3. 조항 증적(evidence) — ★ 가리지 않습니다

**본 SDK 는 조항 증적에 포함된 값을 가리지 않으며, 이용자는 해당 값을 가리지 않은
원문 그대로 전송하여야 합니다.**

이는 관계 법령이 해당 항목의 **게시**를 요구하기 때문입니다.

> 인공지능 기본법 시행령 제27조제1항제4호 — 인공지능사업자는 해당 고영향 인공지능을
> 관리·감독하는 사람의 **성명 및 연락처**를 사무소·사업장 또는 인터넷 홈페이지 등에
> 게시해야 한다.

가려진 값(예: `홍**`)은 위 조항이 요구하는 성명에 해당하지 않습니다. 이용자가 임의로
가려 전송한 경우 해당 값은 **감사 대응 자료로서의 효력을 상실할 수 있으며**, 이로 인하여
발생하는 불이익에 대하여 당사는 책임을 지지 않습니다.

### 4. 당사 서비스 화면에서의 표시

당사는 제3항에 따라 수집한 값을 **원문으로 보관**하되, 이용자 화면에는 다음 항목 일부를
가린 형태로만 표시합니다.

| 항목 | 화면 표시 |
|---|---|
| `supervisor_id` · `supervisor_name` · `supervisor_contact` · `reviewer_id` | 가운데를 가려 표시합니다 (예: `홍**`) |
| `supervisor_role` · `reviewer_role` | 직책·역할에 해당하므로 가리지 않습니다 |

가림은 당사 서버에서 수행되며, 가리기 전의 값은 이용자 화면(브라우저)으로 전달되지
않습니다. 감사 대응 자료로 제공되는 증적 묶음에는 **원문 그대로** 포함됩니다.

### 5. 항목의 필수 여부

본 SDK 는 각 항목의 **필수 여부를 판정하지 않습니다.** 전송되지 않은 항목은 감사 대응
자료에 **「미기록」**으로 사실대로 표기됩니다.

## 라이선스

MIT
