記録されたアクセス元 (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が{}の 行を返す。