記録されたアクセス元 (origin)¶
翻訳について: 正本は英語版です。日本語版が古い場合は英語版を参照してください。
ステータス: 2.5.x プレリリースラインへの提案。ツール契約に対しては加算的です。
引数が増えるツールは無く、既存の応答の形も変わりません。ただしデータベースに列を
1 本足すため、リリース標準の定義ではロールバックフリーではなく、プレリリースの
梯子を通ります (RELEASE_LIFECYCLE_STANDARD.md §2.1)。
1. 問題: agent_id は出自を運ばない¶
保存された行は「これは誰の記憶か」に agent_id で答え、「誰がその内容を produce
したか」に source で答えます。どちらも「どの呼び出し元がここに置いたか」には
答えず、そしてこの 3 つは日常的に一致しません。出荷済みコードのうち 4 つの経路が、
出自を復元できない行を作ります。
- 意図的に共有された
agent_id。 複数のオペレーターを 1 つのエージェント名前空間に 向けるのは意図的な選択でありえます — それが互いの履歴を読めるようにするからです。そう した瞬間、書き手を区別する唯一の手がかりはsource、つまり書き手が自分自身について 申告した値だけになります。 - unscoped な書き込み。
do_storeは空のagent_idを受理し、agent_id = ''の行を 書きます。これは事故ではなく意図的な injected-trust の継ぎ目ですが (bug-137)、残る行は 誰の名も持ちません。 import_memories。 取り込まれた行は取り込み側のagent_idと、書き出し側のsourceをそのまま受け取ります。誰がいつ取り込んだかは、どこにも記録されません。- 捨てられる検証済み subject。
_oauth_principalは検証したiss/subクレームを principal に載せます。クライアントの行が per-subject 分離を opt-in していない限り (OAUTH_DESIGN.md§12)、下流の誰も subject を消費せず、リクエストの終わりと共に消えます。
情報は書き込みの瞬間には存在しています。acl.current_principal() は、どのハンドラ
内でも Principal(client_id, issuer, subject) を返します。そしてそのまま捨てられ
ます。本ドキュメントは、それを保持することについてのものです。
2. これが何であり、決して何になってはいけないか¶
これは測定であって、主張ではありません。 値はサーバーが解決したものから、
サーバーが書きます。引数として受け取るツールはありません。この 1 つの規則こそが、
列を足す価値そのものです。呼び出し元が書ける field は既に source であり、2 本目を
足しても 1 本目以上の証拠にはなりません。
これは分離軸ではありません。 agent_id / project_id / channel は、クエリが
どの行を読むかを選びます。origin は何も選びません。これで絞られる recall は無く、
これによって到達可能・不能になる行もありません。書いた identity で記憶を絞ることは、
記憶がそれを越えるために存在する境界で、記憶を読めなくすることです。session.py が
申告型セッションキーについて示しているのと同じ規則で、理由も同じです。
これは認可の入力ではありません。 アクセス判定は ACL 層に属し、ハンドラに 到達する前に走ります。事後に読む列は何もゲートできませんし、判定に配線すれば ポリシーが 2 箇所に散ります。
これは個人の identity ではありません。 行に着地するのはサーバーが発行した不透明な alias で
あって、生の sub クレームではありません。§5 を参照。
3. なぜ source の拡張ではないのか¶
source はスキーマ強制の無い JSON 列なので、サブキーを足すだけならマイグレーション
は一切不要です。この経路は実測した上で却下しました。既存 field の 2 つの性質が
これを打ち消し、しかもその 2 つは、今いる場所で荷重を負っています。
source は呼び出し元が書く field で、書き込み経路は未知のキーを保存します。
normalize_source は legacy な shape を canonical な契約に畳むために存在しますが、
それは何も捏造せずに行われます。discriminator を捏造すれば attribution を偽り、
anonymous-source 検出器を無効化するからです。したがってキーのホワイトリストは
どこにもありません。
到達する経路は、未知 shape 向けの寛容な分岐ではありません。現在のすべての producer が通る canonical fast path です。
# (1) Already canonical — the fast path used by every 2.5.x producer.
raw_type = source.get("type")
if isinstance(raw_type, str) and raw_type in CANONICAL_SOURCE_TYPES:
return source, False
{"type": "Agent", "id": "...", "origin": {...}} を送った呼び出し元は、その dict を
そのまま返され、書き込みの継ぎ目でそのまま直列化されます。type が妥当なので
check_health(invalid_source_type) にも掛からず、偽装された origin はどこにも
異常として現れません。これを塞ぐには、呼び出し元の値をそのまま残すために書かれた
唯一の関数の中に、verbatim 保存の契約より手前で剥がす処理を足すことになります。
source は recall が返します。 スコア付きの各行が、これをモデルへ持ち帰ります。
ここに origin を置けば、クライアント識別子・subject alias・セッションキーが recall の
たびにコンテキストウィンドウへ入ります。hot path でトークンを払い、それを使い道の
ないモデルが読むことになります。避けるには、読み出し経路でサブキーを剥がすことに
なります。
この 2 つの修理は、observed/declared の分離と admin 限定の読み出し面を、より悪い 場所に書いたものです。列を分ければ両方が構造的に得られ、その代償であるマイグレー ションは、このスキーマの梯子が何度も通してきた唯一の形です。
4. 継ぎ目¶
store と archive_episode の書き込み経路から呼ばれる 1 つのヘルパーが値を解決します。
- client —
Principal.client_id。stdio とローカル principal ではローカルクライアントの 定数、プロバイダ発行トークンでは ACL 層が既に使っている issuer 名前空間付きの識別子 (oauth:<issuer>:<client_id>) なので、行と grant 行が同じものを読みます。 - subject alias — principal が検証済み subject を運び、かつ それに alias が発行済みの
場合のみ入ります。生の
subは決して入りません。 - transport — 呼び出しが stdio と共有 HTTP トランスポートのどちらで届いたか。これは 「1 プロセス 1 クライアント」と「1 プロセスが全員」の違いであり、client field がどれだけの 価値を持つかを後の読み手に伝えるものです。
- 申告されたセッションキー — 呼び出し元が送っていればその値。
解決の失敗はエラーではありません。principal が無い、subject に alias が無い、 transport が決められない。それぞれ自分のキーを省きます。空のオブジェクトは 「解決可能な出自を運ばなかった呼び出し」の正直な記録であり、同時に既存のすべての 行が持つ値でもあります。両者は意図的に区別できません (§7 を参照)。
5. 観測と申告を同じ袋に入れない¶
{
"observed": {
"client": "oauth:https://auth.example/:client_abc",
"subject_alias": "u-1a2b3c",
"transport": "http"
},
"declared": { "session_key": "..." },
"at": "2026-01-01T00:00:00+00:00"
}
入れ子こそが要点です。observed は、サーバーが解決したものを持ちます。OAuth 経路
ではそのフィールドは、ハンドラが走る前に検証された署名済みクレームに由来し、それが
読み手に、証拠として扱う資格を与えます。
declared は、呼び出し元が自分自身について申告したものを持ちます。申告された
セッションキーは比較されるだけで検証されません。誰でも任意の文字列を、他のセッション
のものを含めて送れます。だからこそ、検証済み subject と同じオブジェクトではなく、
信頼度が読み取れる場所に記録します。
平坦な blob の方が小さくなりますが、この列が存在する理由そのものを失います。1 年後に 問題のある行を監査する人は field を 1 つ読むだけで、行の他のどこにも、どちらの半分が 測定値かは書かれていません。
subject ではなく alias を。 subject の記憶が置かれる名前は、既にサーバー発行の
不透明な alias であり、(issuer, subject) → alias の対応は運用者が編集できる台帳に
あります。これは、プロバイダ移行や pairwise 識別子への切り替えが、孤立したデータでは
なく 1 ファイルの修理で済むようにするためです (OAUTH_DESIGN.md §12)。
生の sub を記憶行に書けば、その孤立化を行ごとに、しかも台帳の編集が届かない列で
再現することになります。加えて、export され recall される面に、個人のプロバイダ
識別子を置くことになります。
6. 読み出し面¶
フェーズ 1 では origin を検査系ツールにのみ出します。行単位の admin 読み出しと
health 面です。recall / recall_with_context には足しません。
この非対称は意図的です。recall 応答にフィールドを後から足すのは加算的で、外すのは 契約破壊です。コストがゼロの側から始めれば、選択肢は開いたままになります。そして この列が答える運用者向けの問い (「どの呼び出し元がこの行を書いたか」) は、モデルが recall の途中で答える問いではありません。
同じ理由で、recall のフィルタも提案しません。source_id は既に、申告された
attribution で絞ります。origin 型のフィルタは独自の判断を要する別の機能です。
加えてそれは per-subject 境界のすぐ隣に座ることになります。あの境界は構造的に
引き算であり、そこを通る 2 本目の加算的な経路を得てはなりません。
7. これで分からないこと¶
後から発見するのではなく、ここに書いておきます。
- stdio ではほぼ定数です。 ローカル principal は固定のクライアント id を持ち、 subject を持ちません。そのため単一ユーザーのローカル導入では、すべての行に同じ 2 フィールドが記録されます。この列が働くのは、1 プロセスが多数の呼び出し元を捌く 共有 HTTP トランスポートです。stdio から出ない配備は、ここから情報を期待すべきでは ありません。
- 遡及しません。 マイグレーション前に書かれたすべての行は
{}を持ち、 マイグレーション後であっても、解決可能な出自を持たない呼び出しが書いた行は同じく{}です。この列は、どちらかを区別できません。「これが出荷される前に書かれた」と 「識別できない呼び出し元が書いた」は、設計上同じ値です。区別を捏造することは、 誰も測っていない行についての主張を書くことだからです。 - import は取り込んだ人を記録します。 取り込まれた行の origin は、元の内容を
書いた人ではなく、取り込みを実行した人を指します。前者は知りようがなく、書き出し
側の値を、ここで測定されたかのように持ち越してはなりません。そうでなければこの列は
偽造の面になり、それは §3 が
sourceを却下した失敗様式そのものです。
8. スキーマと互換性¶
memories と episodes にそれぞれ origin TEXT NOT NULL DEFAULT '{}' を足します。
列を足すこれまでのマイグレーションが使ってきたのと同じ ALTER TABLE ... ADD COLUMN
の一歩であり、次のスキーマバージョンとしてスタンプします。
古いビルドはこの列を許容します。これは仮定ではなく確認しました。
- すべての insert が列を明示的に指定しているので、追加された列が古い writer にとって位置依存に なることはありません
- パッケージ内に
SELECT *は存在しないので、予期しないフィールドを受け取る reader はいません - 走っているビルドより新しいスタンプの付いたデータベースは、警告を出して続行します (bug-138)。 よってダウングレードは起動拒否ではなく縮退になります
したがってロールバックしても、列はそのまま残り、読まれません。これはロールバック 耐性であって、ロールバックフリーとは違います。依然としてスキーママイグレーション であり、リリース標準は、どれほど穏やかに見えてもそれをプレリリースの梯子へ送ります。
9. テスト¶
上の主張は、それを支える検査の分だけしか価値がありません。
sourceの中にoriginキーを送った呼び出し元が、保存されるorigin列に影響しないこと — §3 が実測した偽造経路を、直接アサートする。- 解決可能な principal を持つ呼び出しは client を記録し、持たない呼び出しは
{}を記録する。 - 検証済み subject は alias を記録し、生のクレーム値は決して記録しない — alias の一致で アサートし、加えて直列化された行に生の subject 文字列が存在しないことを別途アサートする。
recall/recall_with_contextの応答にoriginキーが含まれないこと。これにより §6 が 慣習ではなくゲートになる。- import は、取り込まれたペイロード内の値ではなく、取り込みを行った呼び出し元の origin を書く。
- マイグレーション前に作られたデータベースが開き、マイグレーションされ、
originが{}の 行を返す。