コンテンツにスキップ

変更を適用する前に AI の提案を検証する

変更を適用する前に AI の提案を検証する

Section titled “変更を適用する前に AI の提案を検証する”

パッケージをインストールします。機能別のサブパスはインポート先として使用します。

ターミナルウィンドウ
bun add @alneas/appkit-core

@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 のツール引数を組み立てます。どちらのトランスポートもデータを返しますが、ツールの実行、アクションの承認、サーバー権限の付与は行いません。

  1. 取得したテキスト、アップロード、アシスタントのメッセージ、ツールの出力は信頼できないデータとして扱ってください。その中の指示を、信頼されたメッセージ、プロバイダーのパラメーター、エンドポイント設定に昇格させないでください。
  2. セッションで明示的に公開されたツールから、完全な呼び出しエンベロープだけを受け入れてください。提案を表示する前に、JSON スキーマと型付き入力パーサーで検証します。余分なプロパティを想定しない場合は、厳格なオブジェクトスキーマを使用してください。
  3. ツール、引数、対象リソース、作用の不変スナップショットを事前表示してください。承認は正確な呼び出し、認証済みセッション、現在のアクション登録に結び付けます。呼び出しや登録が変わった場合は、新たな承認が必要です。
  4. 書き込み、送信、削除、機密情報の開示には、アプリケーションの明示的な承認を必須にしてください。アシスタントのメッセージ、SDK のツール出力イベント、確認 UI は、承認の証明にはなりません。読み取り専用の処理には、適用範囲を限定した事前承認済みのアプリケーションポリシーを使えます。
  5. 実行時にサーバーが、本人性、テナントとリソースの権限、業務ルール、並行処理、永続的な冪等性を再確認します。ブラウザー上の承認、スキーマ、メモリ内での重複呼び出しの拒否は、サーバー側の認可ではありません。
  6. 固定された登録済みハンドラーだけを実行してください。開示前に mapOutput で出力を検証・射影します。生の原因情報を開示したり、射影が失敗したときに生の出力を使用したりしないでください。
  7. キャンセルを伝播し、結果が不確かな場合は実際の状態を照合してください。キャンセルは作用をロールバックしません。書き込みの結果を確認せずに再試行しないでください。

ブリッジは、明示的なツール公開、同期的なスキーマ検査、上限のある 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();},
};
}