운영자 승인 루프은(는) 무엇을 하나요?
외부 상대방과 대화하는 에이전트는 자체 판단으로 메시지를 보내서는 안 되며, 운영자에게 내부적으로 항상 정보를 제공해야 합니다. 이 스킬은 다음과 같은 계약을 정의합니다: 모든 외부 발신 초안은 의무(obligation)로 제출되고, 운영자가 정확한 텍스트를 결정하며, 발송 원장(delivery ledger)이 무엇이 나갔는지 증명합니다.
사용 시기
- 에이전트가 공유 채널, 이메일 또는 채팅에서 고객, 공급업체, 투자자 또는 파트너에게 답변 초안을 작성하고, 발송 전에 사람이 승인해야 하는 경우
- 각 발송 메시지를 정확한 초안 텍스트, 승인한 운영자, 결정 시간과 연결하는 감사 추적(audit trail)이 필요한 경우
- 오래된 승인이 재작성된 초안을 발송하거나, 두 작업자가 동일한 승인 메시지를 두 번 발송하는 상황을 경험한 경우
- 원장에 이미 있는 사실을 초안이 계속해서 상대방에게 다시 묻는 경우
작동 방식
객체
| 객체 | 의미 |
|---|---|
| 의무(Obligation) | 상대방에게 해야 할 한 가지 일. 상태는 drafted, approved 또는 rejected, sent 순으로 변경됩니다. direction, counterparty, channel, updated_at 에포크를 포함합니다. |
| 초안(Draft) | 정확한 초안 텍스트, 해당 텍스트의 sha256, 원본 좌표(플랫폼, 채널, 스레드, 사용자), 우선순위(P0~P3)를 보유한 사이드카 행. 의무당 하나이며, 재제출 시 교체됩니다. |
| 결정(Decision) | 운영자의 승인 또는 거부. 운영자 ID, 논스(nonce), 결정 대상이 된 초안 에포크와 함께 기록됩니다. |
| 승인 스냅샷(Approval snapshot) | 이미 권한이 부여된 결정 작성자가 기록한 불변 텍스트, 해시, 에포크 및 대상. 스냅샷이 없으면 발송을 허가할 수 없습니다. |
| 클레임(Claim) | 임의 토큰과 상태를 가진 지속적 예약; 의무당 최대 하나의 활성 클레임만 허용됩니다. |
| 발송(Delivery) | 하나의 (의무, 결정) 쌍에 대한 한 번의 발송 또는 통지를 증명하는 원장 행. |
참조 스키마는 references/approval-ledger.sql에 있습니다.
초안 제출
- 입력 정리. 제어 문자를 제거하고, 한 줄 필드의 공백을 축소하며, 길이 제한(초안, 요약, 컨텍스트, 상대방)을 적용합니다. 빈 필드나 크기가 초과된 필드는 자동으로 잘리지 않고 거부됩니다.
- 기준 게이트(아래 참조)를 실행합니다. 제출을 거부할 수 있습니다.
- 초안 텍스트를 sha256으로 해싱합니다. 해시 접두사는 승인 패널이 승인 중인 텍스트를 보여주기 위해 요약에 포함됩니다.
- Upsert(갱신 또는 삽입). 동일한 (counterparty, channel)에 대해 열린 초안 의무가 이미 존재하면 초안 사이드카를 교체하고 의무의
updated_at을 업데이트합니다. 이 업데이트가 에포크 순환(epoch rotation)입니다. 이전 에포크에 키가 지정된 결정은 더 이상 새 텍스트를 해제할 수 없습니다. 그렇지 않으면 상태가drafted인 새 의무를 삽입합니다. - 제출 영수증을 구성되고 확인된 내부 운영 대상(verified internal ops destination)으로만 라우팅합니다. 발신지가 해당 내부 대상이면 거기서 확인합니다. 절대 자동 침묵하지 않음(never-silent)은 외부 자동 응답이 아닌 내부 보고를 의미합니다. 초안 해시, 승인 상태, 운영자 ID 및 워크플로 메타데이터를 상대방이 볼 수 있는 채널에 노출하지 마십시오. 알 수 없거나 분류되지 않은 발신지는 조용히 유지됩니다. 다이렉트 메시지가 자동으로 내부 채널로 간주되지는 않습니다.
확인된 내부 대상이 없으면 제출 결과를 내부 도구 결과 또는 운영자 화면에 유지합니다. 외부 또는 알 수 없는 발신지로 대체(fallback)하지 마십시오. 외부인에게 노출되는 도구 결과는 내부 화면이 아닙니다.
초안 제출은 외부 응답을 승인하지 않습니다. 정책상 허용되는 명확화 질문이나 중립적 응답은 별도의 외부 발신 결정이며, 상대방-채널 규율(counterparty-channel-discipline)의 기존 멘션, 채널, 초안 전용, 고정 및 금지 제약 조건을 따릅니다. 또한 내부 승인 메타데이터를 공개해서는 안 됩니다.
기준 게이트
초안이 제출되기 전에 상대방의 현재 기준(시간 원장, 계약 저장소 또는 CRM)을 쿼리합니다:
- 서명되었거나 전달된 계약이 기록된 경우: 증거와 권장 사항과 함께 제출을 거부합니다. 서명 후 상대방에게 사양에 대해 묻는 것은 이 게이트가 막고자 하는 정확한 실패 사례입니다.
- 운영자 재정의:
force_despite_signed_contract는 제출을 허용하고 초안 컨텍스트에[BASELINE_OVERRIDE_SIGNED_CONTRACT]를 표시합니다. - 게이트 서비스에 연결할 수 없는 경우: 제출은 계속되고 컨텍스트에
[BASELINE_CHECK_UNAVAILABLE]이 표시됩니다. 패널은 보호 장치가 꺼져 있음을 확인합니다. 장애가 발생해도 게이트가 자동으로 비활성화되지 않습니다. - 사실을 사용할 수 있는 경우, 가장 최근 몇 가지 사실을
[BASELINE FACTS: ...]요약으로 컨텍스트에 첨부하여 초안이 현재 진실과 함께 제출되도록 합니다.
결정
승인 패널은 상태가 drafted이고 방향이 we_owe_them인 의무를 나열합니다. 승인 또는 거부는 초안 에포크(draft_updated_ts)를 포함하는 결정 행을 쓰고 동일한 트랜잭션에서 의무 상태를 변경합니다. 에포크가 현재 updated_at과 일치하지 않는 결정은 유효하지 않으며(stale) 아무것도 해제해서는 안 됩니다.
이미 권한이 부여된 승인 결정의 경우, 동일한 트랜잭션에서 불변 obligation_approval_snapshots 행(결정 및 의무 ID, 현재 초안 에포크, 정확한 텍스트 및 SHA-256, 플랫폼/채널/스레드, kind draft_sent)을 삽입합니다. 결정 작성자는 쓰기 전에 권한을 설정해야 합니다. 참조 구현은 운영자를 인증하거나 결정을 생성하지 않습니다. 자동 승인 정책은 참조에 의해 활성화되거나 확장되지 않습니다. 스냅샷이 없는 레거시 결정은 명시적 조정(reconciliation) 또는 새 승인이 필요합니다. 현재 변경 가능한 초안에서 권한을 역으로 채우지(backfill) 마십시오.
발송
SQLite 참조는 references/approval_claims.py에 있습니다. 발송 권한을 부여하지만 전송(transport)을 호출하지는 않습니다. SQL 픽스처에서 초기화된 기존 로컬 참조 데이터베이스를 사용하십시오. 모듈은 스키마 또는 프로덕션 마이그레이션을 적용하지 않습니다. 신뢰할 수 있는 결정 작성자만 승인 레코드를 채울 수 있습니다. 모든 작성자는 외래 키(foreign keys)와 재귀적 트리거(recursive triggers)를 활성화하고 스키마 가드를 준수해야 합니다. 관리자에 의한 데이터베이스 변조는 이 모델의 범위 밖입니다.
- 바인딩된 승인 초안을 발견합니다. 발견은 권한이 아닙니다.
claim()은 자체BEGIN IMMEDIATE트랜잭션을 열고, 현재 승인된 에포크, 정확한 텍스트, 계산된 SHA-256 및 전체 대상을 스냅샷과 비교하여 검증한 후 고유 클레임을 삽입하고 토큰을 반환합니다. 충돌이 발생하면 전송 전에 작업자를 중단시킵니다. 완료된 영수증은 다시 클레임할 수 없습니다. begin_dispatch()는 바인딩을 재검증하고 원자적으로claimed를dispatching으로 토큰을 사용하여 변경합니다. 승리한 호출자만 커밋 후에 정확한draft_text와 대상을 받습니다. 텍스트를 다시 생성하거나, 전송을 위해 변경 가능한 사이드카를 다시 읽거나, 다른 시도에 페이로드를 재사용하지 마십시오. 중첩 호출자 트랜잭션은 거부됩니다. 권한이 이후 호출자 커밋에 의존할 수 없습니다. 전송 중에는 데이터베이스 트랜잭션이 열려 있지 않습니다.- 확인된 성공 결과는
complete()로 전달되어, 원자적으로 발송 좌표를 기록하고, 클레임을 delivered로 표시하며 의무를sent로 변경합니다. 동일한 완료는 아무 작업도 하지 않습니다(no-op). 충돌하는 좌표는 실패합니다. 영수증 UNIQUE 키는 레코드를 중복 제거하며, 이전 외부 효과는 고려하지 않습니다. - 예외, 타임아웃, begin-dispatch 후 작업자 중단, 또는 영수증 지속화 실패는 차단된 시도(blocked attempt)를 남깁니다.
mark_unknown()은 불확실성을 기록합니다. 알 수 없는(unknown) 클레임은 만료되지 않으며, 다시 열리지 않고, 자동 재시도되지 않으며, 해당 의무에 대한 다른 결정이 우회하는 것을 허용하지 않습니다. 신뢰할 수 있는 호출자는 확인된 성공 좌표와 증거와 함께reconcile()을 사용할 수 있습니다. 모듈은 해당 증거를 확인하지 않습니다. 영수증이 없다고 해서 전달되지 않았음을 증명하지는 않습니다.
보장 사항은 승인된 결정당 한 번의 자동 발송 시도이지, 정확히 한 번의 외부 전달이 아닙니다. begin-dispatch 후 전송 전에 충돌이 발생하면 0회 발송과 유지된 클레임이 남을 수 있습니다. 알 수 없는 결과를 해제하고 새 시도를 허용하려면 원래 실행자를 차단(fencing)하고 제공자 의미를 확인해야 합니다. 이 참조는 의도적으로 그러한 재시도 작업을 제공하지 않습니다.
| 클레임 상태 | 허용된 다음 상태 |
|---|---|
| claimed | dispatching 또는 발송 전 취소 |
| dispatching | delivered 또는 unknown |
| unknown | 신뢰할 수 있는 조정을 통해서만 delivered |
| delivered, cancelled | 최종 상태; 결정 키 재사용 불가 |
클레임이 활성화되어 있는 동안 데이터베이스 가드는 의무, 초안 및 결정 쓰기를 포함한 교체를 차단합니다. 스냅샷과 클레임은 삭제할 수 없습니다. 재제출 전에 토큰으로 클레임된 작업을 취소하십시오. 그런 다음 유효하지 않은 토큰은 아무것도 부여하지 않습니다. 발송이 시작된 후에는 조정(reconciliation)을 위해 새 편집이나 취소를 보류하십시오. 이는 진행 중인 작업을 회수하는 척하는 대신 변경 사항을 직렬화합니다.
거부된 결정과 초안 사이드카/스냅샷이 없는 레거시 행은 이 외부 초안 발송 경로에 진입하지 않습니다. 내부 운영자 화면에 수동 처리를 위해 보고하십시오. 내부 영수증 바닥글은 내부로 유지됩니다:
approved by <operator> · receipt <decision_id> · draft sha256 <prefix>.
이미 승인된 외부 텍스트를 변경하여 워크플로 메타데이터를 추가하지 마십시오.
집중된 로컬 검증은 임시 데이터베이스, 별도 연결 및 시뮬레이션된 시도 카운터를 사용하며, 제공자나 실제 메시지를 사용하지 않습니다:
python3 -m unittest discover -s tests/skills -p 'test_approval_delivery_claims.py'.
테스트는 SQLite 직렬화 지원이 있는 Python 3.11+가 필요합니다. 참조는 표준 라이브러리만 사용합니다. 기존 데스크 패턴 계약 검사는 별도의 호환성 검사로 유지됩니다.
시간 제한 자동 승인 (선택 사항)
초안은 auto_send_after(에포크 초)를 가질 수 있습니다. 스위프(sweep)는 결정 없이 기한이 지난 초안을 승인하고, 운영자를 auto-ttl로 기록한 후 정상 경로를 통해 발송을 진행합니다. 운영자 작업이 항상 우선합니다: 결정은 스위프가 보기 전에 상태를 변경하고, 재제출은 에포크를 순환시키고 기한을 이동시키거나 지웁니다. 스위프는 쓰기 트랜잭션 내에서 상태와 에포크를 다시 확인하므로 경합 조건(race)은 no-op으로 해결됩니다. 기한이 없는 초안은 영원히 하드 게이트(hard-gated) 상태로 유지됩니다.
신호 연결
초안은 응답하는 인바운드 의무를 지정할 수 있습니다(signal_obligation_id). 이것은 지연 시간 측정(인바운드 신호에서 초안 작성 응답까지)을 위한 유일한 정확한 링크이며, SLA 스캔이 해당 인바운드 항목을 응답된 것으로 처리하도록 합니다. 참조된 행이 없으면 제출을 거부합니다.
예제
초안 제출
file_request(
draft="감사합니다. 금요일까지 슬롯을 잡아둘 수 있습니다. 어떤 시작일이 좋으신가요?",
counterparty="acme-supplier",
context="배송 기간 질문에 대한 답변",
origin_platform="slack", origin_channel="#acme-shared",
origin_thread="1712345678.000100", priority="P1",
signal_obligation_id=412)
-> {obligation_id: 431, draft_sha256: "9f2c...", refiled: false}
구성되고 확인된 내부 운영 대상에는 다음이 표시됩니다:
Draft filed for approval (P1, sha 9f2c8a1b). Waiting on operator.
상대방이 볼 수 있는 발신 채널에는 제출 알림이 전송되지 않습니다. 확인된 내부 대상이 없으면 영수증은 내부 도구 결과 또는 운영자 화면에 남으며, 외부 대체(fallback)는 없습니다.
수정 후 재제출
운영자가 더 짧은 초안을 요청합니다. 동일한 (counterparty, channel)에 대해 다시 제출하면 refiled: true가 반환되고, 사이드카 텍스트와 해시가 변경되며 updated_at이 업데이트됩니다. 이전 패널 행에서 클릭한 승인은 이전 에포크를 가지므로 무시됩니다.
게이트 거부
DeskApprovalError: baseline gate refused this draft: the ledger shows a
signed contract for 'acme-supplier'. Evidence: master agreement executed
2026-08-14. Recommendation: do not ask. Re-file with
force_despite_signed_contract=true if this is genuinely a new thread.
내부 채널의 발송 바닥글
금요일 2026-09-08로 확인되었습니다.
approved by operator-a · receipt 118 · draft sha256 9f2c8a1b2d3e4f50
테스트할 불변 조건
- 제출 영수증은 구성되고 확인된 내부 운영 대상으로만 전송됩니다. 발신지가 확인된 내부 대상인 경우에만 영수증을 받습니다.
- 알 수 없는 발신지는 조용히 유지됩니다. 내부 대상을 사용할 수 없으면 내부 도구 결과 또는 운영자 화면을 사용하며, 외부 대체는 없습니다.
- 동일한 (counterparty, channel)에 대해 두 번 제출하면 하나의 의무와 두 개의 에포크가 생성됩니다.
- 유효하지 않은 에포크의 결정은 발송 행을 생성하지 않습니다.
- 두 명의 동시 클레임자는 하나의 발송 권한을 얻습니다. 실패자는 전송을 시도하지 않습니다.
- 알 수 없는 결과 및 영수증 지속화 실패는 자동 재시도를 활성화하지 않습니다.
- 성공적인 완료는 하나의 트랜잭션에서 영수증과 발송 상태를 기록합니다.
- 변경된 에포크, 텍스트, 해시 또는 대상은 클레임을 획득하거나 시작할 수 없습니다.
- 활성 클레임은 재제출을 차단합니다. 발송 전 취소만 해당 잠금을 해제할 수 있습니다.
- 게이트를 사용할 수 없으면 마커가 표시됩니다. 게이트 서명은 force 없이 거부합니다.
- auto-ttl은 운영자가 이후에 재제출한 텍스트에 대해 실행되지 않습니다.