書き直すべきロジックとブラウザブリッジで許容すべきロジック──逆コンパイルコードの着地点を決める3層ラダー

最終更新日: 2026-07-30 11:03:46

リバースエンジニアリングの前半、つまり機械的な分割、アルゴリズム系統の特定、差分検証まで終えると、「何を計算しているのか」は分かる。しかし、それだけでは出荷できない。ロジックはなお元のランタイムに埋め込まれたままであり、CIに入れられる純粋関数にするのか、人が面倒を見る外部プロセスとして残すのかを決めなければならない。ここで判断を誤ると、前工程で短縮した時間を、運用で利息付きで払い直すことになる。

4段階の全体像では、この工程を「Stage 4、トランスポート層へフォールバックする」と一行で表している。本稿ではそこを掘り下げる。要点は明快だ。1段下の層へ降りるたび、依存範囲、障害パターン、デプロイコストは桁違いに増える。だからデフォルトでは、上の層を取りにいく。

着地点は3層。優先順位は固定する

ロジックの着地点は上から順に3つだけだ。この順番は「先に動いたものを採用する」といった場当たり判断ではなく、チームの規約として明文化しておくべきである。

  1. ネイティブで再実装する。 元のランタイムから切り離し、対象言語と標準ライブラリだけで書き直す。前提になるのは、アルゴリズム系統を正しく特定できていることだ。フィンガープリンティングの工程を通過していれば、実装の90%は公開実装を基にでき、残りの差分だけを個別に処理すればよい。最終的には純粋関数として着地する。

  2. ローカルJSエンジンで最小断片だけ動かす。 短期間では純化コストが高すぎるロジックもある。その場合は元のJSのうち必要な小片だけを残し、ページ全体ではなく、数十行程度をローカルのNode/V8で実行する。

  3. 受動的なブラウザブリッジを使う。 実際にログイン済みのページランタイムにしか存在しない状態もある。ランタイム配信される署名や、セッションに紐づく動的IDなどがそれだ。静的な再構築では再現できず、現時点ではブラウザ内部で読むしかない。この形態は一時的なものとして扱い、インターフェースの注記で明示する。デフォルトとして展開してはならない。

層が1段下がると、コストは線形には増えない

この順序を固定する理由は、3層のコストが少しずつ増えるわけではないからだ。下の層へ行くたびに、負担はおおむね1桁跳ね上がる。

依存範囲

主な障害パターン

CIで扱えるか

ネイティブ再実装

標準ライブラリのみ。外部プロセスは不要

出力不一致。assertひとつで特定できる

可能。純粋関数であるため

ローカルJSエンジン

Nodeランタイムが1つ増える。V8コンテキストはスレッドセーフではなく、並行実行にはロックが必要

エンジンのバージョン差、断片が依存するグローバル変数の欠落

ぎりぎり可能。ただしエンジンの導入が必要

受動的ブラウザブリッジ

実Chrome、拡張機能、人が維持するセッション、ローカルループバック通信

ページが開かれていない、セッション切れ、構造変更、タブが閉じられた

不可。生身の人間が必要になる

第1層の障害はユニットテストで捕捉できる。一方、第3層の障害は「今日はユーザーがそのタブを閉じた」だ。本来は純粋関数にできる処理を第3層に置くと、呼び出しのたびに人間を縛ることになる。参考値として、アルゴリズム系統を特定できれば、難読化された署名SDKでも、組み込みのcryptoだけに依存する600行未満の単体実装に落とし込み、第1層で動かせる。ブラウザブリッジ経由でしか扱えないと思っていたものの多くは、単にアルゴリズムをまだ十分に特定できていないだけだ。

受動的ブラウザブリッジの境界は「1回だけ取得し、決して操作しない」

受動的ブラウザブリッジが暴走する入口は一つしかない。「手伝い始める」ことだ。自動リフレッシュ、自動ログイン、読み込み待ちの自動化を入れるたびに、単なる転送役だったものがクローラーへ近づいていく。だから境界は極端に狭く切る必要がある。以下は、実際の失敗から得た制約だ。

取得は1回のスナップショットだけ。ページは絶対に変更しない。 すでに開かれているページについて、Cookie、セッション状態、ページランタイムをそれぞれ一度だけ取得する。タブを作らない、リフレッシュしない、遷移しない、フォーカスしない、ポーリングして待たない。ページがなければ、それはページがないということだ。ユーザーの代わりに開いてはいけない。

不足を検出した時点で、明示的なエラーを返す。 対象タブがなければtab_unavailable、ページはあるが未ログインならnot_logged_in、ログイン済みでもランタイムが準備できていなければruntime_unavailableを返す。3つのエラーコードはそれぞれ、現実の状態と次に取るべき行動に対応する。ページを待つ、ログインする、対象を切り替える、のいずれかだ。呼び出し側に推測させるだけの「失敗した」という曖昧なエラーにしてはならない。

機微な状態をブラウザの外へ出さない。 拡張機能はcookies / webRequest権限を要求しない。すでに開かれた条件一致タブだけを対象にし、そのページのコンテキスト内でリクエストを完結させ、返却前にフィールドを除去する。Cookieやページの署名状態がChromeの外へ出ることは一度もない。通信先もデフォルトでローカルループバックアドレスに限定する。ブリッジが運ぶのは認証情報ではなく、結果だけだ。

プラットフォーム数よりスコープが多い理由──15スコープは文脈単位で切る

受動的ブラウザブリッジが転送するのは、アダプターがパス、パラメータ、refererまで制約したホワイトリスト済みリクエストだ。ここで最も直感に反するのがスコープの粒度である。対象プラットフォームは数えるほどなのに、スコープは合計15ある。理由は、スコープを「プラットフォーム単位」ではなく「ページコンテキスト単位」で切るからだ。同じTikTokでも、Creative Center、Top Ads、Creator platform、インフルエンサーライブラリ、Ads Managerは5つの独立したスコープになる。セッション状態もページランタイムも別々であり、広告バックエンドにログインしてもCreator platformのランタイムは得られない。プラットフォームごとに1区切りだけ作ると、「サブサイトAにはログイン済みだが、サブサイトBには使えない」という最初のケースで破綻する。Xiaohongshuも同様に、メインサイト、アプリ相当のパス、クリエイターマーケットプレイスの3スコープに分かれる。

スケジューリングの粒度もこれに従う。ロックをかけるのはプラットフォームファミリー単位だけだ。同じファミリー、たとえばDouyinファミリー内のリクエストは直列に実行する。同じ実タブを再利用しており、同一ページコンテキストへ同時に投げると干渉するからだ。異なるファミリー、たとえばDouyinとXiaohongshuなら、無関係な2つのタブなので並列に実行できる。さらにファミリー内には最小リクエスト間隔を設ける。粗すぎれば本来並列にできる処理まで直列化し、細かすぎれば同じタブを共有するリクエストが衝突する。ページランタイムを共有する単位として、プラットフォームファミリーがちょうど自然な境界になる。

ログインは明示的なハンドオフでのみ人に渡す

受動的ブラウザブリッジはページを操作しない。ただしセッションは切れる。そこで人の介入は、明示的に起動する一度きりの操作へ集約する。対話的コマンドがハンドオフに入り、プログラムはOS経由で対応する業務ページを開く。ユーザーが手作業でログインし、ページの準備が整うのを待った後、元のリクエストを再実行する。この間、拡張機能はボタンをクリックせず、フォームにも入力せず、Cookieもエクスポートしない。ログインは実ブラウザ上でユーザーが行い、プログラムは完了後にリクエストを引き継ぐだけだ。

重要なのは、暗黙には絶対に発動させないことである。非対話的なコマンド、たとえばCIやスケジュールタスクは、ブラウザを勝手に開かない。セッションエラーを明確に返し、上位層に判断を委ねる。ハンドオフでは誤検知も防ぐ必要がある。ログイン成功後、ランタイムに与える追加待機は短時間に限る。実装では最大20秒であり、その時点までに判定が出なければならない。業務ページがすでに対象インターフェースの文脈を持たないアカウントページへリダイレクトされている場合、「まだ読み込み中」と誤認して延々と待つのではなく、そのステージを即座に終了する。フォールバックを許容するフローなら、このステージをunavailableとして記録して先へ進めばよく、フロー全体を失敗させる必要はない。

ステージの結果は「成功・失敗」だけでは足りない

ここまでの設計に共通する前提は、どのステージも「成功」か「失敗」だけを返してはならないということだ。オーケストレーションパイプラインにおける各ステージの結果には、6つの形がある。completed(完了)、empty(実行したがデータなし)、ready(送信待ちとして準備済み)、skipped(ルールにより意図的にスキップ)、unavailable(現時点では利用不可。通常はセッションが必要)、blocked(前提条件を満たしていない)だ。

「意図的にスキップした」「セッションが必要」「本当にデータが空だった」は、まったく別のシグナルである。不透明な結果を一つだけ返す設計では、その空結果が「空であるべき結果」なのか、「セッションが切れたのに誰も気付いていない」のかを判別できない。パイプラインを運用できなくなる。ステージ状態を有限のenumとしてモデル化すれば、オーケストレーション層はスクリプトであれモデルであれ、フォールバックするか、再認証するか、中断するかを判断できる。受動的ブラウザブリッジの3つのエラーコードを、フロー全体のレベルまで引き上げた考え方である。

能力ごとの着地点を、モデルに判断させる

ここで初めてモデルを使う。ただし役割は限定的だ。モデルに純化を代行させるのではなく、どこまで純化すべきかを判断する手助けをさせる。署名を破る仕事ではなく、アーキテクチャ上の判断である。中心となる問いはひとつだ。依存している状態は静的に再構築可能なのか、それともランタイムでしか得られないのか。そこから、純化にかかる工数と変更頻度を比較する。各工程でモデルに求めるものは異なる。

工程

必要な能力

選定

model id

モジュール全体を読み、依存範囲を把握する

長いコンテキストで、呼び出しグラフを一度に読める

Kimi K3

kimi-k3

層の選択を両面から議論し、「まず動かすだけ」に反論する

強い推論力。純化にもう1日かけるべき根拠を示せる

Claude Opus 5

claude-opus-5

数十〜数百の能力をまとめて一次仕分けする

低コストかつ高並行

Claude Sonnet 5

claude-sonnet-5

フォールバック後の原因帰属

中程度の推論力。障害ログに基づいて説明する

GPT-5.6 Sol

gpt-5.6-sol

とりわけ重要なのは2番目の工程だ。層の配置で最も起こりやすい失敗は、こちらの「とにかく動かして」という空気にモデルが迎合し、「ブラウザブリッジが一番簡単です」と返してくることにある。それは長期コストを見積もっているのではなく、こちらの言葉をなぞっているだけだ。強い推論を持つ層なら、「この部分は標準的なハッシュに定数による小さな摂動を加えたものです。純粋関数として実装するのに1日かける価値があり、ブリッジへ載せるべきではありません」と反論できる。鵜呑みにはせず、自分で試すとよい。ロジックを3件選び、そのうち少なくとも1件は正しい配置をすでに把握している対照ケースにする。同じ「配置を提案し、根拠を論じ、早すぎるブラウザブリッジ移行には反論してほしい」というプロンプトをclaude-opus-5gpt-5.6-solへ与え、ひとつだけ確認する。上の層へ引き上げようと戦うか、それとも怠惰に第3層へ落とすかだ。

本当の障害はモデル切り替えのコスト

3ベンダーから4層を使うと、SDKは3つ、認証方式も3つ、エラー形式も3つになる。工程ごとにモデルを切り替えるためにクライアントを書き直すのは割に合わない。その結果、多くの人はすべてを単一モデルで処理する。そして最も推論力を必要とする配置判断で、ただ迎合するだけのモデルを使うことになる。

AIReiterはこの層を平坦化する。キーは1つ、インターフェースはOpenAI互換で1つ、その背後に4層すべてがある。切り替えはリクエストボディのmodelフィールドを変えるだけだ。

# Placement argument: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<placement prompt + the reversed logic fragment + dependency list>"}]
  }'

# Bulk first-pass triage: change one field
#   "model": "claude-sonnet-5"
# Degradation attribution:
#   "model": "gpt-5.6-sol"

すでにOpenAI SDKを使っているなら、base_urlhttps://aireiter.com/api/v1に向ければよい。Anthropic SDKなら、同じキーでPOST /api/v1/messagesを呼び出せる。価格面では、Claudeモデルは定価から30%オフ、GPTモデルは半額で利用できる。このワークフローでコストが集中するのは2か所だ。数十〜数百の能力に対する一括一次仕分けはSonnetを高頻度に呼ぶ。モジュール全体から依存範囲を読む工程はKimiを使い、1リクエストあたりのトークン数が多い。一括仕分けはClaudeモデルで動くため、最も密度の高い工程に割引がそのまま効く。推論層での配置判断もClaudeモデルであり、30%オフだ。Kimi K3の長文コンテキスト層も同じキーで利用できる。

  • APIキーを取得する

  • 登録なしで試す — まずはロジック断片をいくつか手で入力し、「ブラウザブリッジに載せるべきか」という問いに対して、2つのモデルが迎合するのか反論するのかを見てみるとよい。

まとめ

リバースしたロジックをどこへ着地させるかは、技術の問題というよりコストの問題だ。ネイティブ再実装 > ローカルJSエンジン > 受動的ブラウザブリッジという3層ラダーは逆転できない。下へ降りるたび、純粋関数が、外部依存、人の介入、実際に開かれたタブを必要とするプロセスへ置き換わるからだ。受動的ブラウザブリッジは禁じ手ではない。ただし、厳格な境界を持つ一時的な部品である。取得は1回だけで操作はしない。ページがなければ即座に明示的なエラーを返す。機微な状態をブラウザ外へ出さない。ログインは明示的なハンドオフでのみ行う。ステージ状態は常に判読可能にする。これらを守れば信頼できる暫定策になるが、ひとつでも欠けば、誰も保守を引き受けたがらないブラックボックスになる。モデルは能力の着地点を判断し、「とにかく動かすだけ」という慣性に抵抗するために使える。一方、各再実装が正しいかどうかを判定するのはモデルではない。差分テストである。