feat(config): validate clients and scope security

This commit is contained in:
2026-09-14 01:07:46 +02:00
parent 5b0c623a8d
commit a3cee2756c
17 changed files with 230 additions and 437 deletions
+38 -7
View File
@@ -12,7 +12,7 @@ $pluginEntries = @(Import-Csv -LiteralPath $pluginManifest -Delimiter ([char]9))
if ($pluginEntries.Count -eq 0) { throw 'Plugin manifest must define at least one plugin.' }
$pluginNames = @()
foreach ($pluginEntry in $pluginEntries) {
foreach ($field in @('name','claude_plugin','codex_plugin')) {
foreach ($field in @('name','claude_plugin','codex_method')) {
if ([string]::IsNullOrWhiteSpace($pluginEntry.$field)) { throw "Missing $field for plugin $($pluginEntry.name)." }
}
if ($pluginNames -contains $pluginEntry.name) { throw "Duplicate plugin name in manifest: $($pluginEntry.name)" }
@@ -52,6 +52,32 @@ function Read-Field($path, $name) {
function Quote-Toml($value) {
return ('"' + $value.Replace('\','\\').Replace('"','\"').Replace("`r",'').Replace("`n",'\n') + '"')
}
function Test-CodexSchema($configPath) {
if (-not (Get-Command codex -ErrorAction SilentlyContinue)) {
if ($env:REQUIRE_CODEX_SCHEMA -eq 'true') { throw 'Codex CLI is required for schema validation.' }
Write-Warning 'Codex CLI unavailable; skipped Codex schema validation.'
return
}
$tempHome = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-schema-" + [Guid]::NewGuid())
$previousCodexHome = $env:CODEX_HOME
try {
New-Item $tempHome -ItemType Directory -Force | Out-Null
Copy-Item $configPath (Join-Path $tempHome 'config.toml') -Force
$env:CODEX_HOME = $tempHome
$previousErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
$schemaOutput = @(& codex --strict-config --help 2>&1)
$schemaExitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
if ($schemaExitCode -ne 0) { throw "Codex schema validation failed: $configPath`n$($schemaOutput -join [Environment]::NewLine)" }
} finally {
$env:CODEX_HOME = $previousCodexHome
Remove-Item $tempHome -Recurse -Force -ErrorAction SilentlyContinue
}
}
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root
@@ -75,6 +101,14 @@ Detect the languages, frameworks, tools, and change areas from the repository an
Load rule files when their subject applies:
- rules/security-auth.md for authentication, authorization, sessions, tokens, permissions, or tenant boundaries.
- rules/security-web.md for browser, frontend, cookie, redirect, XSS, or CSRF work.
- rules/security-api.md for HTTP APIs, request handling, serialization, or endpoints.
- rules/security-data.md for databases, persistence, sensitive data, or multi-tenancy.
- rules/security-files.md for files, uploads, archives, paths, processes, IPC, or deserialization.
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
- rules/angular.md for Angular work.
- rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work.
@@ -154,13 +188,10 @@ foreach ($platform in @('windows','linux')) {
}
$tomlPath = Join-Path $output "codex-$platform/config.toml"
$tomlContent = Get-Content -LiteralPath $tomlPath -Raw
if (([regex]::Matches($tomlContent, '(?<!\\)"')).Count % 2 -ne 0) { throw "Generated Codex config.toml has unbalanced quotes: $tomlPath" }
if (([regex]::Matches($tomlContent, '\[')).Count -ne ([regex]::Matches($tomlContent, '\]')).Count) { throw "Generated Codex config.toml has unbalanced brackets: $tomlPath" }
$settingsPath = Join-Path $output "claude-$platform/settings.json"
try { Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json | Out-Null }
catch { throw "Generated Claude settings.json is not valid JSON: $settingsPath" }
& python (Join-Path $root 'scripts/validate-config.py') $tomlPath $settingsPath
if ($LASTEXITCODE -ne 0) { throw "Generated configuration validation failed for $platform." }
Test-CodexSchema $tomlPath
}
$leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object {
+19 -12
View File
@@ -84,6 +84,14 @@ Detect the languages, frameworks, tools, and change areas from the repository an
Load rule files when their subject applies:
- rules/security-auth.md for authentication, authorization, sessions, tokens, permissions, or tenant boundaries.
- rules/security-web.md for browser, frontend, cookie, redirect, XSS, or CSRF work.
- rules/security-api.md for HTTP APIs, request handling, serialization, or endpoints.
- rules/security-data.md for databases, persistence, sensitive data, or multi-tenancy.
- rules/security-files.md for files, uploads, archives, paths, processes, IPC, or deserialization.
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
- rules/angular.md for Angular work.
- rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work.
@@ -180,19 +188,18 @@ for client in codex claude; do
done
toml_path="$output/codex-$platform/config.toml"
quote_count=$(grep -o '"' "$toml_path" | wc -l)
[ $((quote_count % 2)) -eq 0 ] || { printf 'Generated Codex config.toml has unbalanced quotes: %s\n' "$toml_path" >&2; exit 1; }
open_brackets=$(grep -o '\[' "$toml_path" | wc -l)
close_brackets=$(grep -o '\]' "$toml_path" | wc -l)
[ "$open_brackets" -eq "$close_brackets" ] || { printf 'Generated Codex config.toml has unbalanced brackets: %s\n' "$toml_path" >&2; exit 1; }
settings_path="$output/claude-$platform/settings.json"
if command -v jq >/dev/null 2>&1; then
jq empty "$settings_path" >/dev/null 2>&1 || { printf 'Generated Claude settings.json is not valid JSON: %s\n' "$settings_path" >&2; exit 1; }
elif command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; then
# On Windows, `python3` can resolve to a non-functional Microsoft Store app-execution
# alias that still passes `command -v`; probe it with a real invocation before trusting it.
python3 -c 'import json,sys; json.load(open(sys.argv[1], encoding="utf-8"))' "$settings_path" || { printf 'Generated Claude settings.json is not valid JSON: %s\n' "$settings_path" >&2; exit 1; }
python3 "$root/scripts/validate-config.py" "$toml_path" "$settings_path"
if command -v codex >/dev/null; then
schema_home=$(mktemp -d)
cp "$toml_path" "$schema_home/config.toml"
CODEX_HOME="$schema_home" codex --strict-config --help >/dev/null
rm -rf -- "$schema_home"
elif [ "${REQUIRE_CODEX_SCHEMA:-false}" = true ]; then
printf 'Codex CLI is required for schema validation.\n' >&2
exit 1
else
printf 'WARN Codex CLI unavailable; skipped Codex schema validation.\n' >&2
fi
done
+16 -3
View File
@@ -42,9 +42,16 @@ if (Test-Path -LiteralPath $settingsPath) {
}
$configTomlPath = Join-Path $homePath '.codex/config.toml'
if (Test-Path -LiteralPath $configTomlPath) {
$tomlContent = Get-Content -LiteralPath $configTomlPath -Raw
$balanced = (([regex]::Matches($tomlContent, '(?<!\\)"')).Count % 2 -eq 0) -and (([regex]::Matches($tomlContent, '\[')).Count -eq ([regex]::Matches($tomlContent, '\]')).Count)
if ($balanced) { Result 'PASS' 'installed .codex/config.toml looks structurally valid' } else { Result 'FAIL' 'installed .codex/config.toml has unbalanced quotes or brackets' }
$parseResult = & python -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' $configTomlPath 2>&1
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" }
if (Get-Command codex -ErrorAction SilentlyContinue) {
$previousCodexHome = $env:CODEX_HOME
try {
$env:CODEX_HOME = Split-Path $configTomlPath -Parent
$schemaResult = & codex --strict-config --help 2>&1
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml matches the installed Codex schema' } else { Result 'FAIL' "installed .codex/config.toml has unsupported Codex settings: $schemaResult" }
} finally { $env:CODEX_HOME = $previousCodexHome }
} else { Result 'WARN' 'Codex CLI unavailable; skipped installed Codex schema validation' }
}
foreach ($destination in @((Join-Path $homePath '.codex'), (Join-Path $homePath '.agents/skills'), (Join-Path $homePath '.claude'))) {
@@ -64,6 +71,12 @@ foreach ($destination in @((Join-Path $homePath '.codex'), (Join-Path $homePath
foreach ($path in @('shared/rules','shared/skills','shared/agents','shared/hooks/scripts')) { if (Test-Path (Join-Path $root $path)) { Result 'PASS' "source $path" } else { Result 'FAIL' "missing $path" } }
foreach ($client in @('codex','claude')) {
$config = if ($client -eq 'codex') { Join-Path $root 'adapters/codex/config/config.toml' } else { Join-Path $root 'adapters/claude/config/settings.json' }
$registered = if ($client -eq 'codex') { (Get-Content $config -Raw) -match '\[\[hooks\.Stop\]\]' } else { ((Get-Content $config -Raw | ConvertFrom-Json).hooks.PSObject.Properties.Name -contains 'Stop') }
if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
}
if ($fail) { exit 1 }
Write-Output 'PASS doctor'
exit 0
+6 -6
View File
@@ -55,14 +55,14 @@ if [ -f "$home_path/.claude/settings.json" ]; then
fi
fi
if [ -f "$home_path/.codex/config.toml" ]; then
quote_count=$(grep -o '"' "$home_path/.codex/config.toml" | wc -l)
open_brackets=$(grep -o '\[' "$home_path/.codex/config.toml" | wc -l)
close_brackets=$(grep -o '\]' "$home_path/.codex/config.toml" | wc -l)
if [ $((quote_count % 2)) -eq 0 ] && [ "$open_brackets" -eq "$close_brackets" ]; then
result PASS 'installed .codex/config.toml looks structurally valid'
if python3 -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' "$home_path/.codex/config.toml"; then
result PASS 'installed .codex/config.toml parses as TOML'
else
result FAIL 'installed .codex/config.toml has unbalanced quotes or brackets'
result FAIL 'installed .codex/config.toml is invalid TOML'
fi
if command -v codex >/dev/null; then
if CODEX_HOME="$home_path/.codex" codex --strict-config --help >/dev/null 2>&1; then result PASS 'installed .codex/config.toml matches the installed Codex schema'; else result FAIL 'installed .codex/config.toml has unsupported Codex settings'; fi
else result WARN 'Codex CLI unavailable; skipped installed Codex schema validation'; fi
fi
for destination in "$home_path/.codex" "$home_path/.agents/skills" "$home_path/.claude"; do
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Validate the generated configuration formats without third-party packages."""
from __future__ import annotations
import json
import sys
import tomllib
from pathlib import Path
CLAUDE_TOP_LEVEL = {"permissions", "env", "statusLine", "hooks"}
CLAUDE_PERMISSION_KEYS = {"defaultMode", "allow", "deny"}
CLAUDE_STATUS_LINE_KEYS = {"type", "command"}
CLAUDE_HOOK_KEYS = {"type", "command", "timeout"}
CLAUDE_PERMISSION_MODES = {"acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"}
def fail(message: str) -> None:
raise ValueError(message)
def require_keys(value: object, allowed: set[str], label: str) -> dict[str, object]:
if not isinstance(value, dict):
fail(f"{label} must be an object")
unknown = set(value) - allowed
if unknown:
fail(f"{label} has unknown keys: {', '.join(sorted(unknown))}")
return value
def validate_claude(path: Path) -> None:
settings = require_keys(json.loads(path.read_text(encoding="utf-8-sig")), CLAUDE_TOP_LEVEL, str(path))
permissions = require_keys(settings.get("permissions"), CLAUDE_PERMISSION_KEYS, "permissions")
if permissions.get("defaultMode") not in CLAUDE_PERMISSION_MODES:
fail("permissions.defaultMode is unsupported")
for key in ("allow", "deny"):
if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]):
fail(f"permissions.{key} must be an array of strings")
env = settings.get("env")
if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
fail("env must be an object of string values")
status_line = require_keys(settings.get("statusLine"), CLAUDE_STATUS_LINE_KEYS, "statusLine")
if status_line.get("type") != "command" or not isinstance(status_line.get("command"), str):
fail("statusLine must be a command")
hooks = settings.get("hooks")
if not isinstance(hooks, dict):
fail("hooks must be an object")
for event, matchers in hooks.items():
if not isinstance(event, str) or not isinstance(matchers, list):
fail("hooks must map event names to arrays")
for matcher in matchers:
if not isinstance(matcher, dict) or not isinstance(matcher.get("hooks"), list):
fail(f"hooks.{event} must contain hook arrays")
for hook in matcher["hooks"]:
hook = require_keys(hook, CLAUDE_HOOK_KEYS, f"hooks.{event}")
if hook.get("type") != "command" or not isinstance(hook.get("command"), str) or not isinstance(hook.get("timeout"), int):
fail(f"hooks.{event} contains an unsupported command hook")
def main() -> int:
if len(sys.argv) != 3:
print("usage: validate-config.py <codex-toml> <claude-json>", file=sys.stderr)
return 2
tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8-sig"))
validate_claude(Path(sys.argv[2]))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError) as error:
print(f"configuration validation failed: {error}", file=sys.stderr)
raise SystemExit(1)