feat: support Codex and Claude local surfaces

This commit is contained in:
thanhnv
2026-07-24 16:33:21 +07:00
parent 34a6b013c0
commit cc666db9de
9 changed files with 119 additions and 26 deletions
+48 -7
View File
@@ -60,10 +60,35 @@ VSCODE_EXTENSION_IDS = {
PROMPT_MARKER_START = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
PROMPT_MARKER_END = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
CLIENT_LABELS = {
"claude": "Claude Code",
"codex": "Codex",
"claude": "Claude Code (CLI + VS Code + JetBrains)",
"codex": "Codex local (desktop + CLI + IDE)",
"vscode-copilot": "VS Code / @casan",
}
CLIENT_SURFACES = {
"claude": {
"supported_local": [
"claude-code-cli",
"claude-code-vscode",
"claude-code-jetbrains",
],
"not_covered": ["claude-desktop", "claude-web"],
"contract": "shared_claude_code_project_settings",
},
"codex": {
"supported_local": [
"codex-desktop-local",
"codex-cli",
"codex-ide-extension-local",
],
"not_covered": ["codex-cloud", "codex-web"],
"contract": "shared_codex_local_project_hooks",
},
"vscode-copilot": {
"supported_local": ["vscode-copilot-explicit-at-casan"],
"not_covered": ["github-copilot-built-in-chat"],
"contract": "casan_owned_explicit_route",
},
}
RUNTIME_MODES = ("managed", "vendored")
@@ -131,7 +156,7 @@ def _render_init(result):
print(_color("1", "Next steps"))
print(" 1. Run `casan doctor`")
if "codex" in result["clients"]:
print(" 2. In Codex, open `/hooks` and trust this project's hook.")
print(" 2. In a local Codex client, open `/hooks` and trust this project's hook.")
def _render_verify(result):
@@ -173,6 +198,9 @@ def _render_doctor(result):
for client, item in result["client_checks"].items():
label = CLIENT_LABELS.get(client, client)
print(" %s %s" % (_mark(item.get("ready", False)), label))
surfaces = item.get("supported_surfaces") or []
if surfaces:
print(" Surfaces: %s" % ", ".join(surfaces))
reason = item.get("smoke", {}).get("reason")
if reason and not item.get("ready"):
print(" %s" % reason)
@@ -903,8 +931,8 @@ def select_clients(values, interactive):
while True:
sys.stderr.write(
"\nClient integrations\n\n"
" 1) Claude Code (CLI + official VS Code extension)\n"
" 2) Codex (CLI + official VS Code extension)\n"
" 1) Claude Code (CLI + VS Code + JetBrains)\n"
" 2) Codex local (desktop app + CLI + IDE extension)\n"
" 3) GitHub Copilot in VS Code via explicit @casan route\n\n"
"Select clients (comma-separated) [1,2]: ")
sys.stderr.flush()
@@ -1108,7 +1136,7 @@ def cmd_init(args):
# ── .casan/config.json ──
cfg_dir = os.path.join(target, ".casan")
cfg = {
"schema_version": "21.1",
"schema_version": "21.2",
"project_id": project,
"created_at": now_iso(),
"enforcement_mode": args.mode,
@@ -1121,6 +1149,13 @@ def cmd_init(args):
if "vscode-copilot" in clients else "disabled",
"vscode-native-copilot": "unsupported_global_interception",
},
"client_surfaces": {
client: {
"enabled": client in clients,
**surface,
}
for client, surface in CLIENT_SURFACES.items()
},
"harness_version": version,
"adoption_model": (
"managed-global" if runtime_mode == "managed"
@@ -1511,7 +1546,13 @@ def cmd_doctor(args):
line.strip().lower() for line in listed.stdout.splitlines() if line.strip()}
for client in clients:
item = {"configured": True}
surface = CLIENT_SURFACES.get(client, {})
item = {
"configured": True,
"supported_surfaces": list(surface.get("supported_local", [])),
"not_covered_surfaces": list(surface.get("not_covered", [])),
"surface_contract": surface.get("contract"),
}
if client == "claude":
commands = _commands_in_hooks(
os.path.join(target, ".claude", "settings.json"))
@@ -6,6 +6,10 @@ installer does this). It wires the four Claude Code lifecycle hooks
bridge so a developer can type prompts normally while every certified turn still
carries an H1→H7 trace and an H6 record.
Claude Code shares these project settings across its CLI, official VS Code
extension, and JetBrains integration. Claude Desktop and claude.ai are separate
products and are not covered by this local project-hook integration.
- The hook command resolves the repo root via `$CLAUDE_PROJECT_DIR`, so **no
machine-specific path is committed**.
- It invokes `.casan/casan-hook.py`. That bootstrap reads project config,
@@ -5,6 +5,11 @@ handler calls `.casan/casan-hook.py`, which resolves and verifies the pinned
global harness before dispatching the Codex adapter. No custom `[casan]` TOML
keys are required.
This one integration covers the local Codex runtime across the desktop app,
CLI, and IDE extension. It does not claim coverage for Codex Cloud/Web, where a
repo-local process and its `.casan/casan-hook.py` bootstrap are not the local
execution boundary.
Important Codex-specific facts (Spike-20 §4.2):
- **Trust review is mandatory.** Codex loads project-local hooks only after the
@@ -151,6 +151,23 @@ PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package
for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do
[[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f"
done
python3 - "$PROJ/.casan/config.json" <<'PY' \
&& pass "config declares supported and excluded client surfaces" \
|| fail "config client surface contract is incomplete"
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["schema_version"] == "21.2"
assert d["client_surfaces"]["claude"]["supported_local"] == [
"claude-code-cli", "claude-code-vscode", "claude-code-jetbrains",
]
assert d["client_surfaces"]["codex"]["supported_local"] == [
"codex-desktop-local", "codex-cli", "codex-ide-extension-local",
]
assert "claude-desktop" in d["client_surfaces"]["claude"]["not_covered"]
assert "codex-cloud" in d["client_surfaces"]["codex"]["not_covered"]
PY
if [[ -d "$PROJ/packages/casan-harness" ]]; then fail "harness was copied into the repo (should not be)"; else pass "harness NOT copied into repo (hybrid model)"; fi
if grep -R -q 'packages/casan-harness/adapters' "$PROJ/.claude/settings.json" "$PROJ/.codex/hooks.json"; then fail "generated hooks still target a repo-local harness"; else pass "generated hooks target the stable project bootstrap"; fi