7月1日に Apple が新しいインテリジェンス・フレームワークと Xcode 27 を発表し、App Intents 経由でアプリの操作を Siri のアシスタント機能に接続できる範囲が広がりました。ちょうどその週、Antigravity に「壁紙を今日のおすすめに一括入れ替えするショートカットを追加して」と頼んで生成された Swift を眺めていて、少し背筋が冷えました。生成された PerformIntent は、確認も挟まずにユーザーの保存済みコレクションを丸ごと差し替える実装だったのです。
自分が手で叩くぶんには問題ありません。困るのは、この Intent が Siri やアシスタントから、私が画面を見ていない状態で音声一つで発火し得るという点です。エージェントが書いたコードは動きます。動くからこそ、副作用の重さに無頓着なまま「音声で叩ける操作」になってしまう。ここではその落とし穴を、副作用による分類と確認ゲートで塞ぐ設計をご紹介します。
自分で叩く Intent と、アシスタントに叩かせる Intent は別物です
App Intents(iOS 16 以降)は Swift のプロトコルに準拠するだけで、Siri・ショートカット・Spotlight・そして新しいアシスタント機能から呼び出せるようになります。実装のハードルが下がったぶん、見落とされがちなのが「誰が、どんな文脈で起動するか」です。
// scripts/check-intent-gates.mjsimport { readFileSync, readdirSync } from "node:fs";import { join } from "node:path";function swiftFiles(dir) { return readdirSync(dir, { withFileTypes: true }).flatMap((e) => { const p = join(dir, e.name); if (e.isDirectory()) return swiftFiles(p); return e.name.endsWith(".swift") ? [p] : []; });}// 雑な struct 分割。厳密なパーサではないが、ゲート欠落の検出には十分function structs(src) { const out = []; const re = /struct\s+(\w+)\s*:\s*[^{]*ClassifiedIntent[^{]*\{/g; let m; while ((m = re.exec(src))) { let depth = 1, i = re.lastIndex; for (; i < src.length && depth > 0; i++) { if (src[i] === "{") depth++; else if (src[i] === "}") depth--; } out.push({ name: m[1], body: src.slice(m.index, i) }); } return out;}const violations = [];for (const file of swiftFiles("Sources")) { const src = readFileSync(file, "utf8"); for (const s of structs(src)) { const irreversible = /sideEffect\s*:\s*SideEffect\s*=\s*\.irreversible/.test(s.body); if (!irreversible) continue; const gated = /requestConfirmation\s*\(/.test(s.body) || /openAppWhenRun\s*(:\s*Bool)?\s*=\s*true/.test(s.body); if (!gated) { violations.push(`${file}: ${s.name} は不可逆分類ですが確認ゲートがありません`); } }}if (violations.length) { console.error("❌ 確認ゲート欠落:\n" + violations.join("\n")); process.exit(1);}console.log("✅ すべての不可逆 Intent に確認ゲートがあります");
これを CI の一段に入れておけば、エージェントが追加した Intent がゲート無しでマージされる経路を塞げます。厳密な Swift パーサではないので取りこぼしはあり得ますが、「不可逆分類の宣言はあるのに確認が無い」という最も危険なパターンは確実に捕まえます。私の運用では、このスクリプトを無人ランのビルド前段に置き、exit 1 でそのラン全体を止めるようにしています。