Codexのスキルを作るとき、SKILL.mdに手順を書くだけで、どこまで作業を揃えられるのかが気になっていました。ファイル同士を照合するような処理なら、検査用のコードも一緒に置いた方が、結果を確かめやすそうです。

今回はPythonの検査スクリプトを同梱し、小さなLambdaコードと設定JSONを照合してみました。必須の環境変数を1件だけ設定から外し、検出結果とCodexの説明を記録しています。

スキルに、検査の手順とコードを置く

Codexのスキルは、用途や手順を書いたSKILL.mdと、必要なスクリプトなどをまとめたフォルダーです。公式のBuild skillsにも、scripts/を同梱する構成が載っています。

今回、SKILL.mdには入力の選び方、実行方法、結果の読み方を書きました。変数名の抽出と設定との照合は、Python側が担当します。Codexには、実行結果を根拠に理由と修正案を説明してもらいます。

私が確かめたいのは、この分担で検査を繰り返せるか、という点です。スキルを入れる前後でCodexの性能を比較する実験ではありません。

ローカルに、定義漏れのあるサンプルを用意する

実行環境はmacOSのApple Silicon、Python 3.13.5です。AWSへの接続は行いません。Lambda向けの関数をローカルのファイルとして読み、設定JSONと比べます。

主なファイルは次の配置にしました。

codex-lambda-env-check/
├── .agents/skills/lambda-env-check/
│   ├── SKILL.md
│   └── scripts/check_env.py
├── fixtures/
│   ├── lambda_function.py
│   ├── config-missing.json
│   └── config-fixed.json
├── tests/test_check_env.py
└── evidence/

このフォルダーを作業ディレクトリとして使う構成です。公式資料では、Codexは作業ディレクトリからリポジトリルートまでの.agents/skillsを探します。親の作業ディレクトリから、すべての子フォルダーを再帰的に探す前提にはしていません。

検査対象のlambda_function.pyは、環境変数を読むだけの短いコードです。

import os


def lambda_handler(event, context):
    table_name = os.environ["TABLE_NAME"]
    stage = os.environ["STAGE"]
    log_level = os.getenv("LOG_LEVEL", "INFO")
    # AWS APIは呼ばず、設定を読み取る最小のサンプルです。
    return {"table": table_name, "stage": stage, "log_level": log_level}

TABLE_NAMEとSTAGEは、角括弧で直接参照しています。LOG_LEVELには既定値のINFOを付けました。この違いを検査で分けられるようにします。

修正前の設定は、config-missing.jsonです。STAGEだけを定義し、TABLE_NAMEを意図的に外しました。

{
  "Environment": {
    "Variables": {
      "STAGE": "local"
    }
  }
}

このJSONは、Lambdaの環境変数の公式資料にあるEnvironment.Variablesの形を使っています。AWSから取得した設定ではなく、検証用に作ったものです。SAMやTerraformの設定も、今回は読み取りません。

SKILL.mdには、結果の扱いまで書く

作成したスキル名はlambda-env-checkです。先頭には、用途と対象外の範囲を書きました。

---
name: lambda-env-check
description: Python製Lambdaの環境変数参照とローカルのEnvironment.Variables形式のJSON設定を照合する。定義漏れの検査を頼まれたときに使う。AWSの実環境やSAM・Terraformの評価は対象外。
---

本文では同梱のcheck_env.pyを実行するように指示しています。結果の解釈については、次の区分を設けました。

出力項目扱い
missing_required角括弧で参照するキーが設定にない
missing_nullableget系の参照で、未定義時にNoneになる。必須かは利用箇所を確認する
optional_unsetNone以外の定数の既定値がある。未定義だけで障害とは扱わない
unresolved動的なキーなど、今回のスクリプトでは判定できない

未定義の変数を全部エラーにするかは、少し迷いました。os.getenv("LOG_LEVEL", "INFO")まで設定漏れに数えると、既定値を使うために省略した設定も指摘してしまいます。

一方、既定値なしのos.getenv("KEY")は、キーがないとNoneになります。それが許されるかは、後続のコード次第です。ここは必須とも任意とも決めず、要確認として残しました。Pythonのos.getenvの仕様に沿って区別しています。

スキルには、設定値そのものを説明へ転載しないことも書きました。今回必要なのはキー名と参照位置なので、スクリプトも値を出力しません。

Pythonでは、対象コードを実行せずに読む

検査にはPython標準ライブラリのastを使いました。対象ファイルをimportするのではなく、ソースを構文木として読みます。Lambdaのコードを検査しただけで、そこに書かれた処理まで動いてしまうことを避けたかったためです。

実装のうち、入力を読む部分は次の形です。sourceとconfigは、それぞれ引数で受け取ったPathオブジェクトです。

tree = ast.parse(source.read_text(encoding="utf-8"), filename=str(source))
data = json.loads(config.read_text(encoding="utf-8"))

構文木から対応する参照を見つけ、キーが文字列定数なら設定と照合します。次は、キーと参照方法を判別した後の処理の抜粋です。

entry = {"key": key.value, "line": node.lineno, "mode": mode, "defined": key.value in variables}
report["references"].append(entry)
if not entry["defined"]:
    bucket = {"required": "missing_required", "nullable": "missing_nullable", "optional": "optional_unset"}[mode]
    report[bucket].append(entry)

文字列検索だけにしなかったのは、コメント内のos.environ["KEY"]まで拾いたくなかったからです。ただし、ASTを使えばすべて分かるわけではありません。今回対応したのは、通常のimport osを使った直接の参照だけです。

変数名を別の変数から渡す場合や、動的に既定値を決める場合は、無理に推測しないようにしました。

同梱スクリプトを実行すると、5行目を検出した

このCodexのチャットで作成済みのSKILL.mdを明示的に読み込み、そこに書いたコマンドを実行しました。実験フォルダーからのコマンドは次のとおりです。

python3 .agents/skills/lambda-env-check/scripts/check_env.py --source fixtures/lambda_function.py --config fixtures/config-missing.json

終了コードは1でした。出力されたJSONの該当部分は、次の内容です。

"missing_required": [
  {
    "key": "TABLE_NAME",
    "line": 5,
    "mode": "required",
    "defined": false
  }
]

狙ったとおり、5行目のTABLE_NAMEを1件検出しました。6行目のSTAGEは定義済みです。7行目のLOG_LEVELはoptional_unsetに入り、必須の定義漏れには含まれませんでした。

この結果を読んだCodexは、未定義のまま5行目を実行するとKeyErrorになることと、Environment.VariablesにTABLE_NAMEを追加する修正案を説明しました。

ここで実測したのは、定義漏れの検出です。Lambda関数自体は実行していないので、KeyErrorを実際に発生させたという意味ではありません。説明の記録にも、その違いを残しています。

設定に追加すると、必須項目の指摘は消えた

比較用のconfig-fixed.jsonでは、既存のSTAGEを残し、TABLE_NAMEを追加しました。

{
  "Environment": {
    "Variables": {
      "STAGE": "local",
      "TABLE_NAME": "sample-table"
    }
  }
}

同じスクリプトに、修正後の設定を渡します。

python3 .agents/skills/lambda-env-check/scripts/check_env.py --source fixtures/lambda_function.py --config fixtures/config-fixed.json

今度は終了コードが0になり、missing_requiredは空になりました。LOG_LEVELは引き続き未定義ですが、既定値があるので扱いは変わりません。

修正前後の実行済みJSONを表示したローカルレポート。TABLE_NAMEの定義漏れが1件から0件になりました。
修正前後の実行済みJSONを表示したローカルレポート。TABLE_NAMEの定義漏れが1件から0件になりました。

画像は、保存したJSONを読み込むローカルの検証レポートをブラウザーで撮影したものです。CodexやAWSコンソールの画面ではありません。生のJSON、実行コマンド、終了コード、入力ファイルのハッシュも別に保存しています。

検査用のコードにも、境界のテストを付ける

仕込んだ1件が見つかっただけでは、検査スクリプトとして少し心もとないと感じました。コメントや文字列を参照と誤認しないこと、不正なJSONを成功扱いしないこと、対象コードを実行しないことなども確認しています。

7件のテストを実行し、すべて通りました。動的なキーは要確認になることや、設定値が出力に混ざらないことも含めています。

実際のサンプルコードと7件のテスト実行ログを表示したローカルレポート。
実際のサンプルコードと7件のテスト実行ログを表示したローカルレポート。

スキルの形式検査では、別のところで詰まりました。検査スクリプト本体は標準ライブラリだけで動きますが、Codex付属のquick_validate.pyを動かすにはPyYAMLが必要でした。

手元のPythonには入っていなかったため、一時ディレクトリにPyYAML 6.0.3を入れ、形式検査だけに使いました。結果はSkill is valid!、終了コード0です。これはスキルの形式確認であり、検出精度を保証するものではありません。

今回は、小さな照合を再実行できるところまで

試してみて、SKILL.mdに結果の読み方を書き、機械的な照合をスクリプトに任せる形は、自分には整理しやすいと感じました。何を根拠に指摘したのかを、JSONと行番号で追えるためです。

ただし、実用のLambdaをそのまま全部検査できる状態ではありません。別名import、ラッパー関数、別ファイルの参照、条件分岐、実行時の環境変数変更は解析していません。AWSが自動的に用意する環境変数も判別しないため、設定JSONにないという理由だけで漏れと判断できないケースがあります。

また、今回は作成した側がスキルを明示的に読み込んで実行しました。自然な依頼から自動選択されるか、別のCodexセッションでも同じ説明になるかは未検証です。AWS上の設定や権限、Lambdaの起動成功も確認していません。

次は、実際に使っている設定形式に対象を絞り、複数の関数で試したいと思います。対応範囲を広げるときも、判定できなかった参照が結果から消えないようにしておきたいです。

ここまで読んでいただき、ありがとうございます。もしこの記事の技術や考え方に少しでも興味を持っていただけたら、ネクストのエンジニアと気軽に話してみませんか。

  • 選考ではありません
  • 履歴書不要
  • 技術の話が中心
  • 所要時間30分程度
  • オンラインOK

エンジニアと話してみる