Interface Contract — Dashboard 관리자 인증
목적과 범위
Dashboard 인증 설정, login/cleanup/logout HTTP route, signed cookie와 Streamlit gate의 외부·내부 계약을 정의한다.
- 포함 범위: 환경변수, HTTPS form POST, cookie 발급·삭제, fail-closed gate
- 제외 범위: MCP 인증, OAuth/OIDC, 다중 사용자, DB session/token registry, per-token revocation
- 사용 주체: browser, Dashboard ASGI server, Streamlit app, 운영 배포
계약
auth-config — 서버 인증 설정
- 책임: 현재 configured credential과 signing secret을 server-only로 제공한다.
- 종류·프로토콜: process environment
- 호출 대상·operation:
load_auth_settings()
- 버전: 2.0
- 호출 주체: Dashboard server
- 인증 방식: 없음
- 필요 권한: process secret read
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
JARCA_DASHBOARD_ADMIN_ID |
environment |
string |
필수 |
없음 |
non-empty |
configured admin identifier |
민감 |
JARCA_DASHBOARD_ADMIN_PASSWORD |
environment |
string |
필수 |
없음 |
non-empty |
configured credential |
민감 |
JARCA_DASHBOARD_SIGNING_SECRET |
environment |
string |
필수 |
없음 |
UTF-8 32 byte 이상 |
signing input |
민감 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
AuthSettings |
server memory |
object |
성공 시 필수 |
current configured values |
verifier input; never serialized |
민감 |
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
configuration_unavailable |
configuration |
missing/empty/read error/short secret |
수정 후 가능 |
generic unavailable |
no cookie, no DB |
login — POST /api/auth/login
- 책임: same-origin HTTPS credential을 검증하고 signed cookie를 발급한다.
- 종류·프로토콜: HTTPS form POST
- 호출 대상·operation:
/api/auth/login
- 버전: 1.0
- 호출 주체: browser login form
- 인증 방식: submitted credential, server-only comparison
- 필요 권한: same-origin HTTPS
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
admin_id |
form body |
string |
필수 |
없음 |
bounded form value |
submitted ID |
민감 |
admin_password |
form body |
string |
필수 |
없음 |
bounded form value |
submitted password |
민감 |
remember_me |
form body |
string |
선택 |
off |
on or absent |
choose 30-day TTL |
비민감 |
Origin |
request header |
URL |
필수 |
없음 |
request Host와 scheme·hostname·effective port가 같은 HTTPS origin |
CSRF origin 검증 |
비민감 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
Set-Cookie |
response header |
cookie |
success |
hardened auth cookie, redacted value |
persistent auth |
값은 민감 |
Location |
response header |
string |
success/failure |
/ or generic error path |
browser navigation |
비민감 |
발급 token은 v, sub=dashboard-admin, iat, exp, jti claim만 가진다. sub=dashboard-admin은 관리자 ID가 아닌 고정 비민감 role이고, token 값과 서명은 민감하므로 browser의 hardened cookie에만 저장한다. 성공한 login 시각에 iat와 exp를 한 번 고정하며 기본 TTL은 24시간, remember_me=on이면 30일이다. 활동, 일반 요청, rerun 또는 인증 복원은 token을 재발급하거나 두 시각을 연장하지 않는다.
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
400 |
request |
malformed/oversized body |
수정 후 가능 |
resubmit valid form |
no cookie |
403 |
origin |
non-HTTPS or cross-origin |
수정 후 가능 |
use same-origin HTTPS |
no cookie |
invalid |
authentication |
ID/password mismatch |
가능 |
generic error and delay |
no cookie |
unavailable |
configuration |
invalid server configuration |
config fix 후 |
generic unavailable |
no cookie |
cleanup — GET /api/auth/cleanup
- 책임: invalid cookie를 matching scope로 삭제한 뒤 unauthenticated navigation을 완료한다.
- 종류·프로토콜: HTTPS navigation GET
- 호출 대상·operation:
/api/auth/cleanup
- 버전: 1.0
- 호출 주체: same-origin browser/auth gate
- 인증 방식: same-origin HTTPS
Referer; token validity 불필요
- 필요 권한: same-origin HTTPS
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
Referer |
request header |
URL |
필수 |
없음 |
request origin과 같은 HTTPS authority |
navigation origin 검증 |
비민감 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
Set-Cookie |
response header |
cookie |
always |
same name/path/security, Max-Age=0 |
delete stale cookie |
값은 민감 |
Location |
response header |
string |
success |
/ |
unauthenticated page |
비민감 |
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
403 |
origin |
non-HTTPS/cross-origin |
가능 |
retry same-origin |
no DB |
logout — POST /api/auth/logout
- 책임: explicit logout으로 browser cookie를 삭제한다.
- 종류·프로토콜: HTTPS form POST
- 호출 대상·operation:
/api/auth/logout
- 버전: 1.0
- 호출 주체: authenticated browser
- 인증 방식: same-origin HTTPS; server-side revocation 없음
- 필요 권한: same-origin HTTPS
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
none |
none |
none |
선택 |
없음 |
no credential accepted |
explicit logout |
없음 |
Origin |
request header |
URL |
필수 |
없음 |
request Host와 scheme·hostname·effective port가 같은 HTTPS origin |
CSRF origin 검증 |
비민감 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
Set-Cookie |
response header |
cookie |
always |
matching deletion, Max-Age=0 |
remove browser cookie |
값은 민감 |
Location |
response header |
string |
success |
/ |
unauthenticated page |
비민감 |
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
403 |
origin |
non-HTTPS/cross-origin |
가능 |
retry same-origin |
no DB |
gate — Dashboard 인증 경계
- 책임: config and cookie verification before any protected work.
- 종류·프로토콜: Python/Streamlit call
- 호출 대상·operation:
require_dashboard_authentication()
- 버전: 2.0
- 호출 주체:
dashboard.app.main()
- 인증 방식: current config plus signed cookie
- 필요 권한: valid token with
sub=dashboard-admin
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
auth_cookie |
request cookie |
string |
success path |
absent |
current configuration으로 서명을 검증할 수 있고 claim이 정확히 v, sub=dashboard-admin, iat, exp, jti인 token |
browser proof |
값과 서명은 민감; sub는 비민감 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
authenticated |
server result |
bool |
always |
true/false |
gate decision |
비민감 |
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
unauthenticated |
authentication |
absent/invalid/expired/config failure |
login 가능 |
cleanup then show login |
no protected work |
관리자 ID, password 또는 signing secret 변경은 server-only signing key binding을 바꾸므로 기존 token을 모두 무효화하고 cleanup으로 전이한다. DB session, token registry와 per-token revocation은 만들지 않는다.
호환성
- 유지할 기존 호출 방식: authenticated
page query parameter, page dispatch, gateway와 MCP behavior
- 버전 변경 정책: REQ-001 revision 2.0; session-only boolean을 signed cookie로 대체
- 폐기 정책: client-side token storage, DB-backed session, per-token revocation 없음
- 재시도 계약: credential 실패는 최소 1초 지연; origin/request 오류는 수정 후 재시도
- 중복 처리 계약: login은 새 token 발급 가능; cleanup/logout deletion은 idempotent
- 순서 보장: config → cookie verify → cleanup if invalid → DB/page dispatch only if valid
- 만료 보장: login 성공 시 고정한
iat와 exp는 활동·일반 요청·rerun·복원에서 변경하거나 연장하지 않음
예시
example-login — 정상 login
호출
결과
example-cleanup — invalid cookie cleanup
호출
결과
검증
- 정상 계약: authentication/server tests와 loopback HTTPS evidence
- 실패 계약: wrong credential, invalid configuration, cross-origin, malformed cookie tests
- 경계 계약: expiry, rotation, oversized token, same-host different-port origin tests
- 구현·스키마·예시 정합성:
tests/test_dashboard_auth_server.py, tests/test_dashboard_authentication_app.py, tests/test_dashboard_persistent_authentication.py, source/document redaction search
Dashboard 메뉴 전환 계약 — REQ-002
- 책임: authenticated Dashboard에서 공통 shell/background를 유지하고 선택된 page body만 교체한다.
- 종류·프로토콜: Streamlit rerun,
st.radio callback, query parameter
- 호출 대상·operation:
dashboard.app.main() → render_sidebar_nav() → render_dashboard_body()
- 버전: 1.0
- 호출 주체: authenticated browser
- 인증 방식: 기존
gate 계약 선행; 메뉴는 인증을 우회하지 않음
- 필요 권한: valid Dashboard auth cookie
상태와 입력
정확한 menu options/order는 월정산, 요약, 자산/마감, 거래내역, 확인필요 큐, 미분류, 분류 규칙, 카테고리, 계좌, Import이다. selected value는 st.session_state["dashboard_page"]에 둔다. 직접 query의 page가 known option이면 해당 page를, absent/unknown이면 요약을 사용한다.
출력·불변조건
- callback은
st.query_params["page"]만 갱신하고 month, account_id, filter 등 sibling key를 보존한다.
- menu click은 document/path navigation이 아니며 stable
st.empty body container 안에서 target renderer를 실행한다.
- 공통 sidebar, refresh control, CSS/background는 유지하고 body만 target content로 대체한다.
- 느린 target에는
<target> 불러오는 중…을 표시한 뒤 renderer 완료 시 placeholder를 제거한다.
- renderer 예외는 body 안의
페이지를 불러오지 못했습니다. 다시 시도해 주세요.와 retry control로 대체하며 예외 상세를 노출하지 않는다.
- serialized callback이 여러 번 실행되면 마지막 selection이 query와 body의 최종 상태가 된다.
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
page_fallback |
request |
missing/unknown page |
가능 |
요약 표시 |
sibling query 유지 |
renderer_unavailable |
rendering |
target renderer exception |
가능 |
generic error의 retry 클릭 |
previous body 제거, auth/shell 유지 |
검증 경계
AppTest는 callback, query mapping, synthetic renderer output과 error/loading event를 검증하지만 실제 browser의 document navigation 부재, paint timing, focus/scroll, network request 또는 perceived shell continuity를 증명하지 않는다. 따라서 REQ-002.03-AC01은 authenticated real-browser evidence가 필수이며 AppTest는 companion evidence일 뿐이다. history/back-forward와 path URL은 계약에서 제외한다.
상세 acceptance target은 docs/requirements/req-002_dashboard-menu-transition.md의 REQ-002.01-AC01부터 REQ-002.09-AC01까지를 따른다.
계좌 평가·자산 마감 계약 — REQ-003
valuation-service
- 책임:
register_account_valuation(), list_account_valuations(), select_account_valuation(), correct_account_valuation()을 통해 exact/as_of verified leaf와 append-only history를 제공한다.
- 종류·프로토콜: Python service function;
jarca_ledger_mcp.service re-export
- 호출 대상·operation:
jarca_ledger_mcp.application.account_valuation_service
- 버전: additive v1
- 호출 주체: Dashboard accounts gateway, MCP tool, import/account write service
- 인증 방식: service layer 자체 MCP auth는 주장하지 않음; Dashboard는 기존
gate 선행
- 필요 권한: caller가 DB path와 approval context를 제공
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
account_id/basis_date/amount |
function/MCP |
string/string/int |
register 필수 |
없음 |
account id, YYYY-MM-DD, exact integer |
valuation identity/value |
아니오 |
currency/valuation_kind/source_type/verification_status |
function/MCP |
string |
선택 |
KRW/balance/manual/unverified |
canonical enums listed below |
아니오 |
|
idempotency_key |
function/MCP |
string |
선택 |
'' |
stable caller key |
retry identity |
아니오 |
as_of_at/timezone, optional metrics, loan definition |
function/MCP |
nullable string/int/float |
선택 |
NULL |
source validation; loan definition required |
evidence metrics |
아니오 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
success |
envelope |
bool |
항상 |
true/false |
outcome |
아니오 |
valuation/valuations |
result |
object/list |
성공 시 |
persisted rows |
history/evidence |
source_ref may be sensitive |
reused, carried_forward, warning, unverified_candidates |
result |
bool/string/list |
조건부 |
additive |
idempotency/selection evidence |
아니오 |
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
validation_error |
request |
invalid date/type/currency/loan definition |
수정 후 가능 |
payload 수정 |
no write; no row or cache mutation |
not_found |
lookup |
account/valuation missing |
수정 후 가능 |
id 확인 |
없음 |
idempotency_conflict |
conflict |
same key different payload |
아니오 |
new key or inspect |
없음 |
correction_conflict/ambiguous_valuation |
conflict |
second child/multiple leaves |
수동 검토 |
correct evidence |
없음 |
snapshot-close
- 책임:
preview_asset_snapshot_close() is read-only; close_asset_snapshot() writes an immutable run and rows.
- 종류·프로토콜: Python service, Streamlit
assets_gateway, MCP tools
- 호출 대상·operation:
jarca_ledger_mcp.application.assets_service
- 버전: additive v1
- 호출 주체: authenticated Dashboard operator or MCP caller
- 인증 방식: Dashboard existing auth gate; MCP authentication is outside this contract
- 필요 권한: close conditional approval fields when warnings require it
입력
| 필드 |
위치 |
타입 |
필수 여부 |
기본값 |
허용 값·형식 |
의미 |
민감정보 |
basis_date/selection_policy |
function/MCP/form |
string/string |
basis optional |
today/exact |
YYYY-MM-DD, exact/as_of |
close scope |
아니오 |
idempotency_key/correction_reason |
function/MCP/form |
string |
conditional |
empty |
stable key; reason on re-close |
retry/correction |
아니오 |
allow_missing_exclusions/allow_as_of_carry_forward |
function/MCP |
bool |
optional |
false |
explicit flags |
conditional approval need |
아니오 |
approved_by/approval_reason/approval_reference |
function/MCP/form |
string |
conditional |
empty |
all required together |
approval evidence |
아니오 |
출력
| 필드 |
위치 |
타입 |
필수 여부 |
허용 값·형식 |
의미 |
민감정보 |
source/is_closed_snapshot/snapshot_run_id |
result |
string/bool/string |
success |
live_authority/snapshot |
preview vs close |
아니오 |
close_status/selection_policy/warnings |
result |
string/string/list |
success |
complete_exact/incomplete_draft/conditional |
authority state |
아니오 |
total_assets/total_liabilities/net_worth |
result |
integer |
success |
exact integer |
formula summary |
아니오 |
오류
| 코드 |
분류 |
발생 조건 |
재시도 가능 여부 |
호출자 대응 |
실패 시 상태 변경 |
conditional_approval_required |
policy |
unsafe exclusions/carry-forward without explicit approval |
수정 후 가능 |
preview, approve explicitly |
없음 |
idempotency_conflict |
conflict |
close key payload differs |
아니오 |
inspect run |
없음 |
validation_error |
request |
invalid policy or unreasoned correction |
수정 후 가능 |
provide reason |
no partial rows |
Canonical valuation values and validation
The public contract explicitly supports these exact values: source_type: manual, import, account_write, reconciliation, snapshot; verification_status: verified, unverified, rejected; valuation_kind: balance, cash_balance, total_value, loan, loan_balance; representative_liability_definition: principal_only, principal_interest_fees. These are the currently supported enums, not a delegation to source code.
loan accounts require representative_liability_definition for loan, loan_balance, and balance aliases. Those are the only accepted valuation kinds for loan accounts; cash_balance and total_value do not represent a loan liability and are rejected. legacy valuation-kind aliases remain accepted and map to the canonical purpose; alias acceptance does not broaden the canonical enum set. Invalid values, missing loan definitions, and incompatible definitions return validation_error and perform no write. A validation failure returns validation_error and performs no write.
loan valuations map to liabilities and non-loan valuations map to assets. The output mapping preserves the existing envelopes and signatures: success remains the outcome field, failures include error_code and error, and validation or conflict failures do not partially write.
Account current-cache writes keep last_balance_basis_date (optional YYYY-MM-DD) separate from last_synced_at (execution/sync timestamp). create_ledger_account keeps the established positional db_path argument and adds last_balance_basis_date after it as an optional keyword. update_ledger_account accepts the same field through its existing keyword-field boundary. A balance with no explicit basis advances only the current display cache, leaves the basis unknown, and creates no historical candidate. A dated older non-loan balance appends historical evidence but cannot roll back a newer current cache. loan cache writes without an explicit liability definition do not append a historical candidate; they preserve current-cache and import behavior without inferring a definition. Import and account writers use deterministic candidate keys.
shared contract and adapter limits
All new interfaces retain the existing success/error_code envelope. preview_asset_snapshot_close() and Dashboard render/report are read-only; only explicit register/update/close forms or MCP calls write. MCP tools are register_account_valuation, list_account_valuations, select_account_valuation, correct_account_valuation, preview_asset_snapshot_close, close_asset_snapshot, get_asset_snapshot_report, and get_net_worth_timeseries; actual decorators are in jarca_ledger_mcp/adapters/mcp/server.py. Dashboard uses accounts_gateway.py, assets_gateway.py, and page_views/accounts.py/assets.py.
호환성 추가 규칙 — REQ-003
- 유지할 기존 호출 방식: REQ-001 auth config/login/cleanup/logout/gate, REQ-002 exact 10 menu labels/order,
page query sibling preservation, stable shell/background and browser evidence; existing MCP stdio/streamable-http runtime.
- 버전 변경 정책: additive fields only; no rename/removal of existing response keys.
- 폐기 정책: 없음; guessed migration, fake transactions and MCP auth claim are excluded.
- 재시도 계약: same idempotency payload reuses; changed payload conflicts; DB conflict does not retry blindly.
- 중복 처리 계약: valuation/run idempotency keys and single correction indexes.
- 순서 보장: Dashboard auth → DB path → page/gateway; preview → explicit approval → atomic close; selection → report.
예시 추가
example-valuation — synthetic verified valuation
- 대상 인터페이스:
valuation-service
호출
결과
example-preview — read-only close preview
호출
결과
검증 추가
- 정상 계약:
tests/test_account_valuations.py, tests/test_asset_snapshots.py, tests/test_contract_regression.py
- 실패 계약: idempotency/correction/conditional approval tests and source validation
- 경계 계약: exact/as_of, NULL/0, future/unverified candidate, correction/re-close
- 구현·스키마·예시 정합성:
tests/test_requirement_docs.py, actual service/MCP/gateway symbols
canonical implementation signatures and fields
The application contracts are these actual signatures (defaults included):
register_account_valuation(account_id: str, basis_date: str, amount: int, currency: str = "KRW", valuation_kind: str = "balance", source_type: str = "manual", source_ref: str = "", verification_status: str = "unverified", memo: str = "", idempotency_key: str = "", as_of_at: str | None = None, timezone: str | None = None, cost_basis: int | None = None, net_contributed: int | None = None, reported_principal_kind: str | None = None, reported_principal_label: str | None = None, profit: int | None = None, return_rate: float | None = None, return_basis: str | None = None, principal: int | None = None, interest: int | None = None, fee: int | None = None, representative_liability_definition: str | None = None, db_path: str | None = None, supersedes_id: str | None = None, correction_reason: str = "") -> dict[str, Any]
list_account_valuations(account_id: str, include_rejected: bool = True, db_path: str | None = None) -> dict[str, Any]
select_account_valuation(account_id: str, basis_date: str, as_of: bool = False, currency: str | None = None, valuation_kind: str | None = None, db_path: str | None = None) -> dict[str, Any]
correct_account_valuation(valuation_id: str, amount: int, correction_reason: str, idempotency_key: str = "", db_path: str | None = None, **fields: Any) -> dict[str, Any]
preview_asset_snapshot_close(basis_date: str = "", db_path: str | None = None, selection_policy: str = "exact") -> dict[str, Any]
close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str = "", correction_reason: str = "", db_path: str | None = None, selection_policy: str = "exact", idempotency_key: str = "", allow_missing_exclusions: bool = False, allow_as_of_carry_forward: bool = False, approved_by: str = "", approval_reason: str = "", approval_reference: str = "", preview_fingerprint: str = "") -> dict[str, Any]
get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None, selection_policy: str = "exact") -> dict[str, Any]
Every output is a dict with success; failures also have error_code and error. Successful close responses expose top-level approval, correction, warnings, excluded_items, and carried_items, plus snapshot_run_id and close_status. Their nested provenance contains valuation_date, age_days, carried_forward, verification_status, evidence_kind, approval, correction, warnings, and redacted exclusions. last_balance is an external current comparison, not opening evidence; absent balance_after/opening evidence returns INCOMPLETE_CALCULATION. source_ref is account-scoped evidence identity even with an explicit key; key/source mismatch is a no-write conflict. Correction requires same parent account/date/currency/purpose, rejects cyclic/self parent, permits one child, and requires reason.
canonical read and provenance rules
monthly_source_snapshot_id is a read-model alias of snapshot_run_id, not a persistence field. The alias and actual valuation date/age/carry/verification/run/approval/correction/warnings/exclusions are exposed through each response's documented top-level fields, included_items, and nested provenance in close, live, closed, timeseries, and monthly read models. Reconciliation states investment unavailable, loan purpose/definition/ambiguity, and completeness without fake writes.
C13 database boundary
Writer/bootstrap explicitly calls init_db(). read_db() uses SQLite mode=ro; missing database returns db_not_initialized, while initialized database read failures return db_error. Both failures retain the standard success=false, error_code, error envelope without raw SQLite details. Read paths perform no mkdir, no migrate, and no commit. Only first-use write initializes, and Dashboard auth is evaluated before DB access.
D04 snapshot close output contract
Every successful snapshot close output has these exact top-level fields (the transport envelope may additionally include success): snapshot_run_id, close_status, approval, correction, warnings, excluded_items, carried_items, provenance, valuation_date, age_days, verification_status, evidence_kind, and monthly_source_snapshot_id. monthly_source_snapshot_id is an alias of snapshot_run_id, not a persistence field.
The top-level provenance object has these exact nested provenance fields: valuation_date, age_days, carried_forward, verification_status, evidence_kind, approval, correction, warnings, excluded_items, and carried_items. approval and correction are objects or explicit empty values when not applicable; warnings and item collections are arrays. valuation_date is the actual selected evidence date, age_days is computed from the requested basis date, and verification/evidence status describe selected evidence. Live, closed, timeseries, and monthly read models preserve these fields and alias semantics.
The preview input includes basis_date, selection_policy, included/excluded evidence state hashes, warnings, and candidate_state_hashes. preview output includes preview_fingerprint. Close accepts the exact preview_fingerprint: str = "" parameter shown in the signature block and recomputes the same authority fingerprint before opening the write boundary. A mismatch returns authority_drift and no partial write.
Replay uses replay payload equality: an existing idempotency key replays only when the persisted request fingerprint is equal; a changed payload returns idempotency_conflict. The replay response is reconstructed from the persisted run and its evidence, with no replay-only reused fields.
close_metadata is a recursive close metadata allowlist: only run, approval, correction, warnings, exclusions, carried evidence, and allowlisted evidence fields (source_type, source_id, authority_source, valuation_id, dates/statuses and state_hash) cross the boundary. applied_evidence, excluded_evidence, and carried_evidence are redacted recursively; unknown nested keys are dropped, and credentials, tokens, cookies, raw account data, and arbitrary source-note payloads are never returned. This is the recursive close_metadata allowlist and redaction contract.