ガードが素通りする書き方
検査は、通ったときの見た目が「何も起きなかった」になる。だから壊れていても気づけない。エージェントの手前に置いたガードで、これが1日に3回起きた。
ここには、その3つの再現と直し方、ガードそのものを壊して確かめる手順を置く。経緯は開発日記のAIに鍵を渡さず、合図1枚でmainへにある。
1 grepへパイプして、終了コードを失う
監査コマンドの出力をgrepへ渡し、||で「指摘なし」と表示する書き方だ。監査コマンドが失敗した、という想定で再現する。
audit() { echo "audit: 内部エラーで異常終了" >&2; return 2; }
# [A] 素通りする書き方
audit 2>&1 | grep -i critical || echo "指摘なし"
# [B] pipefailを付けても同じ
( set -o pipefail; audit 2>&1 | grep -i critical || echo "指摘なし" )
# [C] 出力と終了コードを分ける
out=$(audit 2>&1); rc=$?
if [ "$rc" -ne 0 ]; then
echo "監査の実行に失敗(終了コード $rc)。ここで止める"
elif printf '%s\n' "$out" | grep -qi critical; then
echo "critical あり。止める"
else
echo "指摘なし"
fi
それぞれが表示したものは次のとおり(bash 3.2.57、macOS標準)。
[A] -> 指摘なし
[B] -> 指摘なし
[C] -> 監査の実行に失敗(終了コード 2)。ここで止める
分かること。
||が判定しているのは、grepの終了コードだ。 1は「一致なし」の意味で、監査コマンドの成否は見ていないset -o pipefailでも直らない。 パイプラインの終了コードは、最後に失敗したコマンドのものになる。ここでは右端のgrepの1が残るので、「一致なし」と「監査が落ちた」を区別できない- 直し方は、出力と終了コードを分けて受けて、終了コードを先に見る。 検査結果を変数に取り、失敗なら止める
- 検査は、反映より前に置く。 この日の実例では、監査より先にGatewayを再起動していた。反映してから検査すると、検査が止めても反映は済んでいる
同じ系統の書き方(|| trueで失敗を捨てる)は、全スクリプトを探すと合計3か所あった。うち1つ(sandbox explain)を「失敗したら止める」形に直した直後に、その場で実在の問題を捕まえた。複数エージェント構成ではagentIdの指定が必須で、以前の|| trueのままなら気づかずに通していた。
2 git statusは、新しいディレクトリを畳む
秘密情報らしいファイル名を検査するのに、git status --porcelainの出力を使っていた。一時リポジトリで再現する。
git init -q . && git commit --allow-empty -qm init
mkdir newdir
echo 'S=1' > .env; echo 'S=1' > newdir/.env; echo k > newdir/id_rsa; echo x > top.pem
git status --porcelain
git status --porcelain -uall
出力はこうだった。
?? .env
?? newdir/
?? top.pem
?? .env
?? newdir/.env
?? newdir/id_rsa
?? top.pem
既定では、未追跡のディレクトリがnewdir/の1行に畳まれる。旧い検査は、この出力の最後の列をgrepしていた。
git status --porcelain | awk '{print $NF}' | grep -iE '(^|/)\.env($|\.)|\.pem$|(^|/)id_(rsa|ed25519)' || true
.env
top.pem
.envとtop.pemは検出できて、newdir/.envとnewdir/id_rsaは検出できなかった。直後のgit add -Aは中身をすべてステージするので、そのままcommitまで進み得る。
直し方は、先にステージして、commitされる一覧を調べる形にする。
git add -A
git diff --cached --name-only -z | tr '\0' '\n' | grep -iE '(^|/)\.env($|\.)|\.pem$|(^|/)id_(rsa|ed25519)'
もう1つ、同じ検査に|| trueが付いていて、gitそのものが失敗しても「該当なし」に見えた。 gitリポジトリではない場所で再現する。
set -euo pipefail
SUSPECT=$(git -C ./notrepo status --porcelain 2>/dev/null | awk '{print $NF}' | grep -iE '\.env$' || true)
[ -z "$SUSPECT" ] && echo "該当なし"
該当なし
このスクリプトでは直後のgit add -Aが落ちるので、実害にはならなかった。ただ、表示だけを見ると「検査済み」に見える。gitの出力をいったん変数に取り、|| dieで止めてから、grepにかける。
3 止め方が、動くものに届いていない
自動マージを止める環境変数(AUTO_MERGE=0)を、導入スクリプトの実行時に付けて指定していた。しかし、launchdのジョブは導入時のシェルの環境を引き継がない。plistのEnvironmentVariablesに書いた変数だけが届く。
導入スクリプトを、HOMEを一時ディレクトリ、launchctlを何もしないスタブに差し替えて実行し、生成されたplistの環境変数を読んだ。
| 導入コマンド | plistの環境変数 |
|---|---|
旧: AUTO_MERGE=0を付けて導入 | PATH HOME のみ |
新: AUTO_MERGE=0を付けて導入 | PATH HOME WS AUTO_MERGE=0 |
| 新: 指定なし | AUTO_MERGE=1 |
旧版では、止めたつもりで、ジョブは常に既定値(マージまで進む)で動いていた。これは検査ではなく止める手段の話だが、書いたことと、動くものに届いていることは別だという点で同じ型だ。
なお、確かめたのは生成されるplistまでで、実際のlaunchdに登録して、合図を置いてPR作成で止まるところまでは実機で通していない。
ガードを壊して確かめる手順
3つに共通するのは、動いていることしか見ておらず、止めるべきものを止めることを確かめていなかった点だ。確かめるときの手順を置く。
- ガードごとに、通してはいけないものを実物で用意する。秘密情報の検査なら、
.envと鍵のファイル - それを素直な置き場所と、外れた置き場所の両方に置く。外れた置き場所は、新規ディレクトリの中、深い階層、日本語や空白を含むパス
- ガードの内側のコマンドを、わざと失敗させる。存在しないディレクトリ、壊れたリポジトリ。「該当なし」「指摘なし」と出たら素通り
- 検査のブロックは、実ファイルから切り出して実行する。書き写したものを試すと、元のコードとは別物を試すことになる
- 止める手段は、生成された設定を読むだけでなく、実際に止まるところまで通す
手順4の切り出しは、次のようにした。
F=scripts/host/push-from-host.sh
START=$(grep -n '^# --- 秘密情報の混入検査' $F | cut -d: -f1)
END=$(grep -n '^if \[ "\$ASSUME_YES"' $F | cut -d: -f1)
sed -n "${START},$((END-1))p" $F > block.sh
cat > harness.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
die() { echo "エラー: $*" >&2; exit 1; }
say() { printf '\n== %s\n' "$*"; }
WT="$1"
source "$(dirname "$0")/block.sh"
echo "PASS-THROUGH: 検査を通過した"
EOF
bash harness.sh ./t1 # 一時リポジトリ t1 に対して実行する
この日は、次の7ケースで確かめた。
| ケース | 期待 |
|---|---|
新規ディレクトリ内の.env | 中止し、ステージを戻す |
リポジトリ直下の.pem | 中止 |
深い階層のid_ed25519 | 中止 |
日本語・空白を含むパスの.env | 中止 |
| 無害な新規ディレクトリ | 通過 |
| 日本語・空白を含む無害なパス | 通過 |
| gitリポジトリではない場所 | 中止(終了コード128) |
すべて期待どおりだった。
コードレビューで見る所
|| trueや|| echoの左側に、検査コマンドそのものが置かれていないか- 判定が「出力が空か」だけで、コマンド自体の成否を見ていないか
- 検査より前に、反映(再起動・commit・push)が済んでいないか
- 検査が見ているのは、実際に外へ出る一覧か。表示用の出力ではないか
- 設定を書いただけでなく、それが動くものに届いているか。launchdのジョブは、導入時の環境を引き継がない
関連
- 開発日記: AIに鍵を渡さず、合図1枚でmainへ — 3つを見つけた1日の記録
- 鍵を渡さないpush設計 — ガードが守っている設計と、launchdの起動の挙動