壁紙アプリの素材は、アプリのリポジトリの中にありません。
元画像はクラウド同期のフォルダに置いてあり、派生画像を書き出す先はまた別の場所、配信用のステージングはさらに別の場所です。個人開発で6本のアプリを並行して面倒を見ていると、画像だけをアプリのリポジトリから切り離しておくのが一番楽でした。リポジトリを軽く保てますし、同じ素材を複数のアプリから参照できます。
その構成のまま、素材の仕分けや派生画像の書き出しをエージェントに任せてきました。エージェントから見ると、読む場所も書く場所もワークスペースの外側にあります。
Antigravity CLI 1.1.14 の変更点を読んでいて、手が止まったのはここでした。ワークスペース外のパスへのアクセスは、既定で読み取りのみになります。書き込みは実行モードに応じた認可が必要です。
つまり私の構成では、素材を読むところまでは通り、書き出すところで止まる可能性があります。しかも止まるのは、次にそのタスクが走ったときです。
更新してから気づくと、気づくのが遅い
この手の変更で厄介なのは、壊れ方が静かなことです。
エディタの前に座っているときなら、承認ダイアログが出て「あ、そうか」と分かります。困るのは、夜間に走らせているバッチや、CI から呼んでいる非対話実行のほうです。非対話でターン境界を守る設計については非対話実行でターン境界を守るでも触れましたが、あのとき前提にしていたのは「許可されている操作は通る」ことでした。既定が変わるなら、その前提から確認し直す必要があります。
そこで、更新の前に一度だけやっておく作業を決めました。自分のリポジトリの中から、ワークスペースの外へ書いている箇所を全部数える。
数えるだけです。直すかどうかはその後で決めます。この順番にしたのは、直しながら数えると、直せなかったものを忘れるからです。
書き込み先を洗い出す最小のスクリプト
やりたいことは単純です。リポジトリ内のスクリプトと設定ファイルを走査して、書き込み系の操作を見つけ、その行き先がリポジトリの外を指しているなら報告する。
最初に書いたのはこれくらいの短さでした。
#!/usr/bin/env python3
"""ワークスペース外への書き込みを洗い出す(第1版・リテラルのみ)"""
import os, re, sys
WRITE_PATTERNS = [
(re.compile(r'(?:^|\s)>>?\s*("?)([^\s"\'|;&]+)\1'), "shell redirect"),
(re.compile(r'\b(?:cp|mv|rsync)\b[^\n]*?\s("?)(/[^\s"\']+|\.\.[^\s"\']*)\1\s*$'), "copy dest"),
(re.compile(r'open\(\s*["\']([^"\']+)["\']\s*,\s*["\'][^"\']*[wax][^"\']*["\']'), "python open(w)"),
(re.compile(r'--out(?:put)?[= ]\s*("?)([^\s"\']+)\1'), "cli --out"),
]
SKIP_DIRS = {".git", "node_modules", ".next", "__pycache__"}
TEXT_EXT = {".sh", ".bash", ".py", ".mjs", ".js", ".ts", ".json", ".yml", ".yaml", ".toml"}
def audit(root):
root = os.path.abspath(root)
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
for name in filenames:
if os.path.splitext(name)[1] not in TEXT_EXT:
continue
path = os.path.join(dirpath, name)
lines = open(path, encoding="utf-8", errors="ignore").read().splitlines()
for lineno, line in enumerate(lines, 1):
for pattern, kind in WRITE_PATTERNS:
m = pattern.search(line)
if not m:
continue
raw = m.group(m.lastindex).strip('"\'')
if raw.startswith(("$", "~", "%")):
print(f"UNRESOLVED\t{os.path.relpath(path, root)}:{lineno}\t{kind}\t{raw}")
continue
target = raw if os.path.isabs(raw) else os.path.normpath(os.path.join(dirpath, raw))
if not (target + os.sep).startswith(root + os.sep):
print(f"OUTSIDE\t{os.path.relpath(path, root)}:{lineno}\t{kind}\t{target}")
if __name__ == "__main__":
audit(sys.argv[1] if len(sys.argv) > 1 else ".")TEXT_EXT で拡張子を絞っているのは、画像やバイナリを開いて時間を使わないためです。SKIP_DIRS に node_modules を入れておかないと、依存パッケージのビルドスクリプトが大量に引っかかります。
検証用に、私の構成を小さくなぞった雛形を用意しました。素材の入力が外、派生の出力が相対パス、配信ステージングが外、ログが外、キャッシュがユーザー領域という配置です。
# scripts/build_assets.sh
SRC="/Volumes/Shared/wallpaper-source/20260820"
OUT="../dist/derived"
cp "$SRC"/*.png ./work/
python3 scripts/derive.py --out "$OUT"
rsync -a ./work/ /Users/shared/cdn-staging/
echo "done" >> /var/log/wallpaper/build.log# scripts/derive.py
import json
CACHE = "/Users/shared/agent-cache/derive.json"
def save(data):
with open(CACHE, "w") as f:
json.dump(data, f)
with open("./local_report.txt", "w") as f:
f.write("ok")この2ファイルには、ワークスペースの外へ書く箇所が3つあります。rsync の配信ステージング、>> のログ、open(CACHE, "w") のキャッシュです。
第1版を走らせた結果がこうでした。
UNRESOLVED scripts/build_assets.sh:5 cli --out $OUT
OUTSIDE scripts/build_assets.sh:6 rsync dest /Users/shared/cdn-staging
OUTSIDE scripts/build_assets.sh:7 shell redirect /var/log/wallpaper/build.log
summary: outside=2 unresolved=1
3つのうち2つです。open(CACHE, "w") が丸ごと消えています。