
AI 에이전트를 위한 변조 방지 감사 추적: 해시 체인 방식의 런타임 레코드, 의존성 없음, 누구나 검증 가능.
# halo-record
변조 감지 가능한 **AI 에이전트용 감사 추적** — 해시 체인으로 연결된 Runtime Records를 고객이 직접 확인할 수 있는 Runtime Report로 렌더링합니다.
계측이 캡처하는 모든 행위(도구 호출, 모델 호출, 데이터 접근, 승인)는 추가 전용(append-only) 해시 체인 로그의 하나의 **Runtime Record**가 되며, **Runtime Report**는 그 체인을 HTML 페이지로 렌더링한 것으로, 읽는 사람의 브라우저에서 해시 체인을 다시 계산하고 검증한 레코드로부터 표시 내용을 렌더링합니다(페이지 자체의 코드에 의존하지 않는 검증을 위해서는 JSONL에 대해 `halo verify`를 실행하세요). 체인의 체크포인트를 보유한 누구든지, 그것을 생성한 주체를 신뢰하지 않고도 해당 체크포인트까지의 레코드가 생성 이후 변경되지 않았음을 검증할 수 있습니다 — 그 체크포인트가 핵심 요소입니다: 체인만으로는 기록자를 운영하는 주체를 제외한 모든 사람에 대해 변조 감지가 가능합니다([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)). 고객의 보안 팀이 "귀하의 에이전트가 우리 데이터로 무엇을 했습니까?"라고 물을 때, 문단 대신 링크를 건네면 됩니다. 보안 검토는 이미 SOC 2 체크리스트 옆에서 AI 관련 질문을 하고 있으며, 점점 더 그 질문들은 ISO 42001, EU AI Act의 기록 보존 조항, 그리고 고객 자체 설문지에서 나옵니다. 오늘날에는 서면 보증서만으로도 통과됩니다. 이 프로젝트의 배팅은 그것이 오래가지 않을 것이라는 점입니다.
**[Help Net Security](https://www.helpnetsecurity.com/2026/08/31/halo-record-open-source-ai-agent-audit-trail/)**에 소개됨 (2026년 8월).
레코드 형식은 개방되어 있으며 자유롭게 구현할 수 있습니다. 이 패키지는 참조 구현입니다: 레코더, 검증기, 위트니스 클라이언트, 리포트 서버.
> **halo-record를 사용 중이거나 고려 중이신가요?** 누구이며 무엇을 위해 사용하는지 알려주세요 → [Who's using halo-record?](https://github.com/bkuan001/halo-record/discussions/7)
## 직접 확인하세요
에이전트 내부에 레코더를 설치하라는 요청을 받고 있습니다. 그것을 믿고 받아들여서는 안 됩니다:
- **런타임 의존성 제로.** 표준 라이브러리만 사용합니다. `pip install halo-record`는 정확히 하나의 패키지만 설치합니다.
- **네트워크 호출 없음**, 단 네 가지 옵트인 호출은 예외입니다 — 위트니스에 앵커링(대상 id, 레코드 수, 두 개의 체인 지문 — 헤드와 체인 루트 — 를 전송), 위트니스의 체크포인트 다시 읽기(대상 id 전송), RFC 3161 타임스탬프(체크포인트의 상태 해시만 Timestamp Authority에 전송), 그리고 `halo serve --verify-email`(리포트 뷰어가 입력한 주소로, `$HALO_SMTP_HOST`에 설정한 서버를 통해 SMTP로 일회용 코드와 리포트의 대상 이름을 전송). 모두 호출하지 않는 한 꺼져 있으며, 다른 레코드 내용은 인프라를 벗어나지 않습니다.
- **원시 도구 인자는 해시되며, 함께 마스킹된 요약이 제공됩니다.** 인자는 정규화된 해시와 요약으로 저장됩니다: 알려진 비밀 및 PII 패턴이 마스킹된 인자 텍스트로, 200자로 제한됩니다. 어떤 패턴에도 일치하지 않는 짧은 입력은 요약에 전체가 나타나며, 해시 전용 모드(`summaries=False`)는 요약을 전혀 유지하지 않습니다. 마스킹은 최선의 노력입니다(일반적인 비밀 및 PII 형식에 대한 정규식과 엔트로피 캐치올): 심층 방어로 취급하고 보장으로 여기지 마세요. `summary` 외에 제공하는 결과 필드는 동일한 패턴으로 스캔되고 마스킹되지만, 200자로 잘리지는 않습니다(LIMITS §13).
- **감사하기에 충분히 작습니다.** 리포트 페이지에 내장된 JavaScript와 CSS를 포함하여 약 8,700줄의 Python입니다(공백, 주석, docstring을 제외한 코드 줄 수). 하루면 전부 읽을 수 있습니다.
- **Apache-2.0.**
- **문서가 일급 시민입니다.** [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)(체인이 증명할 수 없는 것), [PRIVACY.md](https://github.com/bkuan001/halo-record/blob/main/PRIVACY.md)(레코드에 포함되는 내용과 머신을 벗어나는 것), [RETENTION.md](https://github.com/bkuan001/halo-record/blob/main/RETENTION.md)(보존 정책 하에서의 운영), 그리고 [REVIEWERS.md](https://github.com/bkuan001/halo-record/blob/main/REVIEWERS.md) — 4개 명령의 독립 검증과 검토 결과를 위한 인용 형식.
각 계층이 증명하는 것 — 이 프로젝트의 핵심 구분([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)): 직접 보유한 체인은 이미 누군가가 보유한 헤드를 기준으로 레코드가 **편집**되지 않았음을 증명합니다; 운영자 외부에 보관된 체크포인트만이 어떤 레코드도 **제거**되지 않았음을 증명합니다; 그리고 어떤 해시도 모든 행위가 **캡처**되었음을 증명하지 않습니다.
| 주장 | 자체 보유 체인 | + 외부 체크포인트 | + 신뢰할 수 있는 캡처 |
|---|---|---|---|
| 확립된 아티팩트에 대한 편집 감지 | ✔ | ✔ | ✔ |
| 커밋된 이력의 재작성 감지 | — | ✔ | ✔ |
| 누락/지연된 체크포인트 감지 | — | ✔ (합의된 주기) | ✔ |
| 모든 행위가 기록되었음을 증명 | — | — | 캡처 경계에 따라 다름 |
**설치 전에 하나 보기:** [샘플 Runtime Report](https://bkuan001.github.io/halo-record/sample-report.html) — 가상 데이터, 실제 체인, 그리고 열면 스크립트가 내장된 레코드의 해시 체인을 브라우저에서 다시 계산합니다.
## 60초 데모
에이전트가 필요하지 않습니다. [uv](https://docs.astral.sh/uv/)를 사용하면 설치할 것이 없습니다:
```
uvx --from halo-record halo demo --serve
```
또는 고전적인 방식으로:
```
pip install halo-record
halo demo --serve
```
둘 중 하나는 두 명의 고객을 둔 가상의 지원 에이전트 벤더를 스캐폴딩하고, 체인을 목격하며(운영자 외부의 하나를 대신하는 로컬 witness 파일 포함 — [LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md) 참조), 그들의 게이트된 Runtime Reports를 제공하고, 브라우저에서 운영자 콘솔을 엽니다. 그런 다음 변조 테스트를 시도해 보세요: `.jsonl` 파일 중 하나에서 한 줄을 삭제하고 다시 로드하세요. 보고서가 이를 잡아냅니다.
## 자신만의 에이전트 기록하기
경계에서 한 줄:
```python
from halo_record import trace
agent = trace(run_my_agent, profile="my-agent", log="audit.jsonl") # wraps your entrypoint; records the run boundary to ./audit.jsonl — add record_call() or a framework adapter at each tool boundary to capture individual calls
```
`from halo import ...` 편의 심(shim)도 함께 제공되지만, PyPI의 `halo` 이름은 무관한 터미널 스피너 패키지에 속해 있으며, 해당 패키지가 설치되어 있으면 임포트에서 우선합니다. `halo_record`는 모호하지 않으므로 예제에서는 이를 사용합니다.
`log=`가 없으면 레코드는 `~/.halo/my-agent.jsonl`로 전달됩니다(에이전트당 체인 하나). 래퍼는 실행 경계를 봉인하며, 증거는 호출별 레코드에 남습니다. 프레임워크 어댑터(아래 매트릭스)로 이를 캡처하거나 — 명시적으로 캡처할 수도 있으며, 이는 위임 링크가 어떻게 연결되는지도 보여줍니다:
```python
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"}) as call: # one sealed record per tool call
call.result = crm.lookup("acct-9")
with record_call(rec, "payments.refund", {"amount": 120},
parent_id=rec.last_record_id()) as call: # child links to the action that spawned it
call.result = payments.refund(120)
```
그런 다음 보고서를 렌더링합니다:
```
halo report audit.jsonl -o report.html # one chain -> HTML that re-verifies in the browser
halo serve ./records --port 8721 # all tenants, gated per customer
```
0.2.45에서 처음 제공된 디렉터리를 업그레이드하시나요? `halo serve`는 시작할 때 이전의 각 grant를 다시 읽습니다. 레코드에 subject id가 없는 파일(예: `agent.jsonl`과 같은 일반 `Recorder` 파일)에 대한 grant는 격리되며, 이를 재발급하는 `halo grant` 명령과 함께 매 시작 시 출력됩니다 — [LIMITS.md §17, "Serving a directory first served by 0.2.45"](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#17-one-chain-file-per-subject)를 참조하세요.
퀵스타트는 브라우저에서 자신의 에이전트의 Runtime Report를 보고 있을 때 끝납니다. JSONL 파일만 받고 보고서를 받지 못했다면 뭔가 잘못된 것입니다: 이슈를 열어주세요.
### 검증 블록
가드레일이나 정책 계층이 해당 작업을 검사했다면, 그 판정이 레코드에 실릴 수 있습니다 — 게이트가 결정한 내용을 기록하고, 다른 모든 필드와 마찬가지로 해시 체인에 봉인되는 선택적 블록입니다:
```python
from halo_record import build
build("tool_call", "security", tool="payments.refund",
verification={"status": "allowed", "verifier": "gate/1.2",
"policy_ref": "sha256:1f3a...",
"checked_at": "2026-08-01T12:00:00Z"})
```
한 가지 명명 관련 참고 사항: 이 패키지는 `record` *함수*(데코레이터)를 내보내며, 이는 패키지 객체에서 `halo_record.record` *모듈*을 가립니다. 내부 요소를 사용하려면 `import halo_record.record as record` 대신 모듈 경로에서 직접 가져오세요 — `from halo_record.record import build`.
이는 레코드에 다음과 같이 봉인됩니다:
```json
"verification": {"status": "allowed", "verifier": "gate/1.2", "policy_ref": "sha256:1f3a...", "checked_at": "2026-08-01T12:00:00Z"}
```
`record_call(...)`는 동일한 `verification=` 키워드를 받습니다. `status`는 블록 내에서 필수이며, `verifier`, `policy_ref`, `checked_at`은 선택 사항입니다. 각 상태의 의미는 다음과 같습니다:
| 상태 | 게이트가 보고하는 내용 | 작업이 실행되었는가? |
|---|---|---|
| `allowed` | 작업을 허용함 | 예 — 작업이 진행됨 |
| `blocked` | 작업을 거부함 | 이 필드가 아니라 통합에 의해 결정됨 — 레코드가 여전히 결과를 담고 있을 수 있으며, 차단 자체만으로 미실행이 증명되지는 않음 |
| `modified` | 실행 전에 작업을 변경함 — `action.input`은 수정 후 **실행된 그대로**의 작업을 설명함 | 예, 변경된 형태로 |
| `unverified` | 실행되었으나(또는 참조되었으나) 판단을 내리지 않음 — 검증 주장이 전혀 이루어지지 않았음을 의미하는 블록 부재와는 구별됨 | 예 — 판정 없이 작업이 진행됨 |
이 블록은 운영자의 통합 코드에 의해 제공되며, 게이트가 말했다고 보고하는 내용을 기록합니다 — `principal`과 동일한 신뢰 태세입니다([LIMITS](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#11-verification-status-is-the-gates-report-not-halos-finding) 참조). 봉인은 상태가 사후에 편집되지 않았음을 증명할 뿐, 검사가 발생했는지, 판정이 올바른지, 또는 차단된 작업이 실행되지 않았는지를 증명하지 않습니다. 이는 독립적인 검증이 아닙니다.
`policy_ref`를 증거로 사용할 수 있으려면 규칙 집합의 콘텐츠 해시를 사용하고 규칙 집합 아티팩트를 보관하십시오 — 해석할 수 없는 레이블은 해당 필드를 장식용으로 만듭니다.
## 이미 실행 중인 것에 연결하기
| 경계에서 캡처됨 | 기존 텔레메트리에서 수집됨 |
|---|---|
| 네이티브 레코더 (`from halo_record import trace`) | OpenTelemetry GenAI 스팬 |
| MCP 인터셉터 | LiteLLM 콜백 |
| LangChain / LangGraph 콜백 | Langfuse 내보내기 |
| OpenAI Agents SDK 훅 | 모든 게이트웨이 / 리버스 프록시 로그 |
| Claude Agent SDK 훅 | Claude Code 및 Codex CLI `PostToolUse` 훅 (도구 실행 후 발동) |
프레임워크 어댑터와 수집 경로는 각 레코드에 `source` 태그를 찍으므로, 보고서는 각 증거 조각이 어떻게 수집되었는지 공개합니다. 캡처된 레코드와 수집된 레코드는 동일한 체인에 존재합니다.
LangChain / LangGraph의 경우, 이는 콜백 핸들러입니다:
```python
from halo_record import Recorder
from halo_record.integrations.langchain import HaloCallbackHandler
recorder = Recorder("audit.jsonl")
result = my_chain.invoke(inputs, config={"callbacks": [HaloCallbackHandler(recorder)]}) # every tool call becomes a record
```
MCP의 경우, 한 번의 호출이 클라이언트 세션을 감싸며 — 그러면 *어떤* MCP를 사용하는 에이전트든, 어떤 프레임워크가 구동하든 관계없이 모든 도구 호출에 대해 레코드를 내보냅니다:
```python
from mcp import Client # mcp 2.x
from halo_record import Recorder
from halo_record.integrations.mcp import instrument_client_session
async def run(server): # an MCPServer, a server URL, or stdio parameters
async with Client(server) as client:
instrument_client_session(client, Recorder("audit.jsonl"), server="stripe")
await client.call_tool("charge", {"customer": "cus_123", "amount": 500}) # recorded
```
래핑된 클라이언트에서의 모든 `call_tool()`이 기록되며, 오류 결과를 반환하는 도구는 `error`로 봉인됩니다(어댑터는 `isError`와 mcp 2.x의 `is_error`를 모두 읽습니다). mcp 1.x에서는 `Client` 대신 초기화된 `ClientSession`을 전달하세요. 래퍼는 동일합니다.
게이트웨이 또는 프록시 로그(Cloudflare AI Gateway, Portkey, 모델 앞단의 nginx)의 경우, 로그 행을 체인에 매핑합니다 — 경계에서 캡처된 것이 아니라 명시적으로 수집된 것으로 태그됩니다:
```python
from halo_record.integrations.gateway import record_log
record_log(Recorder("audit.jsonl"), {"tool": "gen_ai:gpt-4o", "model": "gpt-4o", "status": 200, "subject": "acme-corp"})
```
OpenTelemetry GenAI 스팬을 내보내는 모든 것(CrewAI, LlamaIndex, 그리고 OTel 계측을 갖춘 대부분의 에이전트 프레임워크)은 OTel 어댑터를 통해 체인에 들어오며, [TypeScript 패키지](https://github.com/bkuan001/halo-record-ts)는 Vercel AI SDK와 JS 에이전트 생태계를 위한 네이티브 어댑터를 제공합니다. 여러분의 스택에 맞는 어댑터가 없나요? 이슈를 열어주세요. 대부분의 어댑터는 약 100줄 정도입니다.
## 코딩 에이전트 기록하기 (Claude Code 또는 Codex)
Claude Code는 모든 도구 호출 후에 `PostToolUse` 훅을 발생시킵니다. 이를 `halo hook`으로 지정하면 각 작업 — 파일 쓰기, 셸 명령, MCP 커넥터 호출 — 이 로컬 체인의 레코드가 됩니다. 코드 변경 없이, 설정 항목 하나만 추가하면 됩니다:
```json
{
"hooks": {
"PostToolUse": [
{"matcher": "*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
`~/.claude/settings.json`에 이를 추가하면 기록이 `~/.halo/audit.jsonl`에 저장됩니다(`$HALO_LOG`로 재정의 가능). 데이터, 네트워크, 외부 상태에 접근하지 않는 순수 오케스트레이션 도구는 건너뜁니다 — 이 체인은 사고가 아니라 신뢰 경계를 넘는 행위를 기록합니다. `HALO_HASH_ONLY=1`을 설정하면 요약 없이 콘텐츠 해시만 기록합니다. `HALO_AGENT_VERSION`(그리고 선택적으로 `HALO_AGENT_MODEL`)을 설정하면 모든 기록을 이를 생성한 에이전트 빌드에 연결합니다 — 감사자가 특정 기간에 실행 중이던 버전을 물으면, 내보내기가 기억이 아니라 열 단위로 답합니다.
Codex CLI는 동일한 이벤트 형태로 동일한 라이프사이클 훅을 제공합니다(훅은 기본적으로 활성화됨). `~/.codex/hooks.json`에 이를 추가하면 Codex의 셸 명령, `apply_patch` 편집, MCP 호출이 동일한 체인에 기록됩니다:
```json
{
"hooks": {
"PostToolUse": [
{"matcher": ".*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
훅은 이벤트 자체에서 두 가지를 구분하고(Codex는 `turn_id`와 `model`을 추가함) 각 레코드에 `claude-code` 또는 `codex`라는 레이블을 붙입니다. `HALO_HOOK_AGENT`를 설정하면 하나를 강제할 수 있습니다. 둘 다 수집 계층입니다. `PostToolUse` 훅은 도구가 실행된 후에 발생하므로, 레코드는 하네스가 보고하는 내용을 바탕으로 구성됩니다.
보고서가 "이 실행이 어떤 규칙 아래에서 발생했는가?"에 답해야 한다면, 세션의 유효 권한에 대한 JSON 스냅샷을 `HALO_AUTHORITY_FILE`에 설정하세요. 프라이버시를 안전하게 유지하세요. 원시 프롬프트, 비공개 정책 텍스트, 시크릿, 전체 도구 스키마가 아니라 해시와 참조를 사용하세요. 알려진 시크릿 및 개인 데이터 형식은 봉인 시점에 마스킹되지만, 해시와 참조는 그대로 통과하며 자유 형식 텍스트는 감지되지 않습니다(LIMITS §6 참조). 기본 권한이 변경되지 않은 동안에만 `snapshot_id`를 재사용하세요. 동일한 id와 변경되지 않은 내용을 가진 연속 레코드는 압축됩니다. 변경된 내용에 재사용된 id는 전체가 저장되며, 이때 고지가 함께 표시됩니다.
```json
{
"snapshot_id": "auth_2026_07_08T1100Z",
"captured_at": "2026-07-08T11:00:00Z",
"scope": "session",
"workspace": {"path_hash": "sha256:...", "git_commit": "abc1234"},
"refs": [
{"kind": "project_rules", "id": "CLAUDE.md", "hash": "sha256:...", "loaded": true, "truncated": false},
{"kind": "mcp_tool_registry", "id": "filesystem", "hash": "sha256:..."}
],
"omissions": [{"kind": "private_policy", "reason": "customer_secret", "hash": "sha256:..."}],
"stale_if": ["project_rules_hash_changed", "mcp_tool_registry_hash_changed"]
}
```
```sh
HALO_AUTHORITY_FILE=./authority.json halo hook
```
스냅샷은 액션 레코드와 동일한 해시 체인에 봉인됩니다. 좋은 기본값은 시작 시 세션 수준 스냅샷 하나를 두고, 규칙, Skills, 훅, MCP 도구 레지스트리 또는 컴팩션 정책이 변경될 때 새 스냅샷을 추가하는 것입니다. 긴 세션을 간결하게 유지하기 위해 동일한 `authority.snapshot_id`를 가진 연속 레코드는 첫 번째 전체 스냅샷 이후에 컴팩션됩니다. 이후 레코드는 `{"snapshot_id": "...", "same_as_previous": true}`만 유지합니다. 포인터는 해시 체인으로 유지되지만, 부피가 큰 refs/omissions/stale-if 블록은 모든 액션마다 반복되지 않습니다. (컴팩션은 레코더 프로세스별로 이루어집니다. 도구 호출마다 하나의 프로세스를 생성하는 훅 스타일 캡처는 이전 전체 스냅샷이 꼬리 레코드가 아닐 때마다 전체 본문을 다시 저장하므로, 단명 프로세스는 재사용 가드를 위해 체인 크기를 희생합니다.)
SDK 사용자는 동일한 블록을 직접 연결합니다 — `build(..., authority={...})` 또는 `record_call(..., authority={...})`; 해시 전용 캡처도 동일한 표면입니다(이들 중 어느 것이든 `summaries=False`):
```python
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"},
authority={"snapshot_id": "auth_1", "rules_hash": "sha256:..."},
summaries=False) as call: # hash-only: no summaries, no excerpts
call.result = crm.lookup("acct-9")
```
그런 다음, 평소처럼:
```
halo verify ~/.halo/audit.jsonl
halo report ~/.halo/audit.jsonl -o report.html
```
post-action 훅을 노출하는 모든 에이전트 런타임은 동일한 명령을 공급할 수 있습니다. 훅은 stdin에서 하나의 이벤트를 JSON으로 읽고 하나의 레코드를 추가합니다.
하나의 체인, 한 번에 하나의 작성자. 체인은 연결 리스트입니다. 동일한 head를 읽고 둘 다 추가하는 두 작성자는 체인을 분기시키며(동일한 선행자를 주장하는 두 레코드), 검증은 영향을 받은 레코드를 지목합니다. `Recorder`는 자체 추가를 사이드카 잠금(여기서는 POSIX `flock`, TypeScript 패키지에서는 잠금 디렉터리)으로 직렬화하며, `halo hook`은 `Recorder`를 통해 추가하므로 위의 훅 설정은 보호됩니다. 체인 파일에 직접 쓰는 모든 것 — 직접 만든 훅, 병렬 워커, 로그 전송기 — 은 read-head-then-append 시퀀스 전반에 걸쳐 동등한 배타적 잠금을 보유하거나 프로세스별 체인에 기록해야 합니다. [LIMITS.md 섹션 9](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#9-single-writer-chains)는 언어 간 경계를 포함하여 이 내용을 전부 다룹니다.
## 기록이 실패할 때
레코드는 액션이 실행된 후에 기록됩니다. Halo는 레코드를 기록할 수 없을 때 액션을 차단하지 않으며, 액션 전에 레코드를 기록하는 것은 제공되지 않습니다.
- **기본값: 액션의 결과가 그대로 전달됩니다.** 레코드를 기록할 수 없을 때(디스크 가득 참, 권한, 잠금 타임아웃), `trace()`, `record_call`, `@record`, 프레임워크 어댑터(OpenAI Agents, LangChain, LiteLLM, OpenTelemetry), MCP 래퍼 및 `halo hook`은 액션 자체의 결과를 변경 없이 반환하거나, 액션이 실패한 경우 액션 자체의 예외를 다시 발생시킵니다. stderr의 경고가 누락된 레코드를 지목합니다: `halo-record: FAILED to record '<tool>' (<error>); the action ran, but its record is NOT in the evidence log. ...` (사용 가능한 stderr가 없으면 `halo_record` 로거로 전달됩니다). LangChain 핸들러와 OTel 익스포터도 손실을 집계합니다(`lost_records`). 체인에 있는 어떤 것도 기록되지 않은 레코드를 보여줄 수 없으므로, 나머지 레코드는 여전히 검증됩니다. witness 체크포인트 주기를 지켜보는 검토자는 정체된 체인을 보게 되며, 이후 레코드들 사이에 단일 누락 레코드가 있는 경우는 stderr 줄에서만 드러납니다.
- **엄격 모드는 예외도 발생시킵니다.** `strict_recording=True`를 전달하거나(`trace`, `record_call`, `@record`, `instrument_client_session` 또는 어댑터의 생성자에), `HALO_STRICT_RECORDING=1`을 설정하면 동일한 stderr 줄이 기록된 다음 기록 오류가 발생합니다. 이 오류는 `trace`, `record_call`, `@record`, MCP 래퍼, OpenAI Agents 훅 및 LangChain 핸들러(LangChain의 `raise_error`를 설정함)로부터 호출자에게 도달합니다. OTel 익스포터나 LiteLLM 로거로부터는 호출자에게 도달할 수 없습니다. 해당 프레임워크들은 익스포터나 콜백이 발생시키는 것을 잡아서 로그로 남기므로, 거기서는 두 모드 모두에서 stderr 줄이 신호입니다(LIMITS.md 섹션 18). 오류가 발생할 때 액션은 이미 실행된 상태이므로, 이를 "액션이 실패했다"고 해석하고 재시도하는 호출자는 액션의 부작용을 반복하게 됩니다(환불이 두 번 발행됨). 누락된 레코드가 그 위험보다 더 나쁜 경우에만 엄격 모드를 사용하십시오.
- 레코더가 없는 `record_call`과 `@record`(전달된 것도 없고 `trace`에 의해 바인딩된 것도 없음)는 래핑된 코드가 실행되기 전에 `RuntimeError`를 발생시킵니다.
- `@record`와 `trace()`는 `async def` 함수를 받습니다. 래퍼 자체가 `async def`입니다. 함수를 await한 다음 반환되거나 발생한 것을 기록합니다(취소는 오류로 기록됨). `trace()`는 본문이 await되는 동안 레코더를 바인딩된 상태로 유지하므로, 비동기 에이전트 내부의 `record_call` 또는 `@record` 도구는 추적된 체인에 기록되고, 동시 에이전트들은 각자 자신의 체인을 유지합니다. await 가능한 객체를 반환하는 일반 함수는 반환될 때 기록됩니다. 비동기 제너레이터 함수는 호출될 때 제너레이터 객체를 결과로 하여 기록되며, 그것이 yield하는 것은 기록되지 않습니다.
## 무결성 vs. 완전성 (이 부분을 읽으십시오)
각 계층이 무엇을 증명하는지 정확히 파악하십시오. 왜냐하면 그것들은 서로 다른 주장이며, 그 차이가 핵심이기 때문입니다:
자체 보유 체인은 **확립된 head에 대한 무결성**을 증명합니다. 누군가 이미 보유한 체인 head가 주어지면, 그 뒤의 레코드에서의 모든 편집, 재정렬 또는 삭제가 탐지 가능해집니다. 그 자체로는 — 운영자 외부의 누군가가 head를 보기 전에는 — 체인은 내부 일관성을 증명할 뿐, 이력을 증명하지 않습니다. 운영자가 레코드를 삭제하고 다시 봉인할 수 있으며, 새 파일은 검증을 통과할 것입니다. 체인은 그 head가 운영자의 통제를 벗어나는 순간 **역사적으로 커밋**됩니다.
그것이 witness입니다. 운영자 외부의 당사자가 체인의 주기적 체크포인트를 보유하는 것 — subject id, 레코드 수, 그리고 두 개의 체인 지문(head와 체인 루트). 정확한 페이로드이며, 그 외에는 아무것도 없습니다. 체크포인트는 커밋된 이력의 재작성을 탐지 가능하게 만들고, 놓친 체크포인트 자체가 가시적인 이벤트입니다:
```
halo anchor audit.jsonl witness.jsonl # anchor a checkpoint to a local witness
halo anchor audit.jsonl witness.jsonl --check # completeness verdict against it
```
*time*의 경우, 외부 RFC 3161 타임스탬프는 체크포인트가 스스로 주장하는 시계를 운영자의 통제 밖에 있는 Timestamp Authority의 증명으로 대체합니다 — "이 체인은 늦어도 T 시점에 이 헤드에 도달했다"는 것으로, 호스팅 인프라 없이 제3자가 검증할 수 있습니다. 기본 TSA는 무료 freetsa.org입니다(평가용으로 적합). 프로덕션의 경우 `--tsa`를 운영자의 통제 밖에 있는 TSA(예: DigiCert / Sectigo, 또는 신뢰 당사자가 지정한 TSA)로 지정하십시오:
```
halo anchor audit.jsonl witness.jsonl --timestamp # attach a TSA time proof to the checkpoint
halo anchor audit.jsonl witness.jsonl --check # reads the token's claimed time
```
`--check`는 토큰이 이 체인 상태에 바인딩되는지 확인하고 증명된 시간을 읽지만, TSA의 서명을 **검증하지는 않습니다** — 이는 의도적으로 표준 도구에 맡겨 검토자가 우리 코드를 신뢰하지 않도록 합니다. 시간을 독립적으로 검증하려면 (보안 검토자에게 건네는 것이 바로 이것입니다):
```
# tsa.token_b64 lives in the witness log; decode the latest one to a standard .tsr file
python3 -c 'import json,base64; cps=[json.loads(l) for l in open("witness.jsonl") if l.strip()]; t=[c["tsa"] for c in cps if c.get("tsa")][-1]; open("token.tsr","wb").write(base64.b64decode(t["token_b64"])); print(t["digest"])'
curl -s -o tsa-ca.pem https://freetsa.org/files/cacert.pem # CA for the default TSA (a commercial TSA publishes its own)
openssl ts -verify -digest <the digest printed above> -in token.tsr -CAfile tsa-ca.pem # → "Verification: OK"
```
또 하나의 경계를 분명히 말하자면, 체인도 증인도 모든 실제 행동이 기록기를 통과했다는 것을 증명하지는 않는다. 그것은 **캡처 완전성(capture completeness)** — 기록기가 스택 어디에 위치하는지(네이티브 계측, 훅, 게이트웨이 수집)에 관한 속성이지, 어떤 해시의 속성이 아니다. 기록에 `source` 태그가 붙는 이유가 바로 이것이다. 이 페이지 상단의 "직접 확인하기" 아래에 있는 주장 표가 이 세 계층을 요약한 것이다.
누구나 증인을 운영할 수 있다. 직접 운영하는 증인은 이력을 *당신*에게 커밋한다. 그것을 *당신의 고객*에게 커밋하려면 고객이 신뢰할 만한 이유가 있는 증인이 필요하다. 어느 쪽이든 프로토콜은 열려 있다.
호스팅되고 공인된 증인이 이 프로젝트가 스스로를 유지하는 방법이다. 얼리 액세스: [email protected].
## 체인 내의 개인 데이터
체인은 추가 전용(append-only)이다. 기록에 봉인된 것은 무엇이든 그대로 남는다. 왜냐하면 그것을 제거하면 그 이후의 모든 것에 대한 검증이 깨지기 때문이다. 도구 인수는 이미 처리되어 있다 — 해시와 200자로 제한된 마스킹된 요약으로 저장된다(해시 전용 모드에서는 요약을 보관하지 않는다. id 필드에서는 요약에서와 마찬가지로 알려진 비밀 형식의 토큰을 마스킹하고, 그 밖의 비밀 형태 토큰은 `sha256:`에 16자리 16진수를 붙인 것으로 대체한다 — 단, `subject.id`는 예외로, 이 필드는 기록을 해당 테넌트의 체인으로 라우팅한다).
그 문장의 한계에 주목하라: *마스킹된 것*이지, 제거된 것이 아니다. [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md) 섹션 6은 이름이나 우편 주소에는 신뢰할 만한 패턴이 없으므로 둘 다 탐지되지 않고 둘 다 마스킹되지 않는다고 명시한다. 그리고 `subject`는 당신이 제공하는 텍스트를 담는 유일한 필드가 아니다 — `principal`, `approver`, `session_id`, `agent`, `authority`, `data` 그리고 모든 요약도 마찬가지다.
효과적인 패턴은 이렇다: 체인에는 안정적인 가명 id를 넣고, 개인에 대한 매핑은 삭제할 수 있는 시스템에 보관하라. 그러면 삭제 요청은 매핑을 삭제함으로써 충족된다. `subject`는 사람이 아니라 테넌트 조직을 가리키도록 유지하라:
```python
from halo_record import build
build("tool_call", "privacy", subject={"id": "acme", "name": "Acme Corp"})
```
이를 강제하는 설정은 없습니다 — 기록기를 어떻게 호출하느냐의 규율 문제입니다.
이는 삭제를 다루기 쉽게 만들어 주지만, 익명화가 아니며, 아직 내장된
보존 기간이나 정리 기능은 없습니다.
[LIMITS.md 섹션 13](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#13-personal-data-and-erasure)에 전체 필드
목록이 있으며, 매핑이 사라진 뒤에도 저장된 입력 지문이 추측 가능한 값을 확인할 수 있는
이유를 설명하고, 검토자가 물어야 할 질문들로 끝맺습니다.
모델 호출 기록하기 (구매자의 첫 번째 질문: "어떤 모델이 내 데이터를 보았나?"):
```python
from halo_record import Recorder, record_model_call
rec = Recorder("audit.jsonl")
record_model_call(rec, provider="anthropic", model="claude-sonnet-4-6",
zdr=True, purpose="draft support reply",
subject="acme") # tool=model.generate, scope=model:anthropic
```
모델은 또한 `agent.model`로, 보존 조건은 `data.zdr`로 봉인되어,
내보내기의 모델 열과 정책 규칙이 이를 읽을 수 있으며, 둘 다
`summaries=False`에서도 유지됩니다. `messages=`를 전달하면 레코드의 입력에
그 개수가 담깁니다 — 개수일 뿐, 내용은 결코 아닙니다.
## 컴플라이언스 스택에서의 위치
halo-record는 증거 계층이지 인증이 아닙니다. 이는 평가 프레임워크가 서로 다른
---
[Read more](https://github.com/bkuan001/halo-record)