データソースとネイティブ・データベースコネクタ
遮蔽ルールが隠せるのは、見つけられた項目だけです。自分の MCP サーバーでも、顧客の MCP サーバーでも、DuDuClaw が自ら接続するデータベースでも——見つけ方はここに書いてあります。
2026-09 まで、db_field ルール(fields = ["res.partner.name", "hr.employee.*"])が効くのは Odoo だけでした。model.field という書き方は json_path ルールの糖衣構文にすぎず、その展開先——どのツールがどのテーブルのレコードを返すか、JSON のどこに入っているか、項目名が mapper を経ても残るか——は ODOO_TOOLS というハードコードされた一枚の表でした。db_field を Odoo 以外に向ける手段はありませんでした。
その下にはもう一段深い穴がありました。遮蔽が横取りするのは DuDuClaw 自身の MCP サーバー、つまり duduclaw mcp-server 内の単一の絞り込み点だけです。顧客が自分で用意した MCP サーバー(Postgres MCP、MySQL MCP、鼎新 ERP ブリッジなど、エージェントの .mcp.json に書かれたもの)は Claude CLI が直接起動するので、そのツール結果はそもそもパイプラインに一度も届いていませんでした。Odoo の表を差し替え可能にするだけでは、これらのサーバーには一つも効きません——データが遮蔽まで到達していなかったのです。
この機能は両方を埋めます。db_field ルールがどこでも参照できるレジストリ、そしてデータソースの行が実際に遮蔽を通るようにする二つの経路——すでにある MCP サーバー向けのプロキシと、MCP サーバーを持たないデータベース向けのネイティブコネクタです。
データソースの二種類
Section titled “データソースの二種類”| 種類 | 表すもの | 行の出どころ | 設定 |
|---|---|---|---|
| ツール経由のレジストリ項目 | どの MCP ツールがどのテーブルのレコードを返すか、返る JSON のどこにレコードがあるか | どの MCP サーバーでも——DuDuClaw 自身のものでも、MCP プロキシ経由の外部サーバーでも | [redaction.data_sources.<name>] |
| ネイティブなデータベース接続 | DuDuClaw が自ら開く PostgreSQL / MySQL / SQLite の生きた接続 | DuDuClaw 自身の db_select / db_query MCP ツール——外部サーバー不要 |
[db_sources.<name>] |
どちらも呼び名は同じです。db_field ルールの source 欄、そしてダッシュボードの「データソース」カード一枚が両方を一覧します。
レイヤー 1:データソースレジストリ
Section titled “レイヤー 1:データソースレジストリ”[redaction.data_sources.<name>] は、db_field ルールが必要としながら自分では持てない三つの事実を記述します。どのツールがテーブルの行を返すか、呼び出しごとにテーブルをどう決めるか、レコードが JSON のどこにあるか、です。
[redaction.data_sources.crm_pg]label = "客戶 CRM 資料庫"tools = ["pg_query", "pg_select"] # 1 個以上、完全一致か末尾 * の globtable_arg = "table" # 固定の `table = "customers"` と排他record_paths = ["$.rows[*]", "$[*]", "$"] # これが既定値そのものkey_alias = { name = "customer_name" } # ツールが項目名を変える場合だけdb_field ルールはこう参照します。
[redaction.rules.crm_customers]type = "db_field"source = "crm_pg" # `connector` は非推奨の別名として今も使えるfields = ["customers.name", "customers.address", "orders.*"]category = "DB_FIELD"内蔵で二つ出荷され、定義し直すことはできません。odoo(元の ODOO_TOOLS 表をレジストリの binding として表現し直したもの——9 個のツール、固定/動的テーブルの混在、mapper struct を経由するツールごとの key alias)と、duduclaw_db(db_select を束ねる、後述)です。source も connector も省略すると引き続き odoo が既定になるので、レジストリ導入前の設定は意味が変わりません。
テーブル名はもうドット区切りである必要はありません。db_field の検証は Odoo の model.model 形式に加えて、素の SQL 識別子(customers、public.customers)も受け付けるよう緩和されました。それ以外の db_field の fail-closed な挙動は変わっていません。未知の source、空の tools、壊れた record_paths、パターンに合わない table.column 文字列——いずれも黙ってスキップせず読み込みを拒否します。
レイヤー 2a:MCP プロキシ——自分で書いていないサーバーも遮蔽する
Section titled “レイヤー 2a:MCP プロキシ——自分で書いていないサーバーも遮蔽する”duduclaw mcp-proxy --server <name> -- <cmd> [args…] は隠しサブコマンドです。Claude CLI と一つの外部 MCP サーバーの間に立つ、stdio の JSON-RPC パススルーです。エージェントのターンで遮蔽が有効なとき、ゲートウェイはそのエージェントの spawn ごとの .mcp.json を書き換え、duduclaw 以外のstdio サーバーを直接ではなくこのプロキシ経由で起動させます——チャンネル返信の Claude CLI spawn(通常 spawn と one-shot PTY フォールバック、どちらも channel_reply.rs)と、dispatch/cron/heartbeat/goal-loop のターンの spawn(claude_runner.rs の prepare_claude_cmd、同じ共有ヘルパー mcp_proxy_cli_args 経由)の両方を対象とします。プロキシは内蔵の絞り込み点とまったく同じ二つの処理を適用します。
tools/callリクエストのargumentsは同じ egress 判定を通ります(Denyはその場で JSON-RPC のエラーとして返し、呼び出しは upstream に届きません。Allowは許可されたツールに対して<REDACT:…>トークンを実値に復元することがあります)。- 対応するレスポンスの
resultは同じredact_valueパイプラインを通ります。ツール名は<server>.<tool>の名前空間になるので、match_tool = "crm_pg.pg_select"が同名の DuDuClaw ツールを誤って捕まえることはありません。
それ以外——initialize、tools/list、通知、upstream 発のリクエスト、JSON として解釈できない行——はすべてそのまま転送されます。ラップされたサーバー本来の env にある資格情報は、argv ではなく環境変数 DUDUCLAW_MCP_PROXY_ENV(JSON の塊)を通して渡されます。Linux では /proc/<pid>/cmdline は誰でも読めますが /proc/<pid>/environ はそうではないからです。fail-closed もここで同じく効きます。プロキシは duduclaw mcp-server と同じ McpRedactionLayer::try_init の三択の結果に従います——遮蔽が無効なら純粋なパススルー、設定が壊れていれば何も転送せずプロキシ自体が起動を拒否します。
まだ対応していないもの:
- HTTP/SSE の MCP サーバー。 ラップする子プロセスが存在しないため、書き換えの対象から外れ、警告だけをログに残します。そのツール結果は遮蔽されないままモデルに届きます。
- (もはや穴ではありません: PTYセッションプールは2026-09に削除され、Claudeのspawnはすべて書き換えが既にフックしている呼び出しごとのspawnです。)
- codex / antigravity。 これらのランタイム自身の MCP 登録は、まだ一切このプロキシを経由しません。
レイヤー 2b:direct-API のツールループ(openai-compat)
Section titled “レイヤー 2b:direct-API のツールループ(openai-compat)”CLI 系のバックエンドは duduclaw mcp-server か duduclaw mcp-proxy から遮蔽を得ますが、duduclaw-llm の run_tool_loop(openai-compat ランタイム——API モードの Grok / DeepSeek / MiniMax エージェントなど)で動くモデルは、プロセス内の ToolRegistry と直接話すので、横取りするサブプロセスがありません。duduclaw-llm はこのためだけに ToolInterceptor トレイトを新設しました。before_call(server, tool, args) は dispatch 前に引数を拒否・書き換えでき、after_call(server, tool, args, &mut result) はモデルが見る結果を後から書き換えられます。ゲートウェイの RedactionToolInterceptor は、パイプラインの他の部分と同じ RedactionManager に対してこれを実装しているので、一度書いたルールは、モデルがそのツールに duduclaw mcp-server 経由で届いても、MCP プロキシ経由で届いても、このプロセス内経路で届いても、同じように振る舞います。もう一つ、どちらの仕組みもまだ届かないツール派遣経路があります——ローカル推論のツールループ(local_llm.rs)、ローカルでホストされたモデルがツールを呼ぶ経路で、その結果も遮蔽されません。
ネイティブコネクタ(duduclaw-db)
Section titled “ネイティブコネクタ(duduclaw-db)”MCP サーバーを一切持たない顧客——ただ PostgreSQL / MySQL / SQLite のデータベースがあるだけ——のために、duduclaw-db クレートは自前の 4 個の MCP ツールを持つ、ファーストパーティの読み取り専用 SQL コネクタです。DuDuClaw 自身の MCP サーバーを通して dispatch されるため、結果は遮蔽の絞り込み点に直接届き、プロキシを経由する必要がありません。
三つの独立したレイヤーで読み取り専用を保証する
Section titled “三つの独立したレイヤーで読み取り専用を保証する”- ステートメントガード——一番安価で、単独では一番信用しない層。単一の文で、
SELECTかWITHで始まらなければなりません。文字列リテラルの外に;は許しません。データを書き換える CTE(WITH x AS (DELETE … RETURNING *) SELECT * FROM x)はわざとこの層を通過します——それは単一のWITH文そのものだからです。実際に止めるのは層 2 です。 - ドライバレベルの read-only——PostgreSQL は
BEGIN READ ONLY、MySQL はSTART TRANSACTION READ ONLYを実行し、SQLite は読み取り専用のファイルハンドルで開きます。書き込みを本当に不可能にしているのはこの層で、その上のパーサーではありません。 - 上限——
max_rows(既定 200、上限 1000)とtimeout_ms(既定 10 秒、下限 100 ミリ秒、上限 120 秒)が、一回の呼び出しにかけられるコストを縛ります。
ソースの設定
Section titled “ソースの設定”[db_sources.crm_pg]label = "客戶 CRM 資料庫"driver = "postgres" # postgres | mysql | sqliteurl = "secret://env/CRM_PG_DSN" # または url_enc = "<暗号文>"allowed_tables = ["customers", "orders"] # 必須、空不可。["*"] は全テーブル許可max_rows = 200timeout_ms = 10000url はプロジェクトの secret://<backend>/<name> という資格情報の作法を通ります。平文の接続文字列は driver が sqlite でない限り読み込み時に拒否されます(sqlite だけは値がファイルパスであって資格情報ではないため)。パスワード入りの DSN が config.toml に平文で座っている理由はどこにもありません。
allowed_tables = ["*"] は受け付けられますが、これが唯一 db_query(後述)を解禁する設定でもあります。実在のテーブル一覧があるということは「db_select を通してのみ到達可能で、テーブルは検証済みの束縛された識別子」を意味します。自由形式の SQL は本物の SQL パーサーなしにテーブル allowlist と突き合わせることができないので、allowlist を持つどのソースでも丸ごと拒否されます。
エージェントへの許可
Section titled “エージェントへの許可”他のすべての capability と同じく、既定は拒否です。今では agent.toml を手で書き換えるのが唯一の方法ではありません。ダッシュボードのデータソースウィザードのステップ 4、またはデータベース行の許可ダイアログでチェックする方法、そのエージェント自身の設定ページの「ツールと権限」タブにある「このAI社員が使えるデータベース」で選ぶ方法、あるいはチャットでエージェントに直接頼む方法があります——MCP ツール agent_update は db_sources / db_sources_add / db_sources_remove を受け付けます。どの経路も最終的には同じ場所に落ち着きます:
[capabilities]db_sources = ["crm_pg"]MCP dispatch の絞り込み点は呼び出しのたびにこの設定を読み直すので、どの経路で許可してもすぐに反映されます。次のチャットのターンから見えるようになり、gateway の再起動は不要です。
二つの関門があり、どちらも必須です。Scope::DbRead(db:read——23 番目の MCP スコープ)とこの空でない許可リストは、どのツールハンドラが動く前にも MCP dispatch の絞り込み点で一度だけ検証されます。具体的な source 名はハンドラの中でもう一度検証されるので、["crm"] への許可がたまたま同じドライバを使う payroll ソースに届くことはありません。
4 個のツール
Section titled “4 個のツール”| ツール | 引数 | 戻り値 |
|---|---|---|
db_sources |
なし | [{name, label, driver}]——このエージェントが許可されかつ実際に読み込めるソースだけ。許可はあるが壊れているソースは、黙って省くのではなく理由付きで報告される |
db_tables |
source |
{tables: [{name, columns: [{name, type}]}]}、allowed_tables で絞り込み済み |
db_select |
source、table、columns?、filter?([{column, op, value}]、op は = != < <= > >= like in のいずれか)、order_by?、limit? |
{rows, row_count, truncated}——識別子は検証・クオートされ、値はすべてパラメータとして束縛される |
db_query |
source、sql、limit? |
同じ形。ソースの allowed_tables が厳密に ["*"] のときだけ使える。それ以外では接続すら開かずに拒否される |
行の値は、ドライバがたまたまデコードした形をそのまま返すのではなく、意図的に対応付けられます。真偽値はそのまま真偽値、整数と浮動小数点は JSON の数値に、NUMERIC/DECIMAL は文字列になります(浮動小数点にすると金額の端数が丸まるため)。タイムスタンプは ISO-8601 文字列、bytea/BLOB は base64、JSON 型の列は二重エンコードせずパース済みで返ります。どの規則にも当てはまらない型は、推測したり黙って落としたりせず、正直なプレースホルダー "<unsupported type: NAME>" になります。黙って落とすと、その列を狙った遮蔽ルールが誰にも気づかれずに何にも一致しなくなるからです。
プールはキャッシュせず呼び出しのたびに新しく開きます。資格情報のローテーションや [db_sources.…] の編集が、再起動を待たず次の呼び出しから効くようにするためです。
内蔵の duduclaw_db レジストリソース
Section titled “内蔵の duduclaw_db レジストリソース”duduclaw_db は db_select という一つのツールだけを束ね、テーブルは呼び出し自身の table 引数から読み、レコードは $.rows[*]、項目名はそのまま通します。つまり [db_sources.*] のどの接続に対する db_field ルールも、この一つの内蔵ソースを使い回せます——source = "duduclaw_db", fields = ["customers.name"] と書くだけで、別途レジストリに登録する必要はありません。binding が見ているのは table 引数だけで、どの db_sources 接続が呼ばれたかではないからです。もし二つの異なる接続がたまたまどちらも customers という名前のテーブルを持っていて、片方だけを隠したい場合は、代わりに明示的な json_path ルールを書き、match_args = { source = "crm_pg", table = "customers" } のように指定してください(source も table もどちらも db_select のトップレベル引数なので、どちらか一方でも両方でも条件にできます)。db_query はレジストリでわざと未束縛のままです。生の SQL 結果の列がどのテーブルに属するかは、実際のパーサーなしには呼び出しから分かりません。
地端ファイル(CSV/Excel/テキスト)
Section titled “地端ファイル(CSV/Excel/テキスト)”db_field ルールが Postgres や Odoo の項目を隠せるのは、どちらの経路も最終的に MCP ツール呼び出しに行き着くからです——遮蔽が key 名を見られる唯一の場所です。エージェント自身のディスク上のファイルには、デフォルトではこの絞り込み点がありません。Claude CLI 組み込みの Read(と Bash 経由の head/cat)は MCP ツールではないため、そうやって開いた customers.csv はまるごとモデルのコンテキストに入り、どのルールにも見えません。チャンネル添付はこの穴をさらに悪化させます——format_attachment_ref はメッセージ本文に保存済みファイルのパスを付け加えるだけなので、エージェントは結局 Read でそれを開きます。office_script/sheets_read はすでに絞り込み点を通っていますが、持っているのはパターンルールだけで、ここにも項目という概念はありません。しかも PostToolUse フックはツール結果を後から書き換えられないので、「出口で隠す」という選択肢はそもそも存在しません。唯一の入り口は、読み込み自体を MCP 呼び出しにして、組み込みの経路をふさぐことです。
三つのツール、一つのパスの柵
Section titled “三つのツール、一つのパスの柵”| ツール | 引数 | 戻り値 | 上限 |
|---|---|---|---|
file_read |
path、max_bytes? |
{path, table, text, truncated} |
512 KiB、プレーンテキストのみ |
csv_read |
path、delimiter?、has_header?(デフォルト true)、limit?(デフォルト 200)、offset? |
{path, table, columns, rows, row_count, truncated} |
ディスク上 64 MiB、limit は 2000 が上限 |
xlsx_read |
path、sheet?、limit?、offset? |
同じ形に加えて sheet と sheets |
ディスク上 32 MiB(表計算パーサーはディスクサイズをはるかに超えて展開するため、CSV より厳しい上限) |
table は常にファイル自身の拡張子付きベース名です——customers ではなく customers.csv。すべてのパスは 1 バイトも読む前に一つの柵を通ります。canonicalize したうえで、エージェント自身のディレクトリ、<agent_dir>/attachments、<home>/attachments、あるいは operator が config.toml [files] allowed_roots で宣言したエントリのいずれかに含まれていることを要求します。柵から逃れる .. セグメントやシンボリックリンクは canonicalize が走る前に拒否されるので、エラーは「許可範囲外です」という曖昧な言い方ではなく、本当の問題を名指しします。(db_sources と違って)柵の上にエージェント単位の capability ゲートはありません——柵自体がすでに「これは誰のデータか」に答えているからです。したがって三つのツールは常に tools/list に載り、files:read スコープ(Scope::FilesRead、24 番目の MCP スコープ)だけが門番です。
csv_read はワークスペースに既にある csv クレートで読み、ヘッダー行がなければ c1..cN という列名にフォールバックします。xlsx_read は calamine(新規依存、純粋な Rust 実装、dates フィーチャのみ)で xlsx/xlsm/xls/ods を読み、セルは順に JSON の数値、真偽値、null(空)、ISO-8601 文字列(日付時刻)、文字列のいずれかに対応付けられます。どちらのツールも監査記録にセルの内容は残さず、path/table/row_count——読み込みの「形」だけを残します。mcp_db.rs がフィルタ値について守っているのと同じルールです。
内蔵の duduclaw_files ソース
Section titled “内蔵の duduclaw_files ソース”レジストリには、二つの構造化リーダーにあらかじめ束ねられたソースが用意されています。table は引数ではなくツールの戻り値から読み(table_result = "/table")、レコードは $.rows[*]、free_form_names = true です——ファイルのテーブル名や項目名は識別子である必要がありません。このフラグがあるからこそ、次のようなルールが書けます。
[redaction.rules.customer_files]type = "db_field"source = "duduclaw_files"fields = ["customers.csv.name", "客戶清單.xlsx.地址"]category = "DB_FIELD"各エントリは最後のドットで分割されます——customers.csv.name はテーブル customers.csv の name 列です。テーブルはシート情報を含まないファイル名そのものなので、客戶清單.xlsx.地址 というルールは xlsx_read がどのシートを開いたかに関わらず発火します。シート単位で照合する対象がそもそも存在しないため、そのワークブックの全シートをまとめてカバーします。CJK の列名は内部で引用符付きのパス形式($.rows[*]['地址'])にコンパイルされます——JsonPath 文法に追加された ['key'] セグメントは、引用符と改行以外の任意の Unicode 文字を受け付け、まさにこのケースのために作られました。素の .key 形式は ASCII のままです。file_read は何にも束縛されていません。返すのは非構造化テキストなので、そこで効くのはパターン照合パスだけで、項目ルールは決して効きません。
データファイルガード
Section titled “データファイルガード”[redaction] data_file_guard = "on" | "read_only" | "off"、デフォルト on。呼び出し元のエージェントで遮蔽が実際に有効なときにのみ効きます。これは PreToolUse フック(duduclaw hook data-file-guard、既存のセキュリティフックと並べて登録されます)で、DUDUCLAW_DATA_FILE_GUARD を読みます——ゲートウェイが spawn 時に設定する環境変数で、エージェント自身が読んだり書き換えたりできる設定値ではありません。on は .csv/.tsv/.xlsx/.xlsm/.xls/.ods パスへの組み込み Read をブロックし、コマンド文字列がそのいずれかの名前を含む Bash もブロックします。read_only は Read だけをブロックし、off は何もブロックしません。ブロックされると「此檔案受去識別化保護、請改用 csv_read/xlsx_read/file_read」という拒否メッセージが返ります。
一つの限界は曖昧にせず、はっきり書いておきます。Bash の検査はファイル名のヒューリスティックにすぎず、動的にパスを組み立てるコマンド(python -c "open(chr(99)+...)")はまっすぐすり抜けます。これは後で直すべきバグというより、PreToolUse フックが約束できることの本質的な限界です。モデルが誤って未遮蔽の経路に迷い込む確率を下げているにすぎません。
もう一つの限界はすでに塞がれました。このフックは以前シェルスクリプトだったため、bash が PATH にない Windows ホストではコマンド自体が失敗し、Claude Code は非 2 の終了コードを許可として扱っていました——誰も気づかない場所でガードが消えていたのです。2026-09 の機能棚卸しで、兄弟の agent-file-guard と同じく Rust サブコマンドに書き直され、バイナリが動くすべてのプラットフォームで動作します。変更前にインストールされたエージェントも問題ありません。次回の spawn でインストーラーがフック設定を差し替え、残っている .claude/hooks/data-file-guard.sh を削除します。本当の保護面は MCP ツールそのものです。duduclaw_files に束ねられたルールは、値がすでに $.rows[*] の中に入ってはじめて見えるのであって、上流でガードが何かを捕まえたかどうかとは関係ありません。
添付ファイルのヒント
Section titled “添付ファイルのヒント”format_attachment_ref——ゲートウェイがチャンネル添付の参照行に付け加える一文——は、csv/tsv/xlsx/xls/ods/txt/md/json についてもう一文を追加するようになりました。「請用 csv_read/xlsx_read/file_read 讀取」。それ以外の拡張子(画像、音声、動画)は以前とバイト単位で同じままです。これは強制層ではなく、あくまで後押しです。実際に誤ったツールの発火を止めているのは、前段のガードのほうです。
なぜ file_read は表計算ファイルを拒否するのか
Section titled “なぜ file_read は表計算ファイルを拒否するのか”file_read は一つのテキストの塊を返しますが、duduclaw_files に対する db_field ルールは構造化された結果の中の $.rows[*].<column> に束縛されます。表計算ファイルの生バイトを file_read に通してしまうと、客戶清單.xlsx.地址 のような項目ルールは照合すべき .rows[*] を一度も見つけられず、パターンルールだけに守られたまま素通りしてしまいます。そこで file_read はまず拡張子を確認し、csv/tsv/xlsx/xlsm/xls/ods はその場できっぱり拒否して、呼び出し元を黙って無防備な読み込みを返す代わりに csv_read/xlsx_read へ誘導します。
カスタムルール:自社だけの識別子を、正規表現なしで
Section titled “カスタムルール:自社だけの識別子を、正規表現なしで”データソースは遮蔽に対して、ある項目がどこにあるかを教えます。カスタムルールが教えるのは、ある値がどんな見た目か——社内にしか存在せず、内蔵のルールセットには知りようがない識別子のためのものです。社員番号、社内プロジェクトのコードネーム、顧客コード、契約書のプレフィックスなど。
2026-09 より前は、5 つの内蔵ルールセット(general/taiwan_strict/taiwan_minimal/financial/developer)はチェックだけできる固定リストで、拡張はできませんでした。カスタムルールセットファイルは起動時に技術的には解決される仕組みでしたが、それを作る手段が製品のどこにもありませんでした——TOML を手書きするか、カスタムルールなしで済ませるかのどちらかでした。
1 つのルール = データ種別名 + 認識方法
Section titled “1 つのルール = データ種別名 + 認識方法”各ルールは、自分で入力するデータ種別名(員工編號、Employee ID など、最大 32 文字)と、ちょうど 1 つのマッチャーを持ちます:
- キーワード一覧——構文を書かないルート。1 行 1 語で貼り付けます。各語は 2 文字以上必要です。ASCII の語は単語単位、CJK は部分文字列で一致し、いずれも大文字小文字を区別しません(
keywordルール種別がこれまでずっと持っていたのと同じ意味論です)。 - パターン——リストではなく形で認識する正規表現です。
どのルールにも有効フラグがあり、削除せずに一時的に外しておけます。enabled はルール仕様そのものに追加された新しいフィールドで、すべてのルール種別に適用されます——regex、keyword、identity、json_path、db_field のいずれのルールでも enabled = false ならエンジンのコンパイル時点でスキップされ、マッチング時にフィルタされるわけではありません。省略時は true として扱われるので、既存のルールは何も変わりません。
どこに置かれるか
Section titled “どこに置かれるか”カスタムルールはルールセットファイルであって、[redaction.rules.*] にインラインで書くエントリではありません——operator にとっての心的モデルは「ルールセットがもう 1 つ増える」ことであり、ルールセットファイルはすでにチェックする対象のルールセット一覧に現れる仕組みだからです:
[meta]name = "我的規則"description = "在儀表板建立的自訂規則"version = "1"
[meta.labels] # category id → what a human seesCUSTOM_EMPLOYEE_ID = "員工編號"CUSTOM_01 = "內部專案代號"
[rules.employee_id]type = "regex"pattern = 'EMP-\d{4}-\d{4}'category = "CUSTOM_EMPLOYEE_ID"priority = 60enabled = trueはっきりさせておく価値のある点が 2 つあります。
[meta.labels] は新規追加です。 トークンのカテゴリはその構造上そもそも機械的な形をしています([A-Z0-9_]{1,32})。そのため、独自のカテゴリを作るルールセットには、それが何を意味するかをどこかに書いておく必要があります。ASCII のデータ種別名は CUSTOM_<SLUG> になります。ASCII 文字を含まない名前——「內部專案代號」など——は、次に空いている 2 桁の連番を使って CUSTOM_NN になり、人が読める名前はここに記録されます。redaction.get は現在リストされているすべてのルールセットを横断してマージしたマップを category_labels として返します。ダッシュボードはカテゴリを解決するとき、まずこのマップを、次に自分自身の翻訳を確認し、最後に生の id にフォールバックします。
優先度 60 は、カスタムルールをデフォルト帯(50)より上に置きます——自社の社員番号ルールは汎用の連続数字ルールに勝ちます——一方で精密な内蔵ルール(100)よりは下なので、重複が起きても身分証番号のようなパターンは変わらず優先されます。
書き込みはアドバイザリファイルロックと、一時ファイル + アトミック rename を経由します。ルールセット名がまだ config.toml [redaction] profiles になければ自動的に追加され、稼働中のパイプラインはただちに再構築されます——redaction.update が使うのと同じホットリロードで、gateway の再起動は不要です。
例からパターンを生成する
Section titled “例からパターンを生成する”正規表現が書けなくても、代わりに実際の値を 2〜5 個(EMP-2024-0133、EMP-2025-0007 など)と、似ているけれどマスクしてはいけない反例を最大 3 個貼り付ければ、redaction.suggest_pattern がパターンを書いてくれます。
3 つのエンジンが順番に試され、どれが答えたかはレスポンスでわかります:
- ローカル推論——ローカルバックエンドに実際に到達できる場合。
- クラウドのユーティリティモデル——アカウントローテーターを経由。
- ヒューリスティック——モデルを一切使いません。各例を数字/大文字/小文字の連続と区切り文字のリテラルに分解し、すべての例が同じ形を共有することを要求して、
\d{4}、[A-Z]{2,4}、エスケープ済みの区切り文字を出力します。厳密な比較で例を揃えられない場合は、さらに緩い比較で英数字の連続をすべて[A-Za-z0-9]として扱い、再度試みます。
どのエンジンが生成したものであっても、パターンは返される前に必ず検証を通過しなければなりません。すべての例がパターンに全体一致(アンカー付き)し、どの反例も一箇所たりとも一致してはいけません。反例のチェックはあえてアンカーなしで行います——実際のエンジンはアンカーではなく検索でマッチさせるため、反例の内部でだけ発火するパターンでも、実際にはその反例をマスクしてしまうからです。モデルの最初の回答が検証に失敗した場合、失敗したチェック内容をフィードバックとして渡し、ちょうど 1 回だけ再試行させます。それでも失敗すれば、チェーンは次のエンジンに落ちます。どのエンジンも検証を通るパターンを生成できなかった場合、回答は pattern: null、all_ok: false になります——正直に「見つからなかった」と答えるのであって、でっち上げることは決してありません。
貼り付けた例の値が行き先は prompt だけで、それ以外のどこにも渡りません——ログにも監査ログにも一切書き込まれません。prompt の中では XML 区切りで囲まれ、指示ではなくデータであると明示的にラベル付けされます。この RPC は operator ごとに毎分 10 回のレート制限がかかっています。
ルールパックをインポートする
Section titled “ルールパックをインポートする”redaction.profiles.import は TOML 形式のルールパック(貼り付けまたはアップロード)を受け取り、2 つ目のカスタムルールセットとして ~/.duduclaw/redaction/profiles/<slug>.toml に配置します——同じルールを多数の顧客に展開するインテグレーター向けの経路です。
ディスク上の slug は [a-z0-9_-]{1,40} で、次の 3 段階で決まります:明示的に指定された name パラメータ、それがなければ [meta] name から作った ASCII slug(まだ使われていない場合)、それも駄目なら pack_<正規化した [meta] name の SHA-256 先頭 8 桁 16 進数> です。台湾のパックは通常この最後の段階に行き着きます——たとえば「製造業客戶包」には slug 化できる ASCII 文字が一切ありません——この結果は決定的なので、同じパックを再インポートすると前回作成したルールセットを上書きするだけで、2 つ目が積み上がることはありません。人が読める [meta] name はこの処理の影響を一切受けず、ダッシュボードのあらゆる場所で表示されるルールセットのラベルであり続けます。機械的な形になるのはファイル名だけです。明示的に指定した name が不正であるか内蔵ルールセットと衝突する場合はエラーになります(自分で入力したものなので修正できます)。自動生成された slug が行き詰まることは決してありません——インポートダイアログには拒否すべき名前欄がそもそも存在しないからです。
ルールは 1 つずつ検証され、不正なルールがあってもインポート全体を止めるのではなく、理由とその [rules.<id>] ヘッダーが書かれている行番号とともにスキップされます——コンパイルできないパターン、2 文字未満のキーワード、不正なルール id、不正なカテゴリなどです。カード自体が扱わないルール種別(json_path、db_field、identity)は、実際に稼働中のエンジンオプションに対してコンパイルすることで検証されるため、次のリロードを毒するようなルールはここで捕まり、ディスクに書き込まれることはありません。使えるルールがゼロのパックはエラーであり、空のルールセットにはなりません——何もマスクしないルールセットをダッシュボードに並べるくらいなら、拒否した方がましだからです。
dry_run: true を渡せば、名前・インポート件数・ルールごとのスキップ理由・カバーするカテゴリを含むレポート全体を、何も書き込まずに取得できます。
redaction.profiles.remove はカスタムルールセットファイルを削除し、[redaction] profiles から外してリロードします。内蔵ルールセットは拒否されます——バイナリに組み込まれているため削除するファイルが存在せず、一覧からこっそり外すだけでは削除が成功したように見えてしまうからです。
フェイルクローズな読み込み
Section titled “フェイルクローズな読み込み”custom.toml が存在するのに読み込めない、またはパースできない場合、redaction.custom_rules.list はエラーを返します。空のリストに縮退することは決してありません。「ルールがありません」という表示を見た operator は、自分のルールが削除されたと思い込むでしょうが、実際にはルールはディスク上にそのまま残っていて、同じパース失敗によってパイプライン全体が毒されているだけです。
保存する前にルールを試す
Section titled “保存する前にルールを試す”ウィザードの最終ステップでは、いま作ったばかりのルールをサンプルに対して実行します——何かが書き込まれる前に。そのために redaction.dry_run に 2 つの任意パラメータが追加されました:
sample_text——sample_jsonの代わりに、ただの文章を渡せます。内部的には JSON の文字列値として包まれ(まさにJSON.stringify(text))、サンプルは実際のツール結果と同じ経路でパイプラインを通ります。両方が指定された場合はsample_jsonが優先されます。draft_rules——{ id, label?, category, kind, keywords?, pattern? }の配列で、upsert を検証するのと全く同じコードで検証されます(そのため、プレビューが保存時には拒否されるはずのものを通してしまうことは決してありません)。これらはその 1 回の呼び出しに限って候補ルールセットへとコンパイルされ、稼働中の設定のインラインルールの上に重ねられます。既存のルールと同じ id を持つドラフトはそれを覆い隠します——これが編集をプレビューするということの意味です。どこにも何も書き込まれません。候補パイプラインは呼び出しが戻った時点で破棄され、保存済みのルールセットは影響を受けません。
ドラフトが生んだヒットは、ドラフト自身の id を rule_id として持つため、UI は「このルール・N 件」と数えられます。それ以外——内蔵ルールセット、項目ルール——はすべて以前とまったく同じように報告されます。無効なドラフトは呼び出し全体のエラーになり、一部だけ実行することは決してありません。プレビューが黙ってルールを 1 つ落としてしまえば、保存後のルールセットでは実現できないカバレッジを報告してしまうことになるからです。
| メソッド | パラメータ | 戻り値 |
|---|---|---|
redaction.custom_rules.list |
— | { rules: [{ id, category, label, kind, keywords, pattern, example, enabled }] } |
redaction.custom_rules.upsert |
{ id?, label, category?, kind, keywords?, pattern?, enabled? } |
保存された行、およびホットリロードから返る applied / warning |
redaction.custom_rules.remove |
{ id } |
{ ok, removed, applied, warning } |
redaction.custom_rules.set_enabled |
{ id, enabled } |
更新後の行 |
redaction.suggest_pattern |
{ examples: [2..5], counter_examples?: [0..3] } |
{ pattern, engine, checks: [{ value, kind, matched, ok }], all_ok } |
redaction.profiles.import |
{ toml, name?, dry_run? } |
{ name, imported, skipped: [{ rule_id, line?, reason }], categories, dry_run } |
redaction.profiles.remove |
{ name } |
{ ok, removed, applied, warning } |
redaction.dry_run(拡張) |
{ sample_json? | sample_text?, tool?, args?, draft_rules? } |
{ hits: [{ pointer, rule_id, category, token }], token_count, restored_ok } |
この 7 つはいずれも、他の redaction.* と同じ admin ゲートの内側にあります——どれもみな、このデプロイから外に出て行くものを決めるルールセットを書き換えるからです。
一覧の各行にある example フィールドについては触れておく価値があります。キーワードルールの場合、これは最初のキーワードです——自分で入力した、見ればわかる実際の値です。パターンルールの場合は、フォームから記憶されたものではなく、パターンから合成された値です。EMP-\d{4}-\d{4} は EMP-0000-0000 として表示されます(リテラルはそのまま、文字クラスは代表となる 1 文字、繰り返しは最小回数、無制限なら 4 回、選択(alternation)は最初の枝、アンカーは何も出力しません)。ウィザードに貼り付けた値はあえてどこにも保存されないため、表示できるのは再構成した値しかありません。合成を諦めるケース——展開すると 128 文字を超えてしまうパターンや、パースできないパターン——では、その行にはパターン自体がそのまま表示されます。誤魔化すより、正直な表示を選んでいます。
AI 検出(OpenAI Privacy Filter)
Section titled “AI 検出(OpenAI Privacy Filter)”ここまでのルールはどれも「形」に一致するものを検出します:身分証番号、クレジットカード番号、API キー、名前を付けた列など。氏名、住所、誕生日、口座番号にはそもそも決まった形がありません。これまで、それらを捕まえる唯一の方法は、それがどのデータベース列に入っているかを正確に知ることでした——自社の ERP には通用しても、貼り付けたメールスレッドや、想定外の列を含むアップロード済みスプレッドシート、チャットメッセージには何の役にも立ちません。
ai_pii ルールセット(「AI 検出」)がこれを埋めます。動かしているのは OpenAI の Privacy Filter——1.5B パラメータ(アクティブ 50M)の双方向トークン分類器、Apache-2.0 ライセンス——をローカルで、ONNX Runtime 経由で実行します。どこにも何も送信されません:API もテレメトリもなく、推論時にネットワーク通信は一切発生しません。
検出する内容
Section titled “検出する内容”8 つのラベルが遮蔽カテゴリにマッピングされており、モデルによるヒットと同種の正規表現によるヒットは同一の token になります(モデルが見つけた EMAIL と general の正規表現が見つけた EMAIL は同じカテゴリなので、EMAIL を指定したソースフィルタは両方をカバーします):
| モデルラベル | カテゴリ | |
|---|---|---|
private_person |
PERSON |
氏名 |
private_address |
ADDRESS |
住所 |
private_email |
EMAIL |
|
private_phone |
PHONE |
|
private_url |
URL |
|
private_date |
DATE |
誕生日など個人の日付 |
account_number |
ACCOUNT_NUMBER |
銀行口座・顧客口座番号 |
secret |
SECRET |
パスワード、鍵、トークン |
これは何ではないか
Section titled “これは何ではないか”匿名化を保証するものではなく、見逃しもあります。 モデルカード自身がこれをコンプライアンス管理ではなくデータ最小化のコンポーネントだと述べており、私たち自身の計測結果もそれを裏付けています。119 個の span を含む 25 個の zh-TW 文でのテストでは:
| 再現率 | |
|---|---|
| 全体 | 79.8%(ラベルの正確さを問わなければ 89.1%) |
| 電話番号、email | 100% |
| 住所 | 88% |
| URL | 83% |
| 人名 | 72%(ラベルを無視すれば 76%) |
| 日付 | 58% |
| 口座番号 | 60%(ラベルを無視すれば 100%——台湾の銀行口座番号はしばしば private_phone とラベル付けされますが、どちらも遮蔽されるのでデータはマスクされたままです) |
| 長い多言語混在文書(3K 文字) | 88.0% / 95.1%、誤検知ゼロ |
誤検知は 5.5%(110 件中 6 件)で、そのすべてが span が後方に伸びて「出生日期」のような中国語の項目ラベルを飲み込んでしまったものでした——これは過剰マスクであって漏洩ではありません。私たちのテストで確認できたもう 2 つの既知の挙動として、氏名の直後に日付が続くと 1 つの span に融合することがあります(日付自体はカバーされたままで、ラベルが private_person になるだけです)。また email の span が先頭の空白を含んでしまうことがあります。
より難しいコーパス(ウェブクロール、医療記録、法律文書)での独立したベンチマークでは、再現率は 10% から 38% の範囲で報告されています。差はほぼすべて再現率にあります。だからこそこのルールセットの優先度は 30——すべてのパターンルールより下——であり、general/taiwan_strict を併用しておくべき理由でもあります。これは第二層であって、置き換えではありません。
モデルのインストール
Section titled “モデルのインストール”リリースバイナリにはモデルも ONNX Runtime も含まれていません。初めて「AI 検出」にチェックを入れると、カードがダウンロードを提案します:
| アーティファクト | サイズ | ソース |
|---|---|---|
| モデル(6 ファイル) | 約 945 MB、大半は onnx/model_q4.onnx_data |
Hugging Face openai/privacy-filter、特定の commit に固定 |
| ONNX Runtime | プラットフォームにより 7–75 MB | ort が対象とするバージョン(1.24.2)の Microsoft 公式 GitHub リリース |
すべてのファイルは URL、バイト長、SHA-256 がバイナリに固定されています。ダウンロードは <name>.part にストリームされ、HTTP Range ヘッダーで再開可能で、ハッシュ検証されてから初めて本来のパスにリネームされます——つまり最終パスにあるファイルは常に検証済みのファイルです。ハッシュが一致しなければ、部分ダウンロードを削除してインストール全体を失敗させます。「モデルの大半だけインストール済み」という状態は存在しません。installed.json マーカーは最後に書き込まれ、リビジョンを記録します。そのため部分的なインストールは、壊れたインストールとしてではなく未インストールとして読み取られます。
~/.duduclaw/models/privacy-filter/ config.json, tokenizer.json, tokenizer_config.json, viterbi_calibration.json, onnx/model_q4.onnx(+_data), installed.json~/.duduclaw/lib/onnxruntime/1.24.2/ libonnxruntime.dylib | libonnxruntime.so | onnxruntime.dll対応プラットフォーム。 macOS(Apple Silicon)、Linux(x86-64 と arm64)、Windows(x64 と arm64)。Intel 版 macOS は非対応です:Microsoft は ONNX Runtime 1.24 で osx-x86_64 ビルドを公開していないため、正直にインストールできるものが何もありません。カードはそのことを明示し、ふりをするのではなく fail closed します。
性能とメモリ
Section titled “性能とメモリ”10 コアの Apple Silicon マシン、ONNX Runtime CPU、intra-op スレッド 4 本、q4 グラフでの計測:
- 典型的な zh-TW の文で 63–161 ms。長文書では 1,000 文字あたり約 214 ms。
- 読み込み中は常駐メモリ約 1.1–1.7 GB。
idle_unload_minutes(デフォルト 10)の間推論がなければセッションはアンロードされ、次の推論時に再読み込みされるので、アイドル状態のデプロイではメモリが戻ってきます。 - マシンの RAM が足りず重みがページアウトされると、1 文の処理にミリ秒ではなく数秒かかることがあります。十分な余裕を与えるか、このルールセットをオフにしてください。
結果はテキストごとにキャッシュされます(256 エントリ、LRU)。これは見た目以上に重要です。1 つの ner ルールはラベルごとに 1 つのルールへとコンパイルされ、その 8 つがキャッシュを共有するため、1 ターンあたりの推論は 8 回ではなく 1 回で済みます。
「検出ルールセット」で「AI 検出」にチェックを入れるか、手動でルールセットを追加します:
[redaction]profiles = ["taiwan_strict", "ai_pii"] # keep the pattern rules on
[redaction.ner] # every key optionalthreads = 4 # ONNX Runtime intra-op threadsidle_unload_minutes = 10 # 0 disables unloadingmin_chars = 24 # shorter text never reaches the modelmax_chars = 32000 # longer text is chunked on paragraph boundariescache_entries = 256# model_dir = "/some/other/place" # default: <home>/models/privacy-filter自分で定義したルールでラベルを絞り込むこともできます:
[redaction.rules.names_only]type = "ner"category = "PII" # unused: each match is categorised by its labellabels = ["private_person", "private_address"]priority = 30min_chars と max_chars はルールごとに上書きできます。未知のラベルはルールがスキップされるのではなく、読み込みエラーになります。
フェイルクローズな挙動
Section titled “フェイルクローズな挙動”パイプラインの他の部分と一貫しています:動かないルールは、動いているように見えてはいけません。
- モデルをインストールせずにルールセットにチェックを入れると、
nerルールがコンパイルに失敗し、RedactionManager全体が失敗します——gateway は保護未起動状態に入り、duduclaw mcp-serverは、動いていないルールセットの下でツール結果を提供するくらいなら起動を拒否します。カードは保存前に警告します。 - 非対応プラットフォームや、
nerフィーチャーを無効にしてビルドされたバイナリでも同じことが起きます。ルール種別自体はどこでもパースできるので、ルールセットはビルドをまたいで持ち運び可能なままです。動かせない場所ではコンパイルを拒否するだけです。 - モデルを削除する(
redaction.model.remove)と、パイプラインは即座に再構築されるため、状態は次の再起動まで潜んでいるのではなく、その場で可視化されます。
モデルのヒットを監査する
Section titled “モデルのヒットを監査する”AuditEvent::Redact は、どのエンジンが各 span を見つけたかを記録するようになりました:
{"ts":"2026-09-24T…","event":"redact","rule_id":"ai_pii","category":"PERSON", "token":"<REDACT:PERSON:…>","engine":"ner", "model_revision":"7ffa9a043d54d1be65afb281eddf0ffbe629385b"}engine は決定的なマッチャーすべてで "rule"、モデルの場合は "ner" になります。model_revision はモデルによるヒットにのみ存在します。このリリースより前に書かれた監査ログには engine フィールドがなく、読み戻すと "rule" として扱われます。
| メソッド | パラメータ | 戻り値 |
|---|---|---|
redaction.model.status |
— | { installed, model_revision, ort_version, size_bytes, state, progress, error, avg_latency_ms, p50_latency_ms, calls, last_used_at, platform_supported, reason } |
redaction.model.install |
— | { started, installed }——冪等。ダウンロード中に再度呼ぶと started: false を返します |
redaction.model.cancel |
— | { cancelled }——部分ダウンロードは保持され、次のインストールで再開されます |
redaction.model.remove |
— | { removed }——モデルを削除し、ONNX Runtime ライブラリは保持します |
state は absent/downloading/ready/loaded/error のいずれかです。avg_latency_ms と p50_latency_ms は直近 100 回の実際の推論の移動窓から算出され、最初の推論の前は null です——推定値になることは決してありません。redaction.get の available_profiles[] は、ner ルールを持つルールセットに対して requires_model: true を付与するため、ner を使うインポート済みルールパックも ai_pii と同じダウンロードプロンプトを受け取ります。
この 4 つはいずれも、他の redaction.* と同じ admin ゲートの内側にあります。
ダッシュボード
Section titled “ダッシュボード”設定 → 匿名化 では、以前は別々だった「外部システム」カードと「データソース」カードが一つの「外部システムとデータソース」に統合されています。
-
テーブル項目ルールカード。形は変わっていません。ルールごとに 1 枚のカード(id、カテゴリ、種類バッジ、一行のカバレッジ要約)。「追加」は二つのモードを切り替えます。**テーブル項目(簡易)**は
sourceのドロップダウン、複数値の項目リスト(*に対応、idが残る旨の注記付き)、カテゴリ、誰が復元できるかを問います。**JSON パス(高度)**はmatch_tool、match_args、paths、exclude_keysで、簡易フォームで表せないものを指定します。ドロップダウンは表示名だけを示すようになりました。Odoo ERP、地端ファイル、そして自分で定義した各システム名です。三つの内蔵レジストリ id(odoo/duduclaw_db/duduclaw_files)が生の値として現れることはありません。生きたデータベース接続を持つソースを選ぶと、テーブル欄は今まで通りdb_sources.tablesを裏付けとするテーブル・項目ピッカーに変わります。保存のたびに解決済みルール一式をまず dry-compile し、パスの誤りや未知のソースはその場でインライン表示され、何も書き込まれません。「試験実行」ボタンは、貼り付けた JSON サンプル(模擬のツール名と引数付き)を実際のパイプラインに通し、ヒット表(pointer、rule id、category、token)を表示します。元の値は表示しません。 -
外部システムとデータソースカード。以前は別々だった「外部システム」と「データソース」の二枚を統合したものです。鼎新/Salesforce/HubSpot のテンプレートプリセットは廃止され、汎用の「カスタム MCP ツール」種別が同じ役割を担います。行は固定順で並びます。Odoo ERP(
odoo.statusによる接続状態表示。未接続なら統合ページへのリンクのみを表示し、実際の接続設定はそちらで行う)、db_sourcesの接続ごとに一行、カスタムのdata_sourcesレジストリ項目ごとに一行、そして設定不要の固定行「地端ファイル(CSV/Excel)」です。三つの内蔵レジストリ名(odoo/duduclaw_db/duduclaw_files)がそれ自体の行として描画されることはなく、上記四種類の行の裏側にある配管です。各行は二列固定です。読み込み(平易な要約。Odoo の固定テーブル一覧や、データベース接続の許可テーブル一覧)と書き戻し(三択の復元ポリシーのドロップダウンと監査開示のチェックボックス。データベース接続と地端ファイルは読み取り専用で「該当なし」と表示)です。クリックできる「N 件の項目ルール」バッジは、該当システムで絞り込んだ状態でテーブル項目ルールカードへ移動します。データベース接続の行にはさらに「{count} 人のAI社員が利用可能」というバッジが付き(0 人のときは警告色の「まだどのAI社員にも許可されていません」)、クリックするとウィザードのステップ 4 と同じチェックリストを持つ小さなダイアログが開きます。すでに存在しないデータソースを参照しているAI社員がいる場合、カードにはそれが誰で、どの id を参照しているかを簡潔に示す通知が表示されます——修正はそのAI社員自身の設定ページで行います。書き戻しキーは
tool_egressに自動的に導出され、operator には見えません。Odoo は常にodoo_*に書き込みます。カスタムツールソースは、toolsがduduclaw mcp-proxy形式の共通プレフィックスを持てば単一の<server>.*グロブにまとめられ、そうでなければツールごとに厳密一致キーになります。どちらの場合も、導出された全てのキーに同一のルールが書き込まれるので、operator から見れば一つの方針のままです。この UI 以前から残っていたtool_egressの項目は、カード下部の折りたたみ式「その他の書き戻しルール(詳細設定)」に収まり、無言で消えることはありません。データソース追加ウィザード、固定 4 ステップ。 ステップ 1「種類」はデータベース/Odoo/カスタム MCP ツールの三択です(Odoo を選ぶと説明と統合ページへのリンクだけが表示されます。地端ファイルは説明のみで選択不可で、上の無料行がそれに当たります)。ステップ 2「接続/ツール」はその種類に必要な項目だけを尋ねます。データベースなら表示名、driver、接続文字列、許可テーブル。カスタムツールなら表示名、ツール一覧、テーブルの識別方法(引数から/固定/結果から)です。
record_paths/table_result/項目エイリアス/タイムアウトは、既定値がすでに機能する「詳細設定」の折りたたみに収められています。ステップ 3「テスト」はデータベース種別なら実際に接続してテーブル一覧を取得し、チェックして選びます。カスタムツール種別ならサンプルを貼り付けてドライランします。このステップはカスタムツールソースではスキップできます(ボタンの文字が「次へ」ではなく「スキップ」になります)。新しいソースには通常まだそれを参照するルールがないためです。ステップ 4「書き戻し」は、データベース接続についてはもう「該当なし」だけの画面ではありません。「どのAI社員がこのデータベースを使えるか」を尋ねるチェックリストに変わり、編集時は現在の許可状態に合わせて事前にチェックが入り、アーカイブ済みのAI社員は一覧の最後に淡く表示されます。接続自体が読み取り専用でAI が書き戻せないことも、その横に注記されます。カスタムツールのステップ 4 は変わらず、復元ポリシーと監査チェックボックスを選びます。同じチェックリストはAI社員自身の設定ページの「ツールと権限」タブにも表示され、どちらから操作してもすぐに反映されます。設定にもう存在しない id にチェックが入っている場合は「存在しません」と表示され、operator が黙って落とされるのではなく自分でチェックを外せるようになっています。既存のソースを編集する場合は常にステップ 2 から始まり、ステップ 1 の種類選択は作成後には変更できません。内部識別子は表示名から自動生成され(ASCII を含まない表示名はsource_xxxxxx/db_xxxxxx形式)、「詳細設定」に読み取り専用で表示されます。UI では常に表示名を使います。既知の制限。 カスタムソースには永続化された「検証済み」状態がありません。ステップ 3 のテスト結果はダイアログが開いている間だけ存在し、保存自体は接続が実際に確認済みであることを記録しません。カスタムツールソースのステップ 3 ドライランは、少なくとも一つの項目ルールがそれを参照するまでは 0 件ヒットと表示されます。これは想定どおりの挙動(まだ一致するルールがない)であり、接続の失敗を意味しません。すべてのデータベース接続は単一の汎用ソース
duduclaw_dbを経由するため、テーブル項目ルールのフォームには、どの接続のテーブル一覧を候補として出すかだけを決める接続選択のサブピッカーが追加されています。保存されたルール自体には、どの接続が使われたかの記録は残りません。 -
保護未起動バナー。ゲートウェイの遮蔽マネージャが、構文は正しいが壊れた
[redaction]設定(あるいは設定自体がパースできない場合)から構築できなかったとき、カードの一番上に破壊的トーンのバナーが出ます。タイトルは「匿名化保護の起動に失敗しました」、その下に元のエラー理由をそのまま表示(翻訳せず、元のエラーメッセージがどの言語であってもそのまま)、最後に「<time> から発生・監査ログとアクティビティに記録済み」の行が続きます。ゲートウェイはチャットの応答は続けます(ツールを使わないので露出はありません)が、設定が直されて保存されるまで、すべての MCP ツールは使えません。保存されるとバナーは消え、再起動なしでパイプラインがホットリロードされます。
手順:ゼロから PostgreSQL の customers.name を隠すまで
Section titled “手順:ゼロから PostgreSQL の customers.name を隠すまで”-
接続を登録する。 ダッシュボードの「外部システムとデータソース → データソースを追加 → 種類:データベース」で入力する(ウィザードのステップ 3 で接続をテストします)か、
config.tomlに直接書きます。[db_sources.crm_pg]label = "客戶 CRM 資料庫"driver = "postgres"url = "secret://env/CRM_PG_DSN"allowed_tables = ["customers", "orders"]max_rows = 200timeout_ms = 10000CRM_PG_DSNはゲートウェイのプロセス自身の環境変数に、実際のpostgres://user:pass@host/dbという文字列として設定します。config.tomlに平文で触れることはありません。 -
エージェントに許可する。 「外部システムとデータソース → データソースを追加」のステップ 4 のチェックリストで許可するか(既存のソースを編集する場合は現在の許可状態に合わせて事前にチェックが入ります)、チャットでエージェントに「このデータベースを使わせて」と頼むか——どちらも最終的にはそのエージェントの
agent.tomlの同じ id に行き着きます:[capabilities]db_sources = ["crm_pg"] -
項目ルールを書く。 内蔵の
duduclaw_dbレジストリソースを使い回します(よくあるケースでは別途レジストリ登録は不要です)。[redaction.rules.crm_customer_names]type = "db_field"source = "duduclaw_db"fields = ["customers.name"]category = "DB_FIELD"restore_scope = { kind = "owner" }これは読み込み時に、
db_selectに束縛されargs.table == "customers"を条件とする一つのjson_pathルールへ展開され、$.rows[*].nameを隠します。 -
実際に効くことを確かめる。 ダッシュボードの「試験実行」ボタンにサンプルを貼る(ツール
db_select、引数{"table": "customers"})か、シェルから:ターミナルウィンドウ duduclaw redaction verify sample.json --tool db_select --arg table=customersレポートには、ヒットごとの JSON pointer、rule id、マスク済みだが判別できる元値(
王**)、token、category、そして往復の復元確認が並びます。遮蔽パイプラインの他の場所と同じ証跡の形式です。 -
customersに触れる質問をエージェントに投げる。name列はモデルには token として届き、チャンネルへの返信では信頼された egress 境界で復元され、本人には実名のまま見えます。他の遮蔽対象の項目と同じです。
添付 CSV を三ステップで
Section titled “添付 CSV を三ステップで”チャンネル添付には db_sources の許可も接続文字列も要りません。ファイルはもともとディスク上にあり、最初から柵の内側です。
-
ルールを書く。 内蔵の
duduclaw_filesソースを使い回すので、レジストリへの登録は不要です。[redaction.rules.attachment_customers]type = "db_field"source = "duduclaw_files"fields = ["customers.csv.name", "customers.csv.email"]category = "DB_FIELD" -
ファイルを送る。 ユーザーがチャンネルで
customers.csvを添付すると、エージェントが見る添付行には「請用 csv_read/xlsx_read/file_read 讀取」というヒントが付き、ガードがonであればそのパスへのReadはきっぱり拒否されます。 -
エージェントが
csv_readを呼ぶ。nameとemailの二列はモデルには token として届き、チャンネルへの返信ではファイルの所有者に対して、他の遮蔽対象の項目と同じ信頼された egress 境界で実値に復元されます。
- 正直に挙げると、まだ三つの穴があります。 HTTP/SSE の MCP サーバー(ラップする子プロセスがなく、警告をログに残す)。codex / antigravity ランタイム——自分自身の MCP 登録がこのプロキシを一切経由しません。そしてローカル推論のツールループ(
local_llm.rs)——ローカルでホストされたモデルのツール呼び出しは、プロキシにもToolInterceptorにも届きません。チャンネル返信(通常 spawn と one-shot PTY)と dispatch/cron/heartbeat/goal-loop のターンはカバー済みです。この三つはまだです。(四つ目だったPTYセッションプールは、2026-09のプール削除とともに消えました。) - レジストリが記述するのは形であって意味ではありません。
record_pathsやtable_argの誤字が、こっそり保護を弱めることはありません。誤った項目は読み込みを拒否する(fail-closed)か、何にも一致しません。後者は「試験実行」で 0 件のヒットとして見えます。 db_queryは、operator が明示的にallowed_tables = ["*"]と書いたソースにしか存在しません。 自由な SQL に対して allowlist を部分的・best-effort で効かせる仕組みはなく、設計はフィルタするふりをするのではなくツールごと拒否します。- プールは呼び出しごとに開く、キャッシュしません。エージェントが 1 ターンに数回しか呼ばないツールとしては妥当な代償で、資格情報のローテーションや設定編集が、ゲートウェイの再起動なしにすぐ効くという利点があります。
- AI 検出は見逃します。これは測定結果であって、言い回しではありません。 zh-TW 全体の再現率は約 80%、人名は約 72% で、当社のものとまったく似ていないコーパスではさらに下がります。パターンのルールセットは常に有効にしておき、
ai_piiはその上に重ねる二層目として扱ってください。これを有効にしたから準拠になる、というものでは決してありません。ロード中は約 1.1〜1.7 GB のメモリを消費し、1 文あたり 60〜160 ミリ秒かかり、Intel 版 macOS にはそもそも存在しません。 - データファイルガードは後押しであって、サンドボックスではありません。
Bashの検査はファイル名のヒューリスティックで、動的にパスを組み立てるコマンド(python -c "open(chr(99)+...)")はまっすぐすり抜けます。この限界は後から発覚したものではなく、ガード自身のソースコードにそのまま書かれています。(隣にあった Windows の穴——bashがないと黙って無効になるシェルスクリプト——は 2026-09 の棚卸しで Rust サブコマンドに書き直して塞ぎました。)地端ファイルの項目値を実際に遮蔽の射程内に留めているのは MCP ツール面(file_read/csv_read/xlsx_read)であって、その前段のガードではありません。 db_sourcesの許可は即座に反映されます。 MCP dispatch の絞り込み点は呼び出しのたびにagent.tomlを読み直し、各ターンは新しい CLI プロセスとして spawn されるため、新しいチャットのターンはすぐにそれを見ます。(唯一の例外だった pooled REPL——セッション再利用まで許可変更を拾わない——は、2026-09にプールごと削除されました。)db_sources_removeはわざと設定と突き合わせて検証されません。db_sourcesとdb_sources_addはどちらも id がconfig.toml [db_sources.<id>]に存在することを要求しますが、db_sources_removeはしません。そのため、operator がソースを削除した後でも、古くなった許可を名前で取り消すことができます。
db_field はかつて「Odoo だけ」を意味していました。今は「どのツールを通しても、このエージェントが見てよいどのテーブルでも」を意味します。顧客自身の MCP サーバー(プロキシ越しに、少し距離を置いて遮蔽される)でも、DuDuClaw が自ら接続するデータベース(自前の MCP ツールを通して、出どころで直接遮蔽される)でも。どちらの経路も同じレジストリ、同じダッシュボードカード、ルールの同じ source = 欄に収まるので、operator はマスキング方針を一度書けばよく、データが実際にどちらの経路を通ったかを知る必要も気にする必要もありません。