🛡️

ローカルLLMにWebアプリを自律生成させた — 28秒で完成、セキュリティテスト22件全PASS

に公開

gpt-oss:20bに「アプリ作って」と言ったら28秒で完成した

OpenAIのオープンウェイトモデル gpt-oss:20b。MoEアーキテクチャで、20.9Bパラメータのうち実際に動くのは3.6Bだけ。16GB VRAMのRTX 4080で動く。

こいつに「Todoアプリ作って」と言ったら、1回のAPI呼び出しでindex.htmlを吐き出し、2回目で「できたよ」と返してきた。28秒。

面白い。じゃあもっと難しいのは?

3段階のチャレンジを用意して、gpt-oss:20bがどこまで正確にWebアプリを作れるか検証した。全コードをgpt-ossが書く。人間はハーネス(安全装置)だけ用意する。

3段階チャレンジの結果

Level アプリ 時間 LLM呼び出し トークン スコア
1 Todoアプリ 36秒 2回 1,980 7/8
2 ポモドーロタイマー 12秒 2回 1,240 9/11
3 Markdownノートアプリ 29秒 2回 3,111 11/12

全レベルが2回のAPI呼び出しで完成した。 1回目でHTMLを生成、2回目で「done」を返す。ループも修正もなし。一発完成。

合計77秒、6,331トークン、3本のWebアプリ。

実際に動くデモはこちら → gpt-oss:20b Demos

Level 1: Todoアプリ(36秒)

要件: 追加/削除/完了、フィルター、localStorage、ダークモード、レスポンシブ。

Iteration 1: [LLM] 1,855 tokens, 26.6s → index.html (3,002 bytes)
Iteration 2: [LLM] 125 tokens → Done

良かった点:

  • フィルター(All/Active/Completed)が正しく動作
  • localStorageに保存される
  • ダークモード切替あり
  • CSS変数で色管理

落としたところ:

  • <meta name="viewport"> がない → モバイルで拡大される

スコア: 7/8。viewportだけ忘れた。

Level 2: ポモドーロタイマー(12秒)

要件: 25分/5分サイクル、カウントダウン、SVGプログレスリング、Web Audio APIビープ音、セッションカウンター。

Iteration 1: [LLM] 1,194 tokens, 11.9s → index.html (3,270 bytes)
Iteration 2: [LLM] 46 tokens → Done

12秒。最速。しかもSVGプログレスリングとAudioContextビープ音を両方実装した。

良かった点:

  • setInterval/clearInterval のStart/Pause/Resetが正しく動作
  • SVGのstrokeDashoffsetでリングアニメーション
  • AudioContextで400ms のビープ音
  • prefers-color-scheme: light でシステム設定に追従

落としたところ:

  • viewportメタタグなし
  • localStorageなし(リロードでリセットされる)
  • ダークモードの手動切替なし(システム追従のみ)

スコア: 9/11。機能は全部あるが永続化を省いた。

生成されたSVGプログレスリングのコード:

<svg viewBox="0 0 100 100">
  <circle r="45" cx="50" cy="50" stroke="var(--ring)" fill="none"/>
  <circle id="progress-ring" r="45" cx="50" cy="50" stroke="var(--accent)" fill="none"/>
</svg>
const circumference = 2 * Math.PI * 45;
ring.style.strokeDasharray = circumference;
// タイマーに連動して depletes
const progress = 1 - remaining / duration;
ring.style.strokeDashoffset = circumference * (1 - progress);

これを12秒で書いたのは正直驚いた。

Level 3: Markdownノートアプリ(29秒)

要件: サイドバー付きノート一覧、マークダウンエディタ+ライブプレビュー、検索、エクスポート、Ctrl+S/Ctrl+N、ダークモード、レスポンシブ。

Iteration 1: [LLM] 3,012 tokens, 29.0s → index.html (4,157 bytes)
Iteration 2: [LLM] 99 tokens → Done

最も複雑な要件だったが、こちらも2回で完了。

良かった点:

  • サイドバー + エディタ + プレビューの3ペイン構成
  • マークダウンパーサーを正規表現で自前実装: **太字**, *斜体*, `コード`, [リンク](url), # 見出し
  • Ctrl+Sで保存、Ctrl+Nで新規ノート
  • Blob + URL.createObjectURL で .md ファイルエクスポート
  • 検索フィルター
  • localStorageに全ノート保存
  • モバイルでサイドバーが折りたたまれる

落としたところ:

  • ダークモード切替ボタンなし(常にダーク)

スコア: 11/12。ほぼ完璧。

自前実装のマークダウンパーサー:

function mdToHtml(md) {
  md = md.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
  md = md.replace(/\*(.+?)\*/g, '<i>$1</i>');
  md = md.replace(/`(.+?)`/g, '<code>$1</code>');
  md = md.replace(/```([\s\S]+?)```/g, '<pre>$1</pre>');
  md = md.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2">$1</a>');
  md = md.replace(/\n# (.+)/g, '<h1>$1</h1>');
  return md;
}

プロダクション品質ではないが、デモとしては十分動く。

ハーネスの話 — 安全に動かすために300行書いた

gpt-oss:20bの実力はわかった。ではこれを安全に自律実行させるにはどうするか。

2026年のキーワードは「ハーネスエンジニアリング」。OpenAIがCodexで100万行のコードをエージェントに書かせた実験で提唱した概念で、Anthropicも長時間エージェントの制御方法を公開している。

ハーネス = エージェントを包む安全装置。学術論文だと:

  • Fault-Tolerant Sandboxing(2025.12)— コマンド遮断率100%、ロールバック成功率100%
  • AgentSpec(ICSE 2026採択)— DSLでランタイム制約を定義

今回は論文のアイデアを300行のPythonで実装した。3層構造:

Layer 1: サンドボックス

ファイル書き込みをワークスペース内に制限。パストラバーサルは Path.resolve() で潰す。

def is_path_allowed(filepath: str) -> bool:
    resolved = str(Path(filepath).resolve())
    return any(resolved.startswith(p) for p in ALLOWED_PATHS)

.envcredentials.json はファイル名パターンでブロック。

Layer 2: ポリシーエンジン

コマンドはホワイトリスト制。npm, git, node, ls だけ許可。rm -rf, sudo, curl|bash は禁止パターンで即遮断。

ALLOWED_COMMANDS = [r"^npm ", r"^git ", r"^node ", r"^ls "]
BLOCKED_PATTERNS = [r"rm\s+-rf\s+/", r"sudo\s", r"curl.*\|\s*bash"]

Layer 3: gitトランザクション

各ステップでgit commit。失敗したら git checkout -- . でロールバック。

セキュリティテスト: 22項目全PASS

パストラバーサル、rm -rf /、sudo、リバースシェル、パイプ攻撃、.envアクセス — 全22テストで遮断率 100%

カテゴリ テスト数 結果
サンドボックス(パス制限) 6 6/6 PASS
ポリシーエンジン(コマンド制限) 14 14/14 PASS
アクション解析+連携 2 2/2 PASS

gpt-oss:20bでハマったこと

1. 思考モードで content が空になる

gpt-oss:20bはデフォルトで思考モード有効。思考中は content が空で、thinking フィールドに回答が入る。

resp = requests.post(OLLAMA_URL, json={
    "model": "gpt-oss:20b",
    "think": False,  # これがないと content が空
    ...
})

think: false で非思考モードを明示指定して解決。

2. 長いHTMLをJSONで返すと解析が壊れる

4,000文字のHTMLが content フィールドに入ると、正規表現パーサーでは太刀打ちできない。

解決: 4段階フォールバック。 json.loads(全体) → コードブロック抽出 → { 最初〜 } 最後 → フィールド個別抽出。

3. 「1アクション/1レスポンス」が安定の鍵

複数アクションを返させると解析事故が起きる。「JSONを1個だけ返せ」を徹底したら安定した。

gpt-oss:20bの実力まとめ

項目 評価
単機能HTML生成 強い — Todoもタイマーもノートアプリも一発生成
CSS/レイアウト 良い — CSS変数、flexbox、@mediaを正しく使う
JavaScript 良い — DOM操作、イベント、localStorage、AudioContext
SVG できる — プログレスリングを正しく実装
マークダウンパーサー 簡易だが動く — 正規表現ベースで主要構文をカバー
複数ファイル構成 未検証 — 今回は単一HTMLのみ
viewportメタタグ 忘れがち — 3本中2本で欠落
ダークモード 指示すれば実装するが、優先度が低い

結論: 単一HTMLファイルのWebアプリなら、gpt-oss:20bは実用レベル。 77秒で3本作れる。ハーネスは300行。


GitHubで編集を提案

Discussion