ローカル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)
.env や credentials.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行。
Discussion