xsoft-comply 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,394 @@
1
+ Metadata-Version: 2.4
2
+ Name: xsoft-comply
3
+ Version: 0.1.0
4
+ Summary: Python SDK for xsoft_comply AI governance platform (AI기본법 대응)
5
+ Author-email: xsoft <sdk@xsoft.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/raxsoft-sudo/xsoft_comply
8
+ Project-URL: Documentation, https://github.com/raxsoft-sudo/xsoft_comply/tree/main/sdk/python
9
+ Project-URL: Repository, https://github.com/raxsoft-sudo/xsoft_comply
10
+ Keywords: ai-governance,compliance,audit,llm
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Logging
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Provides-Extra: langchain
23
+ Requires-Dist: langchain-core>=0.1; extra == "langchain"
24
+ Provides-Extra: openai-agents
25
+ Requires-Dist: openai-agents>=0.1; extra == "openai-agents"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7; extra == "dev"
28
+ Requires-Dist: pytest-timeout; extra == "dev"
29
+
30
+ # xsoft_comply Python SDK
31
+
32
+ AI 기본법 대응 거버넌스 플랫폼 [xsoft_comply](https://github.com/raxsoft-sudo/xsoft_comply)의 공식 Python SDK.
33
+
34
+ ## 설치
35
+
36
+ ### 0) 가상환경(venv) 먼저 — `externally-managed-environment` 에러 예방
37
+
38
+ Ubuntu 23.04+ · Debian 12+ · Fedora 38+ 등 최신 배포판의 시스템 파이썬은 PEP 668 로 잠겨 있어
39
+ `pip install` 이 다음 메시지로 멈춥니다.
40
+
41
+ ```
42
+ error: externally-managed-environment
43
+ × This environment is externally managed
44
+ ```
45
+
46
+ 시스템 파이썬을 건드리지 말고 **가상환경에 설치**하세요(권장).
47
+
48
+ ```bash
49
+ # 1) venv 만들기 (python3-venv 가 없다면: sudo apt install python3-venv)
50
+ python3 -m venv ~/venv
51
+
52
+ # 2) venv 의 pip 으로 설치 — activate 없이 경로로 직접 불러도 됩니다
53
+ ~/venv/bin/pip install --upgrade pip
54
+
55
+ # 3) 실행도 같은 venv 로
56
+ ~/venv/bin/python -c "import xsoft_comply; print(xsoft_comply.__version__)"
57
+ ```
58
+
59
+ 셸에 붙여 쓰려면 `source ~/venv/bin/activate` 후 `pip`·`python` 을 그대로 쓰면 됩니다
60
+ (빠져나올 때는 `deactivate`).
61
+
62
+ > venv 를 만들 수 없는 환경(컨테이너 베이스 이미지 등)에서만 마지막 수단으로
63
+ > `pip install --break-system-packages ...` 를 씁니다. 시스템 패키지와 충돌할 수 있어 권장하지 않습니다.
64
+
65
+ ### 1) 설치 방법 — 지금은 로컬(소스) 설치
66
+
67
+ `xsoft-comply` 는 **아직 PyPI 에 올라가 있지 않습니다.** 지금은 저장소를 받아 로컬로 설치하세요.
68
+
69
+ ```bash
70
+ # 저장소 루트에서
71
+ ~/venv/bin/pip install -e sdk/python
72
+ ```
73
+
74
+ PyPI 공개 후에는 아래가 표준 설치 경로가 됩니다.
75
+
76
+ ```bash
77
+ ~/venv/bin/pip install xsoft-comply
78
+ ```
79
+
80
+ ### ★ 안전하게 설치하기 (권장)
81
+
82
+ 이 SDK 는 여러분의 **서버 안에서 실행**됩니다. 공급망 공격(타이포스쿼팅·계정 탈취)을
83
+ 막으려면 이름을 정확히 쓰고 해시를 고정하세요.
84
+
85
+ ```bash
86
+ # ① 정확한 배포 이름 = xsoft-comply (import 이름은 xsoft_comply)
87
+ # 비슷한 이름(xsoftcomply · xsoft-compliance 등)은 우리 것이 아닙니다.
88
+ # ② 해시 고정 = requirements.txt 에 박아두고 --require-hashes 로 설치
89
+ ~/venv/bin/pip install --require-hashes -r requirements.txt
90
+ ```
91
+
92
+ `requirements.txt` 예시(해시는 릴리스 노트의 값을 그대로 복사):
93
+
94
+ ```
95
+ xsoft-comply==0.1.0 \
96
+ --hash=sha256:<릴리스 노트에 공지된 sha256>
97
+ ```
98
+
99
+ 설치 후 확인:
100
+
101
+ ```bash
102
+ ~/venv/bin/python -c "import xsoft_comply; print(xsoft_comply.__version__)"
103
+ ```
104
+
105
+ 우리가 지키는 것 — **외부 의존성 0**(표준 라이브러리만), **설치 시 실행되는 코드 없음**
106
+ (`setup.py`·`.pth` 없음), 배포는 **Trusted Publishing(OIDC)+2FA** 로만.
107
+ `import` 만으로는 네트워크를 열지 않습니다(`init()` 을 불러야 전송 스레드가 뜹니다).
108
+
109
+ 외부 의존성 없음(표준 라이브러리만). Python 3.9 이상.
110
+
111
+ ## 빠른 시작
112
+
113
+ ```python
114
+ import xsoft_comply as xc
115
+
116
+ xc.init(
117
+ api_key="xsk_live_...", # API 키 (대시보드에서 발급)
118
+ endpoint="https://your-domain.com/api/v1",
119
+ log_llm_content=False, # 기본값 · 원문 미전송
120
+ )
121
+
122
+ with xc.session(agent_id="support-bot") as s:
123
+ s.llm_call(
124
+ model="gpt-4o-mini",
125
+ provider="openai",
126
+ tokens_in=512,
127
+ tokens_out=128,
128
+ latency_ms=820,
129
+ )
130
+ s.tool_call(tool="crm.lookup", status="ok", latency_ms=12)
131
+ s.data_access(resource="customer_profile", scope="read", record_count=1)
132
+ s.decision(decision="escalate", options=3, confidence=0.72)
133
+ s.human_review(human_review="approved", reviewer_role="manager")
134
+ s.action(action="ticket.create", status="ok")
135
+ ```
136
+
137
+ 세션 컨텍스트를 벗어나면 `session_end` 이벤트가 자동으로 전송된다.
138
+
139
+ ## 조항 증적 (제31~36조) · 2026-08-20
140
+
141
+ `event_type` 은 **6종**이다 — `session_start` · `llm_call` · `tool_call` · `data_access` ·
142
+ `action` · `session_end`.
143
+
144
+ `decision()` · `human_review()` 는 **메서드로 그대로 남는다.** 내부에서
145
+ `action` + 조항 증적(제34조 고영향 책무)으로 조립돼 나간다 → 기존 코드는 그대로 돌아간다.
146
+ 바뀌는 것은 «어디에 적재되는가» 뿐이다(`compliance_evidence` + `ev_high_impact` 로 정규화 분배).
147
+
148
+ ```python
149
+ with xc.session(agent_id="loan-bot") as s:
150
+ # 한 이벤트에 여러 조항 증적을 담을 수 있다(배열)
151
+ s.action("loan.approve", status="ok", evidence=[
152
+ xc.evidence_block(34, supervisor_id="sup-1", decision_basis="심사 기준표 v3"),
153
+ xc.evidence_block(31, ai_generated_label="labeled", label_method="watermark",
154
+ output_id="out-99"),
155
+ ])
156
+
157
+ # 런타임이 아닌 «선언» 증적도 같은 길로 보낸다(원천 우회 금지 = hash-chain 을 받는다)
158
+ s.evidence(36, source="declared", evidence_key="agent-2026",
159
+ company_country="US", has_korean_address="false",
160
+ domestic_agent_name="…", domestic_agent_contact="…")
161
+ ```
162
+
163
+ - `evidence_key` = 개발자 멱등키. 같은 선언을 여러 번 보내도 증적은 **1행**이다.
164
+ 생략하면 이벤트마다 새 키가 되어 매번 새 증적이 된다.
165
+ - 표준필드는 조항별 화이트리스트만 받는다. 오타·미정의 키는 **경고 후 드롭**된다(이벤트는 거부되지 않는다).
166
+ - 값에 원문(prompt/response)이나 판정어("준수/위반")를 넣지 마라.
167
+
168
+ ## 확인 3종 — 보내기 전에 눈으로 확인한다
169
+
170
+ ```python
171
+ xc.init(..., dry_run=True) # ① 전송 0. payload·조항매핑·필수 표준필드·마스킹 결과를 콘솔에 출력
172
+ report = xc.validate(evt) # ② 조항별 필수 표준필드 «사실» 집계 (판정 아님)
173
+ print(xc.preview()) # ③ 1콜 output 로컬 확인
174
+ ```
175
+
176
+ ```bash
177
+ python -m xsoft_comply.preview # 설치 확인용 1콜 출력
178
+ ```
179
+
180
+ `validate()` 는 「필수 N개 중 M개가 비어 있다」 는 **사실만** 돌려준다.
181
+ 준수·위반 여부를 판정하지 않는다(변호사법 109조).
182
+
183
+ ## LangChain 통합
184
+
185
+ ```python
186
+ from langchain_openai import ChatOpenAI
187
+
188
+ handler = xc.langchain_handler()
189
+ llm = ChatOpenAI(callbacks=[handler])
190
+
191
+ with xc.session(agent_id="lc-agent") as s:
192
+ response = llm.invoke("안녕하세요")
193
+ ```
194
+
195
+ `langchain` 또는 `langchain-core` 가 설치돼 있으면 `BaseCallbackHandler` 를 상속한다.
196
+ 없으면 순수 Python 클래스로 동작한다.
197
+
198
+ ## OpenAI Agents SDK 통합
199
+
200
+ ```python
201
+ import asyncio
202
+ import xsoft_comply as xc
203
+ from agents import Agent, Runner, function_tool
204
+
205
+ @function_tool
206
+ def lookup_order(order_id: str) -> str:
207
+ return f"주문 {order_id} 상태 = 배송중"
208
+
209
+ agent = Agent(name="order-bot", model="gpt-4o-mini", tools=[lookup_order])
210
+ xc.instrument(agent) # AgentHooks 주입
211
+
212
+ async def main():
213
+ with xc.session(agent_id="order-bot"):
214
+ await Runner.run(
215
+ agent, "주문번호 A-1024 상태 알려줘.",
216
+ hooks=xc.agents_run_hooks(), # RunHooks 주입(핸드오프까지 잡는다)
217
+ )
218
+
219
+ asyncio.run(main())
220
+ ```
221
+
222
+ 기록되는 것 = 에이전트 시작·종료, 도구 호출(이름·소요시간), LLM 호출(모델·토큰 수), 핸드오프.
223
+ 원문(입력·출력)은 보내지 않고 **길이만** 남긴다.
224
+ 두 훅을 함께 써도 **이중기록되지 않는다**(실행훅이 에이전트훅이 이미 붙은 에이전트는 건너뛴다).
225
+
226
+ ## 이벤트 종류별 로그 토글
227
+
228
+ ```python
229
+ xc.init(..., log_events={"tool_call": False, "data_access": False})
230
+ ```
231
+
232
+ 끈 종류는 seq 를 소비하지 않는다 → 체인이 끊기지 않는다.
233
+ `session_start` · `session_end` 는 끌 수 없다(끄면 trace 자체가 성립하지 않는다).
234
+
235
+ ## 정책 자동차단
236
+
237
+ ```python
238
+ rules = [
239
+ {"id": "no-mass-delete", "when": {"tool": "crm.delete_all"},
240
+ "effect": "block", "reason": "대량 삭제는 사람 승인 후에만"},
241
+ {"id": "export-warn", "when_re": {"tool": r"^crm\.export"}, "effect": "warn"},
242
+ ]
243
+ xc.init(..., policy=rules) # policy_enforce=False 면 기록만 하고 막지 않는다
244
+
245
+ with xc.session(agent_id="support-bot") as s:
246
+ outcome = s.run_tool("crm.delete_all", delete_everything)
247
+ if outcome.blocked:
248
+ ... # 도구는 **호출되지 않았다**
249
+ ```
250
+
251
+ 차단은 예외를 던지지 않는다. `outcome.blocked` 로 알려주고, 감사기록에는
252
+ `policy_result="blocked"` · `metadata.policy_rule` 이 남는다.
253
+ `s.check("tool_call", tool="…")` 로 평가만 할 수도 있다.
254
+
255
+ ## 전자서명 (ed25519 · 의존성 0)
256
+
257
+ ```python
258
+ xc.init(..., sign=True) # 키가 없으면 로컬에 만들어 0600 으로 보관
259
+ print(xc.signing_public_key()) # 감사인에게 넘길 공개키(개인키는 반환 경로 없음)
260
+ ```
261
+
262
+ 서명 대상 = trace_id·seq·event_id·type·occurred_at·라벨 컬럼·metadata 해시·직전 서명.
263
+ DB 에 저장된 행만 보고 검증할 수 있고, 값이 한 글자만 달라져도 깨진다.
264
+ 서명은 **전송 스레드**에서 한다(호출 스레드는 O(1) 유지).
265
+
266
+ ## 원문 보관 (객체스토리지 · 옵션)
267
+
268
+ ```python
269
+ xc.init(
270
+ ...,
271
+ log_llm_content=True,
272
+ r2={"endpoint": "https://<account>.r2.cloudflarestorage.com",
273
+ "bucket": "comply-raw", "access_key_id": "...", "secret_access_key": "..."},
274
+ )
275
+ s.llm_call(model="gpt-4o-mini", content=prompt_and_response)
276
+ ```
277
+
278
+ 원문은 **고객 소유 버킷으로 직접** 올라간다(우리 수집 API 를 거치지 않는다).
279
+ 이벤트에는 위치(`content_uri`)와 길이·해시만 남는다. 버킷 장애 시에도 이벤트는 정상 적재되고
280
+ 원문은 로컬 스풀에도 남기지 않는다.
281
+
282
+ ## 보관기간(retention)
283
+
284
+ ```python
285
+ xc.init(..., retention_days=3650) # 기본 1825일(5년) · 초장기 애드온은 늘려 잡는다
286
+ ```
287
+
288
+ 이벤트 metadata 에 `_retain_until`(만료일)이 남는다. 만료분 정리는 운영측 도구가 한다
289
+ (`sdk/tools/retention.py`).
290
+
291
+ ## 수동 flush / 종료
292
+
293
+ ```python
294
+ xc.flush(timeout=5.0) # 버퍼가 빌 때까지 최대 5초 대기
295
+ xc.shutdown() # 전송 스레드 정지
296
+ ```
297
+
298
+ 프로세스 종료 시 `atexit` 핸들러가 자동으로 최대 2초 flush 를 시도한다.
299
+
300
+ ---
301
+
302
+ ## 장애격리 설계: "절대 안 멈추는" 이유
303
+
304
+ xsoft_comply SDK 는 **고객 애플리케이션의 가용성을 절대 침해하지 않는다.**
305
+ 다음 8개 계약이 이를 보장한다.
306
+
307
+ ### 1. 모든 공개 함수는 예외를 밖으로 내보내지 않는다
308
+
309
+ `init()` · `session()` · `s.llm_call()` 등 모든 공개 함수는 최상위 `try/except BaseException` 으로 감싼다.
310
+ `KeyboardInterrupt` · `SystemExit` 만 재raise 한다(프로세스 종료 신호를 막지 않기 위해).
311
+
312
+ ```python
313
+ # SDK 내부 구조 (단순화)
314
+ def llm_call(self, ...):
315
+ try:
316
+ evt = build_event(...)
317
+ buffer.put(evt) # O(1) 비블로킹
318
+ except (KeyboardInterrupt, SystemExit):
319
+ raise
320
+ except BaseException:
321
+ pass # 모든 오류 삼킨다
322
+ ```
323
+
324
+ ### 2. 호출 스레드는 네트워크를 만지지 않는다
325
+
326
+ 이벤트 기록은 `queue.put_nowait()` (O(1)) 만 실행한다.
327
+ 큐가 가득 차면 가장 오래된 항목을 먼저 버리고 새 항목을 넣는다. **블로킹 없음.**
328
+
329
+ ```
330
+ 고객 스레드 → put_nowait(event) → [queue] → daemon 스레드 → HTTP POST
331
+ ```
332
+
333
+ ### 3. 전송은 daemon 백그라운드 스레드 1개
334
+
335
+ `threading.Thread(daemon=True)` 로 생성된다.
336
+ daemon 스레드는 Python 인터프리터가 종료될 때 강제 종료되므로 고객 프로세스 종료를 막지 않는다.
337
+
338
+ ### 4. HTTP 타임아웃: connect 2s / read 3s
339
+
340
+ `urllib.request` 에 합산 5s 타임아웃을 설정한다.
341
+ `requests` · `httpx` 등 외부 라이브러리 의존성 없음.
342
+
343
+ ### 5. 전송 실패 시 디스크 스풀 → 지수 백오프
344
+
345
+ 연결 거부 · 5xx · 타임아웃 등 모든 네트워크 장애 시:
346
+ 1. 이벤트를 `~/.xsoft_comply/spool/events.jsonl` 에 JSONL 로 append
347
+ 2. 백오프 대기 (1s → 2s → 4s → 8s → 30s 상한)
348
+ 3. 백오프 후 재전송 시도
349
+
350
+ ### 6. 스풀 파일 상한 (기본 32 MB)
351
+
352
+ 스풀 디렉터리 총 크기가 32 MB 를 초과하면 오래된 파일부터 삭제한다.
353
+ 디스크 쓰기 실패도 삼킨다.
354
+
355
+ ### 7. init() 전에 이벤트를 기록해도 죽지 않는다
356
+
357
+ 큐가 `None` 인 상태에서 `buffer.put()` 은 즉시 반환(no-op) 된다.
358
+
359
+ ### 8. atexit에 flush 등록 (타임아웃 2s)
360
+
361
+ ```python
362
+ import atexit
363
+ atexit.register(_atexit_handler) # init() 최초 호출 시 1회 등록
364
+
365
+ def _atexit_handler():
366
+ flush(timeout=2.0) # 2초 후 반드시 반환
367
+ ```
368
+
369
+ 프로세스가 정상 종료될 때 버퍼에 남은 이벤트를 최대 2초 안에 전송 시도한다.
370
+ 2초 안에 완료되지 않아도 프로세스 종료를 막지 않는다.
371
+
372
+ ---
373
+
374
+ ## 원문 미저장 보장
375
+
376
+ `log_llm_content=True` 로 설정해도 원문(prompt/response) 은 절대 전송되지 않는다.
377
+ 대신 `content_len` (길이) 와 `content_sha256_8` (sha256 앞 8자) 만 metadata 에 포함된다.
378
+
379
+ 다음 metadata 키가 있으면 SDK 에서 자동 제거하고 `_dropped_keys` 에 이름만 기록한다:
380
+ `prompt`, `response`, `messages`, `content`, `input`, `output` 등 25개.
381
+
382
+ ## PII 마스킹
383
+
384
+ 전송 전 로컬에서 자동 마스킹된다:
385
+ - 한국 주민등록번호 `\d{6}-?\d{7}` → `******-*******`
386
+ - 휴대전화 `01X-XXXX-XXXX` → `010-****-****`
387
+ - 이메일 → `a***@domain`
388
+ - 카드번호 16자리 → `앞4-****-****-뒤4`
389
+
390
+ 마스킹 발생 시 `metadata.pii_masked = true`.
391
+
392
+ ## 라이선스
393
+
394
+ MIT