T-CREATOR

Codex を安全に使うアーキテクチャ設計:境界づけ・サンドボックス・権限分離

Codex を安全に使うアーキテクチャ設計:境界づけ・サンドボックス・権限分離

OpenAI Codex は強力なコード生成 AI ですが、実際のプロダクション環境で安全に活用するには、適切なアーキテクチャ設計が欠かせません。

本記事では、Codex が生成するコードを安全に実行するための 3 つの重要な設計原則——境界づけ(Boundary)サンドボックス(Sandbox)権限分離(Privilege Separation)——について、実装例とともに詳しく解説いたします。これらの原則を正しく理解し実装することで、AI 生成コードのリスクを大幅に低減できるでしょう。

背景

Codex とは

OpenAI Codex は、自然言語から高品質なプログラムコードを生成できる AI モデルです。GitHub Copilot の基盤技術としても知られており、開発者の生産性を飛躍的に向上させる可能性を秘めています。

しかし、AI が生成したコードをそのまま本番環境で実行すると、予期しない動作やセキュリティリスクが発生する恐れがあります。特に以下のような懸念が存在するのです。

AI 生成コードの特性

Codex が生成するコードには、以下のような特徴があります。

  • 予測困難性: 同じプロンプトでも毎回異なるコードが生成される場合がある
  • 完全性の欠如: 生成されたコードに論理エラーやセキュリティホールが含まれる可能性
  • 制御の難しさ: AI 自体の動作を完全に制御することは難しい

これらの特性を理解した上で、安全なアーキテクチャを構築する必要があります。

次の図は、Codex を利用したアプリケーションの基本的な構成を示しています。

mermaidflowchart TD
    user["ユーザー"] -->|"プロンプト入力"| app["アプリケーション<br/>Layer"]
    app -->|"API呼び出し"| codex["OpenAI Codex<br/>API"]
    codex -->|"コード生成"| app
    app -->|"コード実行"| exec["実行環境"]
    exec -->|"結果"| app
    app -->|"出力"| user

このような構成では、生成されたコードが直接実行環境で動作するため、適切な安全対策が必要になります。

セキュリティの重要性

AI 生成コードを無制限に実行すると、以下のようなリスクが考えられます。

#リスク分類具体例影響度
1リソース枯渇無限ループ、メモリリーク★★★
2データ漏洩機密情報へのアクセス★★★
3システム破壊ファイル削除、設定変更★★★
4外部攻撃不正なネットワーク通信★★☆

これらのリスクを軽減するために、本記事で紹介する 3 つの設計原則が重要な役割を果たします。

課題

Codex 利用における主要な課題

Codex を実際のシステムに組み込む際、開発者が直面する主な課題は以下の 3 点です。

1. 生成コードの信頼性担保

AI が生成したコードは必ずしも意図通りに動作するとは限りません。

開発者が期待する処理を正確に実行するかどうかを、どのように検証すればよいのでしょうか。人間が書いたコードであれば、レビューやテストで品質を担保できますが、AI 生成コードは毎回異なる可能性があるため、従来の方法だけでは不十分です。

2. 実行環境の保護

生成されたコードが本番環境で予期しない動作をした場合、システム全体に影響が及ぶ可能性があります。

たとえば、無限ループによる CPU 使用率の急上昇や、ファイルシステムへの不正アクセスなどが考えられるでしょう。こうした事態を防ぐには、実行環境そのものを隔離する仕組みが必要になります。

3. 権限の適切な制限

Codex が生成するコードに、システムの全権限を与えることは極めて危険です。

必要最小限の権限のみを付与し、それ以外の操作は一切許可しないという原則を、どのように実装すればよいのでしょうか。権限管理は、セキュリティの基本でありながら、AI 生成コードに対しては特に慎重な設計が求められます。

次の図は、これらの課題がどのように関連しているかを示しています。

mermaidflowchart TB
    challenge1["課題1<br/>生成コードの<br/>信頼性担保"]
    challenge2["課題2<br/>実行環境の<br/>保護"]
    challenge3["課題3<br/>権限の<br/>適切な制限"]

    challenge1 -->|"検証不足"| risk1["予期しない<br/>動作"]
    challenge2 -->|"隔離不足"| risk2["システム<br/>全体への影響"]
    challenge3 -->|"過剰な権限"| risk3["不正アクセス"]

    risk1 --> incident["セキュリティ<br/>インシデント"]
    risk2 --> incident
    risk3 --> incident

これら 3 つの課題は相互に関連しており、包括的な対策が必要です。

解決策

これらの課題に対して、3 つの設計原則——境界づけサンドボックス権限分離——を適用することで、安全な Codex 利用環境を構築できます。

1. 境界づけ(Boundary)

境界づけとは、AI 生成コードが実行できる範囲を明確に定義し、その境界を越えた操作を防ぐ設計手法です。

境界づけの基本概念

境界づけには以下の 3 つのレイヤーがあります。

#レイヤー目的実装方法
1入力境界不正なプロンプトの検出バリデーション、フィルタリング
2出力境界危険なコードの検出静的解析、パターンマッチング
3実行境界許可された操作のみ実行ホワイトリスト方式

これらの境界を多層的に設けることで、リスクを段階的に低減できるでしょう。

次の図は、境界づけの概念を視覚化したものです。

mermaidflowchart LR
    input["ユーザー<br/>入力"] -->|"検証"| boundary1["入力境界<br/>バリデーション"]
    boundary1 -->|"許可"| codex["Codex<br/>API"]
    codex -->|"生成"| code["生成<br/>コード"]
    code -->|"検査"| boundary2["出力境界<br/>静的解析"]
    boundary2 -->|"安全"| boundary3["実行境界<br/>ホワイトリスト"]
    boundary3 -->|"実行"| result["実行<br/>結果"]

    boundary1 -.->|"拒否"| reject1["エラー"]
    boundary2 -.->|"危険"| reject2["エラー"]
    boundary3 -.->|"不許可"| reject3["エラー"]

各境界で異なる検証を行うことで、多段階の防御を実現しています。

2. サンドボックス(Sandbox)

サンドボックスは、生成されたコードを隔離された環境で実行する技術です。

たとえコードに問題があっても、ホストシステムには一切影響を与えないようにします。

サンドボックスの実装方式

サンドボックスには複数の実装方式があり、要求されるセキュリティレベルに応じて選択できます。

#方式隔離レベルパフォーマンス実装の複雑さ
1プロセス分離★☆☆★★★★☆☆
2コンテナ(Docker)★★☆★★☆★★☆
3仮想マシン★★★★☆☆★★★
4WebAssembly★★☆★★★★★☆

実際のプロダクション環境では、コンテナベースのサンドボックスが、セキュリティとパフォーマンスのバランスが取れた選択肢となるでしょう。

次の図は、サンドボックスの動作原理を示しています。

mermaidflowchart TB
    subgraph host["ホストシステム"]
        app["アプリケーション"]
        monitor["監視<br/>プロセス"]
    end

    subgraph sandbox["サンドボックス環境"]
        runtime["実行<br/>ランタイム"]
        generated["生成<br/>コード"]
        resources["制限された<br/>リソース"]
    end

    app -->|"コード送信"| runtime
    runtime -->|"実行"| generated
    generated -.->|"アクセス試行"| resources
    monitor -->|"監視"| sandbox
    runtime -->|"結果"| app

    generated -.->|"遮断"| blocked["外部システム<br/>(アクセス不可)"]

サンドボックス内で実行されるコードは、外部システムにアクセスできないため、安全性が大幅に向上します。

3. 権限分離(Privilege Separation)

権限分離は、最小権限の原則に基づき、生成コードに必要最小限の権限のみを付与する設計手法です。

権限分離の設計原則

権限分離を実現するには、以下の 3 つの原則を守る必要があります。

最小権限の原則(Principle of Least Privilege)

各コンポーネントには、その機能を果たすために必要な最小限の権限のみを与えます。たとえば、データを読み取るだけの処理には、書き込み権限を一切付与しません。

職務分離(Separation of Duties)

重要な操作は、複数の独立したコンポーネントに分割します。これにより、単一のコンポーネントが侵害されても、システム全体への影響を最小限に抑えられるでしょう。

動的権限付与(Dynamic Privilege Assignment)

権限は実行時に動的に付与し、処理が完了したら直ちに取り消します。常に権限を保持する必要はありません。

次の図は、権限分離のアーキテクチャを表現しています。

mermaidflowchart TB
    subgraph privilege_high["高権限レイヤー"]
        auth["認証<br/>サービス"]
        policy["ポリシー<br/>管理"]
    end

    subgraph privilege_medium["中権限レイヤー"]
        orchestrator["オーケストレーター"]
        validator["検証<br/>エンジン"]
    end

    subgraph privilege_low["低権限レイヤー"]
        executor["実行<br/>エンジン"]
        generated_code["生成<br/>コード"]
    end

    auth -->|"認証トークン"| orchestrator
    policy -->|"権限ポリシー"| validator
    orchestrator -->|"実行指示"| executor
    validator -->|"検証済み"| executor
    executor -->|"最小権限で実行"| generated_code

このように権限を階層化することで、生成コードは最も低い権限で実行され、重要な操作には上位レイヤーの承認が必要になります。

統合アーキテクチャ

3 つの原則を組み合わせた統合アーキテクチャは、以下のように構成されます。

mermaidflowchart TD
    user["ユーザー"] -->|"プロンプト"| boundary_in["入力境界<br/>検証"]
    boundary_in -->|"検証済み"| codex_api["Codex API"]
    codex_api -->|"生成コード"| boundary_out["出力境界<br/>静的解析"]

    boundary_out -->|"安全"| auth_layer["権限レイヤー<br/>トークン発行"]
    auth_layer -->|"制限付きトークン"| sandbox_manager["サンドボックス<br/>マネージャー"]

    sandbox_manager -->|"起動"| sandbox_exec["サンドボックス<br/>実行環境"]
    sandbox_exec -->|"制限付き実行"| generated_code["生成コード<br/>実行"]

    generated_code -->|"結果"| result_validator["結果検証"]
    result_validator -->|"検証済み結果"| user

    sandbox_manager -.->|"監視"| monitor["監視<br/>システム"]
    monitor -.->|"異常検知"| alert["アラート"]

この統合アーキテクチャにより、境界づけ、サンドボックス、権限分離が連携して機能し、多層防御を実現できます。

具体例

ここからは、Node.js と Docker を使った実際の実装例を段階的に解説いたします。

プロジェクト構成

まず、プロジェクト全体の構成を確認しましょう。

bashcodex-secure-executor/
├── src/
│   ├── boundary/        # 境界づけ関連
│   ├── sandbox/         # サンドボックス関連
│   ├── privilege/       # 権限分離関連
│   └── orchestrator/    # 全体統制
├── docker/
│   └── Dockerfile       # サンドボックス用
└── package.json

この構成により、各原則が独立したモジュールとして実装され、保守性が向上します。

1. 境界づけの実装

入力境界の検証

ユーザーからのプロンプトを検証し、危険なパターンを検出します。

typescript// src/boundary/input-validator.ts

import { z } from 'zod';

/**
 * プロンプトのスキーマ定義
 * 文字数制限と禁止文字列のチェック
 */
const promptSchema = z.object({
  prompt: z
    .string()
    .min(1, 'プロンプトは1文字以上必要です')
    .max(5000, 'プロンプトは5000文字以内にしてください'),
  language: z.enum(['javascript', 'typescript', 'python']),
  context: z.string().optional(),
});

export type PromptInput = z.infer<typeof promptSchema>;

スキーマ定義により、型安全性と実行時検証を同時に実現しています。

次に、危険なパターンを検出するフィルターを実装します。

typescript/**
 * 危険なパターンのリスト
 * システムコマンドやファイル操作などを検出
 */
const DANGEROUS_PATTERNS = [
  /exec\s*\(/gi, // コマンド実行
  /spawn\s*\(/gi, // プロセス生成
  /eval\s*\(/gi, // eval関数
  /rm\s+-rf/gi, // ファイル削除
  /\.env/gi, // 環境変数ファイル
  /process\.env/gi, // 環境変数アクセス
];

これらのパターンは、一般的な攻撃手法を網羅しています。

実際の検証ロジックは以下のように実装します。

typescript/**
 * 入力プロンプトの検証クラス
 */
export class InputValidator {
  /**
   * プロンプトの安全性を検証
   * @param input - 検証対象の入力
   * @returns 検証結果
   */
  validate(input: unknown): {
    success: boolean;
    data?: PromptInput;
    errors?: string[]
  } {
    // スキーマ検証
    const result = promptSchema.safeParse(input);

    if (!result.success) {
      return {
        success: false,
        errors: result.error.issues.map(issue => issue.message)
      };
    }

    // 危険なパターンの検出
    const dangerousPatterns = this.checkDangerousPatterns(
      result.data.prompt
    );

    if (dangerousPatterns.length > 0) {
      return {
        success: false,
        errors: [
          `危険なパターンが検出されました: ${dangerousPatterns.join(', ')}`
        ]
      };
    }

    return { success: true, data: result.data };
  }

検証は段階的に行われ、各段階で問題があれば即座に拒否されます。

typescript  /**
   * 危険なパターンをチェック
   * @param prompt - チェック対象のプロンプト
   * @returns 検出されたパターンのリスト
   */
  private checkDangerousPatterns(prompt: string): string[] {
    const detected: string[] = [];

    for (const pattern of DANGEROUS_PATTERNS) {
      if (pattern.test(prompt)) {
        detected.push(pattern.source);
      }
    }

    return detected;
  }
}

このような多段階検証により、入力境界での防御を強化できるでしょう。

出力境界の検証

Codex が生成したコードを静的に解析し、危険なコードを検出します。

typescript// src/boundary/output-validator.ts

import * as acorn from 'acorn';
import * as walk from 'acorn-walk';

/**
 * 生成コードの出力検証クラス
 */
export class OutputValidator {
  /**
   * 禁止されているNode.jsのモジュール
   */
  private readonly FORBIDDEN_MODULES = [
    'child_process',  // プロセス操作
    'fs',             // ファイルシステム
    'net',            // ネットワーク
    'http',           // HTTP通信
    'https',          // HTTPS通信
  ];

これらのモジュールは、サンドボックス外への影響を持つため禁止しています。

AST(抽象構文木)を解析して、危険な構造を検出します。

typescript  /**
   * 生成されたコードの安全性を検証
   * @param code - 検証対象のコード
   * @returns 検証結果
   */
  validate(code: string): {
    success: boolean;
    errors?: string[]
  } {
    const errors: string[] = [];

    try {
      // コードをASTにパース
      const ast = acorn.parse(code, {
        ecmaVersion: 2022,
        sourceType: 'module'
      });

      // ASTを走査して危険なパターンを検出
      walk.simple(ast, {
        // require文のチェック
        CallExpression: (node: any) => {
          if (node.callee.name === 'require') {
            const moduleName = node.arguments[0]?.value;
            if (this.FORBIDDEN_MODULES.includes(moduleName)) {
              errors.push(
                `禁止モジュール '${moduleName}' の使用が検出されました`
              );
            }
          }
        },

AST ベースの解析により、文字列パターンマッチングよりも精度の高い検出が可能です。

typescript        // import文のチェック
        ImportDeclaration: (node: any) => {
          const moduleName = node.source.value;
          if (this.FORBIDDEN_MODULES.includes(moduleName)) {
            errors.push(
              `禁止モジュール '${moduleName}' のimportが検出されました`
            );
          }
        },

        // eval関数のチェック
        Identifier: (node: any) => {
          if (node.name === 'eval') {
            errors.push('eval関数の使用が検出されました');
          }
        }
      });
    } catch (error) {
      errors.push(`コードのパースに失敗しました: ${error}`);
    }

    return {
      success: errors.length === 0,
      errors: errors.length > 0 ? errors : undefined
    };
  }
}

出力境界での検証により、危険なコードが実行環境に到達する前にブロックできます。

2. サンドボックスの実装

Docker ベースのサンドボックス

まず、サンドボックス用の Dockerfile を作成します。

dockerfile# docker/Dockerfile

# 軽量なNode.jsイメージを使用
FROM node:18-alpine

# セキュリティ強化のため非rootユーザーを作成
RUN addgroup -g 1001 -S sandbox && \
    adduser -u 1001 -S sandbox -G sandbox

非 root ユーザーでの実行は、セキュリティの基本原則です。

必要最小限のパッケージのみをインストールします。

dockerfile# 作業ディレクトリの作成
WORKDIR /workspace

# 必要最小限のパッケージのみインストール
RUN apk add --no-cache dumb-init

# ユーザーを切り替え
USER sandbox

# タイムアウト付きで実行
CMD ["dumb-init", "node", "--max-old-space-size=128", "execute.js"]

メモリ制限を設定することで、リソース枯渇攻撃を防げるでしょう。

サンドボックスマネージャー

Docker コンテナを管理するマネージャークラスを実装します。

typescript// src/sandbox/sandbox-manager.ts

import Docker from 'dockerode';
import { randomUUID } from 'crypto';

/**
 * サンドボックスの設定
 */
interface SandboxConfig {
  memoryLimit: number; // メモリ制限(バイト)
  cpuQuota: number; // CPU使用率制限
  timeout: number; // タイムアウト(ミリ秒)
  networkDisabled: boolean; // ネットワーク無効化
}

設定を構造化することで、環境ごとに異なる制限を適用できます。

typescript/**
 * サンドボックス実行結果
 */
interface ExecutionResult {
  success: boolean;
  output?: string;
  error?: string;
  executionTime: number;
}

実行結果を型定義することで、エラーハンドリングが容易になります。

サンドボックスマネージャーの本体を実装します。

typescript/**
 * Dockerベースのサンドボックスマネージャー
 */
export class SandboxManager {
  private docker: Docker;
  private readonly DEFAULT_CONFIG: SandboxConfig = {
    memoryLimit: 128 * 1024 * 1024,  // 128MB
    cpuQuota: 50000,                  // 50%
    timeout: 5000,                     // 5秒
    networkDisabled: true
  };

  constructor() {
    this.docker = new Docker();
  }

デフォルト設定は厳しめに設定し、必要に応じて緩和します。

コード実行の中核ロジックを実装します。

typescript  /**
   * サンドボックス内でコードを実行
   * @param code - 実行するコード
   * @param config - サンドボックス設定
   * @returns 実行結果
   */
  async execute(
    code: string,
    config: Partial<SandboxConfig> = {}
  ): Promise<ExecutionResult> {
    const finalConfig = { ...this.DEFAULT_CONFIG, ...config };
    const containerId = randomUUID();
    const startTime = Date.now();

    try {
      // コンテナの作成
      const container = await this.createContainer(
        containerId,
        code,
        finalConfig
      );

設定のマージにより、柔軟性と安全性のバランスを取っています。

typescript      // コンテナの起動と実行
      await container.start();

      // タイムアウト付きで実行結果を待機
      const output = await this.waitForResult(
        container,
        finalConfig.timeout
      );

      // コンテナのクリーンアップ
      await this.cleanup(container);

      return {
        success: true,
        output,
        executionTime: Date.now() - startTime
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : String(error),
        executionTime: Date.now() - startTime
      };
    }
  }

try-catch ブロックにより、エラー発生時も確実にクリーンアップが実行されます。

コンテナ作成のヘルパーメソッドを実装します。

typescript  /**
   * サンドボックスコンテナを作成
   * @param id - コンテナID
   * @param code - 実行コード
   * @param config - 設定
   * @returns Dockerコンテナ
   */
  private async createContainer(
    id: string,
    code: string,
    config: SandboxConfig
  ): Promise<Docker.Container> {
    return await this.docker.createContainer({
      Image: 'codex-sandbox:latest',
      name: `sandbox-${id}`,
      HostConfig: {
        Memory: config.memoryLimit,
        CpuQuota: config.cpuQuota,
        NetworkMode: config.networkDisabled ? 'none' : 'bridge',
        ReadonlyRootfs: true,  // ファイルシステムを読み取り専用に
        AutoRemove: true        // 終了後自動削除
      },
      Env: [
        `CODE=${Buffer.from(code).toString('base64')}`  // Base64でコードを渡す
      ]
    });
  }

読み取り専用ファイルシステムにより、永続的な変更を防止できるでしょう。

typescript  /**
   * 実行結果を待機
   * @param container - コンテナ
   * @param timeout - タイムアウト時間
   * @returns 実行結果
   */
  private async waitForResult(
    container: Docker.Container,
    timeout: number
  ): Promise<string> {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        container.kill();
        reject(new Error('実行がタイムアウトしました'));
      }, timeout);

      container.logs({
        follow: true,
        stdout: true,
        stderr: true
      }).then(stream => {
        let output = '';

        stream.on('data', (chunk: Buffer) => {
          output += chunk.toString();
        });

        stream.on('end', () => {
          clearTimeout(timer);
          resolve(output);
        });
      });
    });
  }

タイムアウト機能により、無限ループなどの問題に対処しています。

typescript  /**
   * コンテナをクリーンアップ
   * @param container - クリーンアップ対象のコンテナ
   */
  private async cleanup(container: Docker.Container): Promise<void> {
    try {
      await container.stop();
      await container.remove();
    } catch (error) {
      console.error('クリーンアップに失敗しました:', error);
    }
  }
}

クリーンアップが失敗しても、AutoRemove により最終的には削除されます。

3. 権限分離の実装

権限トークンの生成

実行時に必要最小限の権限を含むトークンを生成します。

typescript// src/privilege/privilege-manager.ts

import * as jwt from 'jsonwebtoken';

/**
 * 権限の種類
 */
enum Permission {
  READ = 'read',
  WRITE = 'write',
  EXECUTE = 'execute',
  NETWORK = 'network',
}

権限を列挙型で定義することで、タイプセーフな権限管理が可能です。

typescript/**
 * 権限トークンのペイロード
 */
interface PrivilegeToken {
  sessionId: string;
  permissions: Permission[];
  expiresAt: number;
  resources: string[]; // アクセス可能なリソース
}

トークンにリソース情報を含めることで、細かい制御が実現できるでしょう。

権限マネージャークラスを実装します。

typescript/**
 * 権限分離を管理するクラス
 */
export class PrivilegeManager {
  private readonly SECRET_KEY: string;

  constructor(secretKey: string) {
    this.SECRET_KEY = secretKey;
  }

  /**
   * 制限付き権限トークンを生成
   * @param permissions - 付与する権限
   * @param resources - アクセス可能なリソース
   * @param ttl - トークンの有効期限(秒)
   * @returns 権限トークン
   */
  generateToken(
    permissions: Permission[],
    resources: string[],
    ttl: number = 60
  ): string {
    const payload: PrivilegeToken = {
      sessionId: randomUUID(),
      permissions,
      resources,
      expiresAt: Date.now() + (ttl * 1000)
    };

    return jwt.sign(payload, this.SECRET_KEY, {
      algorithm: 'HS256',
      expiresIn: ttl
    });
  }

短い有効期限により、トークン漏洩時の被害を最小化しています。

typescript  /**
   * トークンを検証し、権限情報を取得
   * @param token - 検証するトークン
   * @returns 権限情報
   */
  verifyToken(token: string): PrivilegeToken | null {
    try {
      const decoded = jwt.verify(
        token,
        this.SECRET_KEY
      ) as PrivilegeToken;

      // 有効期限の追加チェック
      if (decoded.expiresAt < Date.now()) {
        return null;
      }

      return decoded;
    } catch (error) {
      console.error('トークン検証に失敗しました:', error);
      return null;
    }
  }

二重の有効期限チェックにより、セキュリティを強化しています。

typescript  /**
   * 特定の操作に対する権限をチェック
   * @param token - トークン
   * @param requiredPermission - 必要な権限
   * @param resource - アクセス対象のリソース
   * @returns 権限の有無
   */
  hasPermission(
    token: string,
    requiredPermission: Permission,
    resource: string
  ): boolean {
    const privilege = this.verifyToken(token);

    if (!privilege) {
      return false;
    }

    // 権限チェック
    const hasPermission = privilege.permissions.includes(
      requiredPermission
    );

    // リソースチェック
    const hasResourceAccess = privilege.resources.includes(resource) ||
                              privilege.resources.includes('*');

    return hasPermission && hasResourceAccess;
  }
}

権限とリソースの両方をチェックすることで、きめ細かいアクセス制御が可能です。

4. 統合オーケストレーター

3 つの原則を統合し、全体を調整するオーケストレーターを実装します。

typescript// src/orchestrator/secure-executor.ts

import { InputValidator } from '../boundary/input-validator';
import { OutputValidator } from '../boundary/output-validator';
import { SandboxManager } from '../sandbox/sandbox-manager';
import {
  PrivilegeManager,
  Permission,
} from '../privilege/privilege-manager';

/**
 * 実行リクエスト
 */
interface ExecutionRequest {
  prompt: string;
  language: string;
  userId: string;
}

型定義により、API の契約を明確にしています。

typescript/**
 * 安全なCodex実行のオーケストレーター
 */
export class SecureExecutor {
  private inputValidator: InputValidator;
  private outputValidator: OutputValidator;
  private sandboxManager: SandboxManager;
  private privilegeManager: PrivilegeManager;

  constructor(privilegeSecret: string) {
    this.inputValidator = new InputValidator();
    this.outputValidator = new OutputValidator();
    this.sandboxManager = new SandboxManager();
    this.privilegeManager = new PrivilegeManager(privilegeSecret);
  }

依存性注入により、各コンポーネントの独立性を保っています。

メインの実行ロジックを実装します。

typescript  /**
   * Codexを使用してコードを安全に生成・実行
   * @param request - 実行リクエスト
   * @returns 実行結果
   */
  async execute(request: ExecutionRequest) {
    // ステップ1: 入力境界の検証
    const inputValidation = this.inputValidator.validate({
      prompt: request.prompt,
      language: request.language
    });

    if (!inputValidation.success) {
      return {
        success: false,
        error: '入力検証エラー',
        details: inputValidation.errors
      };
    }

各ステップで検証を行い、問題があれば即座に中断します。

typescript// ステップ2: Codexでコード生成(実際にはOpenAI APIを呼び出す)
const generatedCode = await this.callCodexAPI(
  inputValidation.data!.prompt
);

// ステップ3: 出力境界の検証
const outputValidation =
  this.outputValidator.validate(generatedCode);

if (!outputValidation.success) {
  return {
    success: false,
    error: '出力検証エラー',
    details: outputValidation.errors,
  };
}

生成されたコードも検証することで、多層防御を実現しています。

typescript// ステップ4: 権限トークンの生成
const token = this.privilegeManager.generateToken(
  [Permission.EXECUTE], // 実行権限のみ
  ['sandbox'], // サンドボックス内のリソースのみ
  60 // 1分間有効
);

// ステップ5: サンドボックスでの実行
const executionResult = await this.sandboxManager.execute(
  generatedCode,
  {
    timeout: 5000,
    memoryLimit: 128 * 1024 * 1024,
    networkDisabled: true,
  }
);

最小権限でサンドボックス実行することで、リスクを最小化しています。

typescript    // ステップ6: 結果の返却
    return {
      success: executionResult.success,
      output: executionResult.output,
      error: executionResult.error,
      executionTime: executionResult.executionTime,
      metadata: {
        userId: request.userId,
        timestamp: new Date().toISOString()
      }
    };
  }

実行結果にメタデータを付加することで、監査とデバッグが容易になります。

typescript  /**
   * Codex APIを呼び出す(簡略版)
   * @param prompt - プロンプト
   * @returns 生成されたコード
   */
  private async callCodexAPI(prompt: string): Promise<string> {
    // 実際にはOpenAI APIを呼び出します
    // ここでは簡略化のためダミー実装
    return `
      function example() {
        console.log("Hello from Codex");
        return 42;
      }
      example();
    `;
  }
}

実際の実装では、OpenAI API の適切なエラーハンドリングが必要でしょう。

使用例

最後に、統合システムの使用例を示します。

typescript// 使用例

import { SecureExecutor } from './orchestrator/secure-executor';

async function main() {
  // セキュアエグゼキューターの初期化
  const executor = new SecureExecutor('your-secret-key');

  // 実行リクエスト
  const request = {
    prompt: 'フィボナッチ数列を計算する関数を書いてください',
    language: 'javascript',
    userId: 'user-12345'
  };

シンプルな API により、複雑なセキュリティ機構が隠蔽されています。

typescript  // 安全な実行
  const result = await executor.execute(request);

  if (result.success) {
    console.log('実行成功!');
    console.log('出力:', result.output);
    console.log('実行時間:', result.executionTime, 'ms');
  } else {
    console.error('実行失敗:', result.error);
    console.error('詳細:', result.details);
  }
}

main().catch(console.error);

エラーハンドリングにより、問題が発生しても適切に対処できます。

次の図は、実装した統合システムの実行フローを示しています。

mermaidsequenceDiagram
    participant User as ユーザー
    participant Exec as SecureExecutor
    participant InVal as InputValidator
    participant Codex as Codex API
    participant OutVal as OutputValidator
    participant PrivMgr as PrivilegeManager
    participant SandMgr as SandboxManager

    User->>Exec: execute(request)
    Exec->>InVal: validate(prompt)
    InVal-->>Exec: 検証結果

    alt 入力検証失敗
        Exec-->>User: エラー返却
    end

    Exec->>Codex: コード生成リクエスト
    Codex-->>Exec: 生成コード

    Exec->>OutVal: validate(code)
    OutVal-->>Exec: 検証結果

    alt 出力検証失敗
        Exec-->>User: エラー返却
    end

    Exec->>PrivMgr: generateToken()
    PrivMgr-->>Exec: 制限付きトークン

    Exec->>SandMgr: execute(code, config)
    SandMgr-->>Exec: 実行結果

    Exec-->>User: 最終結果

このシーケンス図により、各コンポーネントの役割と連携が明確になります。

図で理解できる要点

実装例における重要なポイントは以下の通りです。

  • 入力と出力の両方で多段階検証を実施
  • Docker による完全な隔離環境での実行
  • 動的に生成される制限付き権限トークン
  • すべてのコンポーネントが疎結合で独立して機能

まとめ

本記事では、OpenAI Codex を安全に活用するための 3 つの設計原則——境界づけサンドボックス権限分離——について詳しく解説いたしました。

重要なポイント

境界づけにより、入力と出力の両方で危険なパターンを検出し、不正なコードの実行を未然に防ぐことができます。

サンドボックスにより、生成されたコードを完全に隔離された環境で実行し、ホストシステムへの影響を遮断できるでしょう。

権限分離により、最小権限の原則に基づいて必要最小限の権限のみを付与し、セキュリティリスクを最小化できます。

これら 3 つの原則を統合することで、AI 生成コードを本番環境で安全に実行できる堅牢なアーキテクチャを構築できます。

今後の展開

本記事で紹介した実装は基本的なものですが、以下のような拡張が可能です。

  • より高度な静的解析ツール(ESLint、TypeScript Compiler など)の統合
  • 実行結果のキャッシュによるパフォーマンス向上
  • 複数のサンドボックス環境の並列実行
  • より細かい権限管理(ファイル単位、API 単位など)

AI 技術の進化とともに、セキュリティ要件も変化していくでしょう。しかし、本記事で解説した基本原則は、今後も変わらず重要な指針となるはずです。

安全で信頼性の高い AI 活用システムを構築し、開発者の生産性向上とセキュリティの両立を実現してください。

関連リンク