変更を適用する前に AI の提案を検証する
変更を適用する前に AI の提案を検証する
Section titled “変更を適用する前に AI の提案を検証する”パッケージをインストールします。機能別のサブパスはインポート先として使用します。
bun add @alneas/appkit-core公開インポート
Section titled “公開インポート”@alneas/appkit-core/ai
import {createAjvValidator} from '@alneas/appkit-core/ai';const validator = createAjvValidator({locale:'en'});console.log(typeof validator);この例を使用するアプリケーションで型チェックしてください。より限定されたエントリーポイントと任意のピア依存の正確な要件は、エクスポートリファレンスを参照してください。パッケージのインポート検査や型チェックは、ホストされたプロバイダーでの動作を保証するものではありません。
トラブルシューティングとよくある問題
Section titled “トラブルシューティングとよくある問題”構造化出力の検証と提案確認の契約を提供します。メタデータの検査だけでは、AI コーディングエージェントが任意の要求から適切な API を選べることは証明できません。コーディングエージェントによる一連のタスク評価は未検証です。最終的な書き込みの認可と実行は、アプリケーションが担います。
プロンプトインジェクションと実行の境界
Section titled “プロンプトインジェクションと実行の境界”createAISDKChatAdapter は AI SDK UI Message Stream プロトコルを正規化するもので、SDK のモデルインスタンスを包むラッパーではありません。createOpenAICompatibleChatAdapter は Chat Completions のツール引数を組み立てます。どちらのトランスポートもデータを返しますが、ツールの実行、アクションの承認、サーバー権限の付与は行いません。
- 取得したテキスト、アップロード、アシスタントのメッセージ、ツールの出力は信頼できないデータとして扱ってください。その中の指示を、信頼されたメッセージ、プロバイダーのパラメーター、エンドポイント設定に昇格させないでください。
- セッションで明示的に公開されたツールから、完全な呼び出しエンベロープだけを受け入れてください。提案を表示する前に、JSON スキーマと型付き入力パーサーで検証します。余分なプロパティを想定しない場合は、厳格なオブジェクトスキーマを使用してください。
- ツール、引数、対象リソース、作用の不変スナップショットを事前表示してください。承認は正確な呼び出し、認証済みセッション、現在のアクション登録に結び付けます。呼び出しや登録が変わった場合は、新たな承認が必要です。
- 書き込み、送信、削除、機密情報の開示には、アプリケーションの明示的な承認を必須にしてください。アシスタントのメッセージ、SDK のツール出力イベント、確認 UI は、承認の証明にはなりません。読み取り専用の処理には、適用範囲を限定した事前承認済みのアプリケーションポリシーを使えます。
- 実行時にサーバーが、本人性、テナントとリソースの権限、業務ルール、並行処理、永続的な冪等性を再確認します。ブラウザー上の承認、スキーマ、メモリ内での重複呼び出しの拒否は、サーバー側の認可ではありません。
- 固定された登録済みハンドラーだけを実行してください。開示前に
mapOutputで出力を検証・射影します。生の原因情報を開示したり、射影が失敗したときに生の出力を使用したりしないでください。 - キャンセルを伝播し、結果が不確かな場合は実際の状態を照合してください。キャンセルは作用をロールバックしません。書き込みの結果を確認せずに再試行しないでください。
ブリッジは、明示的なツール公開、同期的なスキーマ検査、上限のある JSON スナップショット、呼び出し回数の制限、安全な出力の射影を適用します。すべてのプロンプトインジェクションを検出したり、サーバー側の認可、永続的な冪等性、アプリケーションの承認を実装したりはしません。プロバイダーの認証情報とアップロード用エンドポイントはサーバー側で管理してください。ファイルのアップロードは、データを開示する明示的な判断です。モデルが生成したコードを評価・実行したり、モデルのテキストから任意の UI コンポーネントを選択したりしないでください。
エラーコードと復旧を参照してください。固定の復旧ヒントは調査と修正の方法を示すもので、実行や自動再試行を認可するものではありません。
アプリケーションでの承認の例
Section titled “アプリケーションでの承認の例”この例は、公開 API を使ったアプリケーションコードです。record.rename ハンドラーと rename_record のブリッジへの結び付けに、同じ厳格なスキーマと必要最小限の出力射影を設定してください。authorizeServer はサーバーを呼び出しますが、最終的なサーバー側ハンドラーでも権限と冪等性を再確認します。SDK の tool-result イベントを実行要求として扱わず、検証済みのツール呼び出しだけをキューに入れてください。
import {createAjvValidator} from '@alneas/appkit-core/ai';import {jsonSnapshot} from '@alneas/appkit-core/ai/validation';import type {ActionRegistry,ActionToolBridge,ActionToolCall,ActionToolResult} from '@alneas/appkit-core/ai';
export interface PendingApproval { readonly approvalId:string;readonly callId:string;readonly recordId:string; readonly name:string;readonly sessionId:string;readonly effect:string;}/** Application example, not a server authorization service or library approval API. */export function createRenameApprovalExample({registry,bridge,sessionId,authorizeServer}:{ registry:ActionRegistry;bridge:ActionToolBridge;sessionId:string; authorizeServer:(snapshot:PendingApproval)=>Promise<boolean>;}){ const validate=createAjvValidator({ajvOptions:{coerceTypes:false,useDefaults:false,removeAdditional:false}}); const schema={type:'object',properties:{recordId:{type:'string',minLength:1},name:{type:'string',minLength:1}},required:['recordId','name'],additionalProperties:false}; let revision=0,sequence=0,closed=false,inFlight:string|null=null; let pending:{view:PendingApproval;call:ActionToolCall;registration:ReturnType<ActionRegistry['getAction']>;revision:number}|null=null; const clear=()=>{pending=null;revision++;}; return { queue(input:unknown):PendingApproval|null { clear();if(closed)return null; try{ const call=jsonSnapshot(input) as unknown as ActionToolCall; if(!call||typeof call.callId!=='string'||!call.callId.trim()||call.name!=='rename_record'||!bridge.listTools().some(t=>t.name===call.name)||!validate(schema,call.arguments).valid)return null; const registration=registry.getAction('record.rename');if(!registration)return null; const args=call.arguments as {recordId:string;name:string}; const view=Object.freeze({approvalId:'approval-'+ ++sequence,callId:call.callId,recordId:args.recordId,name:args.name,sessionId,effect:'Rename this record'}); pending={view,call,registration,revision};return view; }catch{return null;} }, reject(approvalId:string){if(pending?.view.approvalId===approvalId||inFlight===approvalId)clear();}, async approve(approvalId:string):Promise<ActionToolResult|null>{ if(closed||inFlight||!pending||pending.view.approvalId!==approvalId)return null; const selected=pending;pending=null;inFlight=approvalId; try{ if(registry.getAction('record.rename')!==selected.registration||selected.view.sessionId!==sessionId)return null; if(!await authorizeServer(selected.view))return null; if(closed||revision!==selected.revision||selected.view.sessionId!==sessionId||registry.getAction('record.rename')!==selected.registration)return null; // The registered handler must re-check server identity/permissions, concurrency // and durable idempotency. This browser approval is not an authorization token. return await bridge.execute(selected.call); }finally{inFlight=null;} }, setSession(next:string){if(sessionId!==next){sessionId=next;clear();}}, close(){closed=true;clear();}, };}