#!/usr/bin/env python3"""agent.md の frontmatter を厳格に検証する lint。未知キーは黙って無視されるため、権限に関わるキーの綴り誤りは「エラーにならず既定値へフォールバックする」形で通過してしまう。CI 側で明示的に弾く。"""import sys, os, glob, difflib, timeimport yamlKNOWN_KEYS = { "mainAgent": bool, "subagent": bool, "hidden": bool, "inheritMcp": bool, "commandExecutionPolicy": str, "model": str,}# 未指定・綴り誤り時に実際に適用される値。# バージョンによって変わるため、ここを自分の環境に合わせて更新する。DEFAULTS = { "mainAgent": False, "subagent": False, "hidden": False, "inheritMcp": True, "commandExecutionPolicy": "ask", "model": "inherit",}POLICY_VALUES = {"ask", "allow", "deny"}SECURITY_KEYS = {"inheritMcp", "commandExecutionPolicy", "hidden"}RISK = {"allow": 2, "ask": 1, "deny": 0}def split_frontmatter(text): if not text.startswith("---"): return None, text, "frontmatter がありません" end = text.find("\n---", 3) if end == -1: return None, text, "frontmatter が閉じられていません" try: data = yaml.safe_load(text[3:end]) except yaml.YAMLError as e: return None, text[end + 4:], f"YAML パースエラー: {e}" if data is None: data = {} if not isinstance(data, dict): return None, text[end + 4:], "frontmatter が map ではありません" return data, text[end + 4:], None
続いて、1ファイル分の検査本体です。ここが lint の中心になります。
def lint_file(path): findings = [] with open(path, encoding="utf-8") as f: text = f.read() data, body, err = split_frontmatter(text) if err: return [("ERROR", err)], {} for key, value in data.items(): if key in KNOWN_KEYS: expected = KNOWN_KEYS[key] if not isinstance(value, expected): findings.append( ("ERROR", f"{key}: 型が {expected.__name__} ではありません({value!r})")) continue # 既知キーとの距離で綴り誤りを推定する。 # cutoff=0.6 は inherit_mcp / inheritMCP を拾い、 # 無関係な memo や owner を拾わない値として選んだ。 near = difflib.get_close_matches(key, KNOWN_KEYS, n=1, cutoff=0.6) if near: suggestion = near[0] level = "ERROR" if suggestion in SECURITY_KEYS else "WARN" findings.append((level, f"未知キー {key!r} -> {suggestion!r} の綴り誤りの可能性。" f"現状 {suggestion}={DEFAULTS[suggestion]!r} が適用されます")) else: findings.append(("WARN", f"未知キー {key!r}(無視されます)")) policy = data.get("commandExecutionPolicy") if policy is not None and policy not in POLICY_VALUES: findings.append(("ERROR", f"commandExecutionPolicy: 未知の値 {policy!r}。" f"許容値は {sorted(POLICY_VALUES)}。既定の 'ask' へ落ちます")) policy = None if "# " not in body: findings.append(("WARN", "H1 見出しがありません。システムプロンプトの区切りを確認してください")) # 実効値を組み立て直す。型が違うキーは採用しない。 effective = dict(DEFAULTS) for k, v in data.items(): if k in KNOWN_KEYS and isinstance(v, KNOWN_KEYS[k]): effective[k] = v if policy is None: effective["commandExecutionPolicy"] = DEFAULTS["commandExecutionPolicy"] if effective["commandExecutionPolicy"] == "allow" and effective["inheritMcp"]: findings.append(("ERROR", "commandExecutionPolicy=allow と inheritMcp=true の同時指定。" "承認なしで継承済み MCP ツールを呼び出せます")) return findings, effective