nagi-ledger
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@nagi-ledger show me the latest session report"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
nagi-ledger
日本語 | English
自律型 AI コーディングエージェントのための監査台帳とガードレール。 MCP サーバー + Claude Code hooks。MCP SDK を除いて標準ライブラリのみで動きます。
AI エージェントに単独で作業をさせると、2 つの問いに答えられなくなります。
実際に何をしたのか。 どのサブエージェントを派遣し、何回リトライし、検証は何と結論したのか。
同じ失敗をどう止めるのか。 月曜に判明した行き止まりが、金曜にまた試される。何も覚えていないからです。
nagi-ledger はこの両方に答えます。そして重要なのは、エージェントの自制心に頼らず、機構として答える点です。派遣はエージェント自身が制御できない hook が記録します。既知の行き止まりは次の試行の前に照会され、該当すれば試行そのものが阻止されます。
記憶するよう指示された規則は、単なる提案にすぎない。 ハーネスが強制する規則だけが、制約になる。
何をするものか
flowchart LR
subgraph session["コーディングセッション"]
direction TB
S["セッション開始"] --> D["サブエージェントを<br/>派遣しようとする"]
D --> R["サブエージェントが動く"]
R --> F["ツールが失敗する"]
F --> E["ターンを終えようとする"]
end
S -. "未解決の作業を注入" .-> B["session_brief"]
D -. "履歴を照会" .-> G["dispatch_guard<br/>繰り返しを阻止"]
R -. "自動記録" .-> H["hook_ingest"]
F -. "自動記録" .-> H
E -. "まだ終わってない?" .-> Q["goal_gate<br/>停止を阻止"]
B --> L[("SQLite 台帳")]
G --> L
H --> L
Q --> L構成要素は 6 つ。それぞれエージェントのライフサイクルの別々の地点に接続されます。
構成要素 | フック | 役割 |
|
| 未解決の作業をセッション開始時に注入します。進行中の目標、検証待ちの派遣、直近の行き止まり、未コミットの変更が残るリポジトリ。すべて片付いているときは何も出力しません。つまり、綺麗な状態のセッションではコンテキストを 1 バイトも消費しません。 |
|
| サブエージェントを派遣する直前に、そのタスクのリトライ回数と記録済みの行き止まりを照会します。リトライ上限を超えている場合、または既知の行き止まりに該当する場合は、理由を添えて阻止します (exit 2)。それ以外のときは沈黙します。 |
|
| すべてのサブエージェント派遣とツール失敗を台帳に記録します。非同期で走り、エージェントに拒否権はありません。 |
|
| 目標が設定されている間、エージェントがターンを終えることを阻止します ( |
| MCP (stdio) | 台帳を 10 個の MCP ツールとして公開します。エージェントが意図的に台帳を読み書きするための口です。検証結果の記録、行き止まりの登録、セッションレポートの生成など。 |
| (フックではない、手動 CLI) | 台帳全体を 1 つの JSON に読み取り専用でエクスポートします。ledger-view — 素の TS SPA — がこの JSON を読んで台帳を可視化します。 |
台帳の本体 (ledger.py) は MCP のコードもフックのコードも一切含まない依存ゼロのモジュールです。そのため、すべての関数を直接ユニットテストできます。
Related MCP server: agent-activity
出力言語について
session_brief.py が SessionStart/PostCompact で注入するブリーフィング — ## 開いてるループ、### アクティブ goal、### 検証待ち、### 直近の dead-end、### 未 commit といった見出し — は日本語で固定されており、セッション開始のたびにエージェントのコンテキストへそのまま入ります。ledger_session_report という MCP ツール (ledger.py の session_report()) も同様で、Markdown の見出しは ## 自律実行リスト です。
これはドキュメント不足というより、このプロジェクトの前提そのものです。開発者本人が日常的に日本語で使っているツールであり、翻訳はされていません。今のところ英語に切り替える手段はありません (NAGI_LANG のような環境変数は存在しません)。
それ以外は英語です。goal_gate.py 自身の出力 (stop-gate の阻止理由、CLI のメッセージ、status) は英語のテンプレートで、日本語が混じり得るのは set で登録した goal テキスト自身がそのままエコーされる場合だけです — それはスクリプトの言語ではなく、あなたの入力です。dispatch_guard.py の阻止理由も英語の枠組みに、記録した dead-end の理由 (あなた自身が書いた言語) が差し込まれる形です。hook_ingest.py は標準出力に何も書かず、デバッグ用の行だけを stderr に書きます。注入される日本語が困る場合は、session_brief.py を接続しない、ledger_session_report を呼ばないようにしてください。それ以外の要素にはこの問題はありません。
なぜ「指示」ではなく「フック」なのか
このプロジェクトが立脚している設計原則です。
エージェントが覚えていなければならない規則は、提案にすぎない。 ハーネスが強制する規則だけが、制約になる。
プロンプトに 「同じ失敗する手法を 2 回を超えて繰り返さないこと」 と書くことはできます。それはコンテキストが長くなるまで、あるいはモデルが自信を持つまで、あるいは要約がその一行を落とすまでは有効です。dispatch_guard は同じ規則を exit 2 にします。
この方針から、実装上の判断が 2 つ導かれました。
ガードは必ず「開く方向」に倒れる (fail open)
すべてのフックは、内部エラーが起きたら必ず exit 0 で終了します。全部の派遣を阻止する壊れたガードは、ガードが無いより悪いからです。クラッシュも、データベースの欠損も、ファイルロックも、すべて「通す。ただし stderr に文句を書く」に解決されます。
ただし fail open が原理的に不可能な箇所が 1 つあります。SessionStart フックが標準入力で待ちに入った場合、例外ハンドラでは救えません。 例外が発生しないからです。ただ固まり、セッションが永遠に始まらないだけです。この経路は「標準入力を一切読まない」ことで塞いであり、開いたまま閉じられていないパイプに対してスクリプトを起動する回帰テストで守っています。
dispatch_guard の阻止は JSON ではなく終了コードで伝える
初期の実装では permissionDecision: "ask" という JSON を返していました。しかし Claude Code の auto 権限モードでは、この判断は黙って握りつぶされます。 ガードは正しく判断したのに、派遣はそのまま実行され、理由は誰にも届きませんでした。
終了コード 2 はすべての権限モードで尊重されます。届かないガードは、ガードではありません。
これは PreToolUse と permissionDecision: "ask" に固有の話であり、ここにあるすべてのフックに対する一般則ではありません。goal_gate.py の Stop フックは自分のイベント種別に合った別の (正しい) 仕組みを使っています。{"decision": "block", "reason": "..."} を標準出力に出して exit 0 で終了する、これが Stop フック自体が定める契約どおりの動きです (上の構成要素の表を参照)。stop-gate を手で実行してこの JSON と exit 0 を見ても、それは上記の原則と矛盾しているわけではなく、イベント種別が違えばプロトコルも違う、というだけです。
使い方
Python 3.10 以上が必要です。
git clone https://github.com/namakoo-dev/nagi-ledger.git
cd nagi-ledger
python -m venv .venv
.venv/bin/pip install -r requirements-dev.txt # Windows: .venv\Scripts\pip
.venv/bin/pytest -qrequirements.txt には実行時の唯一の依存 (MCP SDK、server.py のみが必要とします) が入っています。requirements-dev.txt はそれに pytest を加えたものです。
MCP サーバーを登録する
claude mcp add nagi-ledger -s user -- /abs/path/.venv/bin/python /abs/path/server.pyMCP サーバーには mcp パッケージが必要なので、仮想環境側のインタプリタを指定してください。一方フックのスクリプトは意図的に標準ライブラリのみで書かれているため、どの Python でも動きます。
フックを接続する
~/.claude/settings.json に以下を追加します。PY をインタプリタのパス、DIR をチェックアウト先のパスに置き換えてください。
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "PY DIR/session_brief.py", "timeout": 15 }] }
],
"PostCompact": [
{ "hooks": [{ "type": "command", "command": "PY DIR/session_brief.py", "timeout": 15 }] }
],
"PreToolUse": [
{ "matcher": "Agent|Task",
"hooks": [{ "type": "command", "command": "PY DIR/dispatch_guard.py", "timeout": 15 }] }
],
"PostToolUse": [
{ "matcher": "Agent|Task",
"hooks": [{ "type": "command", "command": "PY DIR/hook_ingest.py agent-dispatch", "timeout": 30, "async": true }] }
],
"PostToolUseFailure": [
{ "hooks": [{ "type": "command", "command": "PY DIR/hook_ingest.py tool-failure", "timeout": 30, "async": true }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "PY DIR/goal_gate.py stop-gate", "timeout": 15 }] }
]
}
}SessionStart と PostCompact に同じスクリプトを渡しています。圧縮はセッション開始と同じ状況、つまり未解決の作業が文脈から失われた状態を作るため、必要な処置も同じです。
各要素は独立しています。必要なものだけ接続して構いません。
フックを手で試す
各フックスクリプトは標準入力から JSON オブジェクトを 1 つ読みます。Claude Code が PostToolUse/PreToolUse で流し込むのと同じ形です。つまり、実際の派遣を待たなくても、手で流し込んで行が入るところを見られます。以下の例では tool_name: "Agent" を使っています。これは上の dispatch_guard の PreToolUse マッチャーと hook_ingest.py/dispatch_guard.py 側のチェックに合わせたものです。お使いの環境でサブエージェント派遣ツールの名前が違う場合は、フックを接続する のマッチャーとこのチェックの両方を、実際に Claude Code が送ってくる名前に合わせてください — 信用する前に、実際のフックイベント JSON を確認してください。
1. hook_ingest.py agent-dispatch — 派遣を記録する
echo '{
"tool_name": "Agent",
"tool_input": {
"subagent_type": "general-purpose",
"model": "sonnet",
"description": "fix the flaky widget test",
"prompt": "Investigate and fix the flaky test in test_widget.py."
}
}' | PY DIR/hook_ingest.py agent-dispatchexit 0 で終わり、標準出力には何も出ません (stderr にはデバッグ用に dispatch_id=1 が出ます)。行が入ったことを確認するには:
sqlite3 ~/.nagi/ledger.db "select id, task, agent_type, model from dispatches order by id desc limit 1;"tool_input が無い、tool_name が "Agent" 以外、tool_input が JSON オブジェクトでない、といった形は「記録すべきものが無い」として黙って何もしません (それでも exit 0、行数 0) — これはバグではなく意図的な設計です。上の「ガードは必ず『開く方向』に倒れる」を参照してください。
2. hook_ingest.py tool-failure — 失敗したツール呼び出しを記録する
echo '{
"tool_name": "Bash",
"tool_input": {"command": "pytest -q"},
"tool_response": {"error": "1 failed, 2 passed"}
}' | PY DIR/hook_ingest.py tool-failureactions に 1 行挿入されます (tier=0、category=tool_failure、description="Bash: 1 failed, 2 passed")。
3. dispatch_guard.py — 繰り返された派遣を阻止する
手順 1 と同じ agent-dispatch ペイロードを hook_ingest.py にあと 2 回 (同じ description で計 3 回) 流し込んでそのタスクのリトライ回数を上限に到達させ、続けて同じペイロードを PreToolUse イベントとして dispatch_guard.py に送ります。
PAYLOAD='{
"tool_name": "Agent",
"tool_input": {
"subagent_type": "general-purpose",
"model": "sonnet",
"description": "fix the flaky widget test",
"prompt": "Investigate and fix the flaky test in test_widget.py."
}
}'
echo "$PAYLOAD" | PY DIR/hook_ingest.py agent-dispatch # 2 回目の派遣
echo "$PAYLOAD" | PY DIR/hook_ingest.py agent-dispatch # 3 回目の派遣
echo "$PAYLOAD" | PY DIR/dispatch_guard.py # ここで阻止される
echo "exit=$?"BLOCKED by dispatch_guard.
Task: fix the flaky widget test
prior dispatches: 3, last verdict: PENDING
Budget rule: same-purpose retries are limited to 2.
Proceed only if you have new information that invalidates the above; otherwise change approach or stop.
exit=2目標ゲートを使う
python goal_gate.py set "全テストが緑で CHANGELOG が更新されていること" --max-turns 20
python goal_gate.py status
python goal_gate.py extend 10 # バックグラウンド処理の完了待ちで予算が足りないとき
python goal_gate.py done "214 テスト緑、CHANGELOG を a1b2c3d でコミット"done を宣言するまで (あるいは abort するか、ターン予算が尽きるまで)、エージェントはターンを終えられません。
台帳を JSON でエクスポートする
python export_json.py # 既定 DB を標準出力へ
python export_json.py --out ledger.json
python export_json.py --db /path/to/other/ledger.dbactions / dispatches / approaches の全行を、間引きなしで 1 つの JSON にまとめます。台帳を書き換えることは決してありません (読み取り専用で開きます)。対象の DB が存在しない場合は空の DB を作らず、stderr に理由を出して exit 1 します。
出力は ledger-view — 素の TS SPA — でそのまま可視化できます。
MCP ツール一覧
ツール | 用途 |
| 自律実行した操作を記録する。影響度を 0〜2 の 3 段階で区別。 |
| サブエージェントの派遣を記録し、そのタスクのそれまでのリトライ回数を返す。 |
| 派遣に |
| そのタスクのリトライ回数、直近の検証結果、リトライ上限超過フラグを返す。 |
| 試した手法を |
| そのタスクで既に何を試し、結果がどうだったかを返す。 |
| 直近の操作と派遣を Markdown レポートにする。 |
| 影響度・分類・検証結果ごとの集計。 |
| actions/dispatches/approaches を横断キーワード検索し、本文を含まない索引行だけを返す。 |
| 完全一致では拾えない、言い回しが違う重複タスクを類似度で検出する。 |
派遣の記録は自動です (フックがやります)。一方、検証結果の記録は意図的に手動のままにしてあります。「その仕事が本当に正しいか」を決めるのは判断であり、そこを自動化したらこの仕組みの意味が失われるからです。
データの保存先
~/.nagi/ledger.db に SQLite で保存します。テーブルは actions / dispatches / approaches の 3 つ。
WAL モードを使っています。非同期フックが同時に発火しうるためで、書き込みが 1 件失われることは監査証跡に穴が開くことを意味するからです。
すべてのパスは環境変数で上書きできます。テストスイートが実際の台帳に触れないのも、この仕組みを使っています。
環境変数 | 既定値 |
|
|
|
|
|
|
| 現在の git リポジトリ (あれば) |
テスト
.venv/bin/pytest -q # Windows: .venv\Scripts\pytest; 214 テスト
.venv/bin/python tests/smoke_stdio.py # Windows: .venv\Scripts\python; MCP サーバーを stdio で起動して実際に呼ぶsmoke_stdio.py は仮想環境のインタプリタで実行してください。素の python ではありません。
サーバーの操作に mcp パッケージを使うため、それが入っていないシステム Python では
ModuleNotFoundError: No module named 'mcp' で落ちます。
CI は Linux と Windows の両方で、Python 3.10 と 3.12 に対してこれらを実行します。
テストは正常系よりも異常系に厚く書いてあります。この種のツールでは、壊れ方こそが本題だからです。
破損状態での fail open — 読めないデータベース、ディレクトリがあるべき場所にファイルがある状態、標準入力に流し込まれたゴミ、オブジェクトではなく JSON 配列。すべてのケースで操作は通らなければなりません。
「書き込まないこと」の証明 — 読み取り専用の要素については、全テーブルの行数を前後で記録して一致を検査します。監査対象を書き換える監査ツールには価値がありません。
非 ASCII 文字のサブプロセス往復 — 記録される理由は英語でないことが多く、Windows のコンソールコードページはそれを表現できません。これは実際に起きたバグでした。
json.dumpsが非 ASCII をエスケープして問題を隠しており、stderr への出力を平文に変えた瞬間に露出しました。標準入力での固まり — 開いたまま閉じられていない標準入力パイプを与えてサブプロセスを起動し、速やかに終了することを検査します。
現状と適用範囲
日常的に使っている実働ツールであり、フレームワークではありません。意図的に小さく作ってあります。SQLite と標準ライブラリのみ、1 つの関心事につき 1 ファイル、プラグイン機構なし。
Claude Code のフックを対象にしていますが、MCP サーバー側は任意の MCP クライアントで動きます。
既知の制約を、影響の大きい順に挙げます。
目標ゲートに「待機」の概念がない。 「バックグラウンド処理の完了を待っている」と「早々に諦めた」を区別できないため、待機がターン予算を消費します。現状の回避策は
extendです。stop-gateの同時実行が直列化されていない。 状態ファイルはロックなしの read-modify-write です。単一セッションでの利用 (これが唯一のサポート対象です) では発生しません。注入されるテキストは日本語で固定、切り替え不可。
session_brief.pyのブリーフィングとledger_session_reportMCP ツールは日本語がハードコードされています。詳細は上の「出力言語について」を参照してください。今のところNAGI_LANGのような切り替えはありません。
ライセンス
MIT — LICENSE を参照してください。
Available Tools
8 toolsledger_check_approachesARead-onlyIdempotent
Call this BEFORE retrying or re-planning a task. Never re-attempt an approach listed in dead_ends or no_gos unless you have new information that invalidates its reason.
Args: task (str): The exact task string used with ledger_log_approach.
Returns: dict: { "task": str, "dead_ends": [{"approach": str, "reason": str, "ts": str}, ...], "no_gos": [{"approach": str, "reason": str, "ts": str}, ...], "works": [{"approach": str, "reason": str, "ts": str}, ...], "total": int } Each list is ordered newest first. Empty lists when nothing has been recorded for the task.
Inputs: task (required). Returns: object
ledger_log_actionA
Record a single autonomous action taken during a self-directed dev loop.
Use this to log any action the agent takes on its own initiative, tagged with a tier indicating how consequential it was.
Args: tier (int): Impact tier. Must be 0 (routine/info), 1 (notable), or 2 (high-impact, e.g. irreversible or user-facing). category (str): Short category label, e.g. "refactor", "deploy", "file_write", "dependency_change". Must be non-empty. description (str): Human-readable description of what was done. Must be non-empty. project (Optional[str]): Project/repo name this action belongs to.
Returns: dict: {"id": int} — the new action's row id.
Errors: Raises ValueError if tier is not in {0,1,2} or category/description are empty, with a message explaining the valid values.
Inputs: tier (required), category (required), description (required), project. Returns: object
ledger_log_approachA
Record the outcome of an approach tried (or considered) for a task.
Record every failed approach IMMEDIATELY after it fails, and every deliberate NO-GO decision, so future attempts (by you or another agent) skip them instead of re-discovering the same dead end.
Args: task (str): Stable identifier/description of the task. Use the SAME string across approaches to the same underlying task so ledger_check_approaches can find them. approach (str): Short description of the specific approach tried or considered. Must be non-empty. outcome (str): One of "DEAD_END" (tried and failed), "NO_GO" (decided against without trying), or "WORKS" (confirmed working). reason (str): Why the approach failed, was rejected, or worked. Must be non-empty.
Returns: dict: {"id": int} — the new approach record's row id.
Errors: Raises ValueError if task/approach/reason are empty, or outcome is not one of DEAD_END/NO_GO/WORKS, with a message naming the valid values.
Inputs: task (required), reason (required), outcome (required), approach (required). Returns: object
ledger_log_dispatchA
Record a subagent dispatch (e.g. via the Agent tool) for audit and retry tracking.
Call this every time a subagent is dispatched for a task, so retries of the same task can be counted and budget-limited via ledger_task_status.
Args: task (str): Stable identifier/description of the task being dispatched. Use the SAME string across retries of the same underlying task so retry counting works. agent_type (str): Subagent type used (e.g. "fork", "general-purpose"). model (str): Model used for the dispatch (e.g. "sonnet", "opus"). brief_summary (str): One-line summary of what the dispatch was asked to do.
Returns: dict: {"id": int, "retry_count": int} where retry_count is the number of PRIOR dispatches recorded under the same task (0 for the first).
Errors: Raises ValueError if any field is empty.
Inputs: task (required), model (required), agent_type (required), brief_summary (required). Returns: object
ledger_log_verdictAIdempotent
Attach a verification verdict to a previously logged dispatch.
Use this after independently verifying a subagent's work, to record whether its claims were confirmed, refuted, or partially true.
Args: dispatch_id (int): The id returned by ledger_log_dispatch for the dispatch being verified. verdict (str): One of "CONFIRMED", "REFUTED", "PARTIAL". notes (Optional[str]): Optional free-text notes on the verification.
Returns: dict: {"ok": true, "task": str} — the task string of the dispatch the verdict was attached to.
Errors: Raises ValueError with an actionable message if dispatch_id does not exist, or if verdict is not one of the three allowed values.
Inputs: verdict (required), dispatch_id (required), notes. Returns: object
ledger_session_reportARead-onlyIdempotent
Generate a markdown report of actions and dispatches from the last N hours.
Actions are grouped by category; dispatches are listed with their resolved verdict or PENDING if not yet verified. Useful for a human-readable audit summary of recent autonomous activity.
Args: since_hours (int): How many hours back to include. Defaults to 24.
Returns: str: Markdown report, or a short "no entries" message if nothing was logged in the window.
Inputs: since_hours. Returns: result
ledger_statsARead-onlyIdempotent
Aggregate statistics over actions and dispatches from the last N days.
Args: days (int): How many days back to include. Defaults to 7.
Returns: dict: { "actions_by_tier": {"0": int, "1": int, "2": int}, "actions_by_category": {category: int, ...}, "dispatch_count": int, "verdicts": {"CONFIRMED": int, "REFUTED": int, "PARTIAL": int, "PENDING": int} }
Inputs: days. Returns: object
ledger_task_statusARead-onlyIdempotent
Look up dispatch/retry/verdict status for a given task string.
Use this BEFORE dispatching a subagent to check whether the same task has already been retried too many times (budget enforcement).
Args: task (str): The exact task string used with ledger_log_dispatch.
Returns: dict: { "dispatch_count": int, # total dispatches recorded for this task "retry_count": int, # dispatch_count - 1, floored at 0 "last_verdict": str | None, # verdict of most recent dispatch, or null "over_retry_limit": bool # true when retry_count >= 2 }
Inputs: task (required). Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 8 tools
Each tool targets a distinct domain object: general actions, subagent dispatches, verdicts, task status, and approaches. No two tools perform the same function, and the descriptions make the boundaries clear, even where overlap might occur (e.g., log_action vs log_approach).
The consistent 'ledger_' prefix is good, but the suffix pattern is mixed: some tools use verb_noun (log_action, log_dispatch, log_verdict, log_approach, check_approaches) while others use noun_noun (task_status, session_report) or a bare noun (stats). This inconsistency makes the set slightly less predictable.
Eight tools is well within the ideal range for a specialized audit ledger. The set includes both write and query operations without unnecessary redundancy or bloat, and each tool earns its place.
The surface provides comprehensive coverage for the domain: writing actions, dispatches, verdicts, and approaches, plus reading and aggregating that data. There are no obvious gaps; the append-only nature of the ledger is appropriate and fully supported.
Maintenance
Related MCP Connectors
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Guarded MCP server for agent-readable business truth, provenance, readiness, and discovery.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Host any HTTP API as MCP tools with a tamper-evident cross-agent settlement ledger.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceTamper-evident audit logging for AI agents. Append-only, hash-chained, optionally Ed25519-signed log. The MCP server lets an agent keep and verify a record of what it actually did.8MIT
- AlicenseAqualityBmaintenanceA read-only MCP server that exposes local coding-agent session logs as three tools for introspection of recent work, debugging tool failures, and tracking token usage and estimated cost without parsing log files.3MIT
- AlicenseNot gradedqualityBmaintenanceThis MCP server enables AI agents to manage artifacts across sessions by providing tools for searching, retrieving, and registering entries in a persistent ledger, ensuring consistency and traceability of agent outputs.MIT
- AlicenseNot gradedqualityCmaintenanceAn MCP server providing an append-only, hash-chained evidence ledger for agent actions, where every record is a tamper-evident receipt cryptographically bound to all prior records and persisted as human-readable JSONL local state. It exposes tools to append records, verify chain integrity (pinpointing tampering), query records by actor/action/target/time, and fetch ledger stats—with no update or delete capabilities by design.1Apache 2.0