From eb3525456f6fc2500cc24b34f5003f63149efab5 Mon Sep 17 00:00:00 2001 From: thanhnv Date: Fri, 24 Jul 2026 12:16:11 +0700 Subject: [PATCH] feat: add production Core runtime modes --- README.md | 25 +- bin/casan | 39 ++- docs/casan/CASAN_ADOPTION_WINDOWS.md | 232 +++++++--------- docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md | 2 +- docs/casan/CASAN_INSTALL_HYBRID.md | 55 ++-- docs/casan/CASAN_PROMPT_ENFORCEMENT.md | 170 +++++------- docs/packaging/ADOPTION_GUIDE.md | 68 ++--- docs/packaging/PROMPT_ENFORCEMENT_GUIDE.md | 115 ++++---- install.ps1 | 2 +- install.sh | 2 +- packages/casan-devkit/casan-init.py | 251 +++++++++++++++--- .../templates/project/casan-hook.py | 30 ++- .../tests/hybrid-install-tests.sh | 65 ++++- 13 files changed, 659 insertions(+), 397 deletions(-) diff --git a/README.md b/README.md index 9442437..0db2037 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,18 @@ release tooling. Source repository vẫn giữ tests để kiểm chứng chính ### 2. Adopt vào repository hiện hữu +CASAN tách rõ hai quyết định: + +| Phạm vi | Ý nghĩa | +|---|---| +| `--level core` (mặc định) | Capability áp dụng cho project: governance Core, không thêm domain-pack/CI | +| `--runtime managed` (mặc định project mới) | Dùng Core global đã pin version/hash; repo nhẹ, nâng cấp tập trung | +| `--runtime vendored` | Copy Core production-only vào `.casan/runtime/casan-core`; phù hợp offline/air-gapped/self-contained | + ```bash cd -# Chỉ thêm governance config/hooks; không thêm domain-pack hoặc CI template +# Production mặc định: managed Core casan init --client claude,codex --mode enforce casan doctor @@ -83,8 +91,15 @@ casan init --level core --client claude,codex casan init --level core --client vscode-copilot --vscode-install yes casan init --level core --client all casan init --level devkit --client claude,codex + +# Project phải tự chứa Core (offline/air-gapped) +casan init --runtime vendored --client claude,codex ``` +Output `init` và `casan level show` luôn hiển thị runtime mode cùng đường dẫn +thực tế. Chạy lại `init` giữ mode hiện tại; chỉ đổi khi truyền rõ +`--runtime managed` hoặc `--runtime vendored`. + Với Codex, sau init phải mở `/hooks`, kiểm tra và trust đúng project hook hash. ### 3. Dùng project bình thường @@ -148,8 +163,9 @@ flowchart TB ## Cấu trúc cài đặt thực tế -CASAN dùng mô hình hybrid: policy code nằm ở global installation; project chỉ -giữ bootstrap, pin và state riêng. +CASAN mặc định dùng managed runtime: policy code nằm ở global installation, +project giữ bootstrap, pin và state riêng. Với `--runtime vendored`, cùng Core +production-only được đặt tại `.casan/runtime/casan-core`. ```mermaid flowchart TB @@ -283,7 +299,8 @@ casan uninstall --remove-vscode-extension `uninstall` xóa workflow CASAN trong `.gitea`, xóa các scaffold file CASAN còn nguyên checksum và tự dọn thư mục cha khi đã rỗng. Workflow/file của project, scaffold file đã chỉnh sửa và `.casan-bak` được giữ lại để tránh mất dữ liệu. -Lệnh cũng không mặc định gỡ VS Code extension dùng chung cho các project khác. +Vendored Core trong `.casan/runtime/casan-core` cũng được xóa. Lệnh không mặc +định gỡ VS Code extension dùng chung cho các project khác. Khi nâng cấp CASAN: diff --git a/bin/casan b/bin/casan index e5c6edd..5913ef5 100755 --- a/bin/casan +++ b/bin/casan @@ -8,13 +8,44 @@ set -uo pipefail # --- locate the harness root (dir containing scripts/bash/casan-harness.sh) ---- _self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_find_project_root() { + local d="${CASAN_APP_ROOT:-$PWD}" + while [[ "$d" != "/" && -n "$d" ]]; do + [[ -f "$d/.casan/config.json" ]] && { echo "$d"; return 0; } + d="$(dirname "$d")" + done + return 1 +} +PROJECT_ROOT="$(_find_project_root || true)" +PROJECT_RUNTIME_MODE="managed" +if [[ -n "$PROJECT_ROOT" ]]; then + PROJECT_RUNTIME_MODE="$(python3 -c \ + 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("runtime_mode", "managed"))' \ + "$PROJECT_ROOT/.casan/config.json" 2>/dev/null || echo managed)" +fi +PROJECT_VENDORED_HARNESS="${PROJECT_ROOT:+$PROJECT_ROOT/.casan/runtime/casan-core/packages/casan-harness}" +if [[ "$PROJECT_RUNTIME_MODE" == "vendored" && + ! -f "$PROJECT_VENDORED_HARNESS/scripts/bash/casan-harness.sh" ]]; then + echo "casan: project is pinned to vendored Core but runtime is missing: $PROJECT_VENDORED_HARNESS" >&2 + echo "casan: restore it with 'casan init --runtime vendored' from an approved DevKit install" >&2 + exit 1 +fi + _find_harness() { - # 1) sibling packages/casan-harness (source hub or bundle root layout) + # 1) project-vendored Core, when explicitly present. local c + if [[ "$PROJECT_RUNTIME_MODE" == "vendored" ]]; then + c="$PROJECT_VENDORED_HARNESS" + [[ -f "$c/scripts/bash/casan-harness.sh" ]] && { (cd "$c" && pwd); return 0; } + fi + # 2) managed/global Core passed by the launcher. + c="${CASAN_GLOBAL_HARNESS_ROOT:-}" + [[ -n "$c" && -f "$c/scripts/bash/casan-harness.sh" ]] && { (cd "$c" && pwd); return 0; } + # 3) sibling packages/casan-harness (source hub or bundle root layout). for c in "$_self/../packages/casan-harness" "$_self/../casan-harness" "$_self/packages/casan-harness"; do [[ -f "$c/scripts/bash/casan-harness.sh" ]] && { (cd "$c" && pwd); return 0; } done - # 2) walk up looking for it + # 4) walk up looking for it. local d="$_self" while [[ "$d" != "/" ]]; do [[ -f "$d/packages/casan-harness/scripts/bash/casan-harness.sh" ]] && { echo "$d/packages/casan-harness"; return 0; } @@ -28,7 +59,7 @@ if [[ -z "${HARNESS:-}" || ! -d "$HARNESS" ]]; then exit 1 fi BASH_DIR="$HARNESS/scripts/bash" -CASAN_APP_ROOT="${CASAN_APP_ROOT:-$(cd "$HARNESS/../.." && pwd)}" +CASAN_APP_ROOT="${CASAN_APP_ROOT:-${PROJECT_ROOT:-$(cd "$HARNESS/../.." && pwd)}}" VERSION_FILE="$_self/../VERSION" [[ -f "$VERSION_FILE" ]] || VERSION_FILE="$HARNESS/../../VERSION" @@ -41,7 +72,7 @@ casan — CASAN governance harness CLI ($(version)) Usage: casan [args] Commands: - init [--level core|devkit] [...] Adopt CASAN (project default: core) + init [--runtime managed|vendored] Adopt CASAN Core into this project uninstall [--purge] Remove CASAN from this project (preserves user config) doctor [--client ...] Verify configured hooks, pin, adapters, and VS Code route level Show / change the project's packaging level diff --git a/docs/casan/CASAN_ADOPTION_WINDOWS.md b/docs/casan/CASAN_ADOPTION_WINDOWS.md index e20e7a4..0ab7b07 100644 --- a/docs/casan/CASAN_ADOPTION_WINDOWS.md +++ b/docs/casan/CASAN_ADOPTION_WINDOWS.md @@ -1,204 +1,150 @@ -# Áp dụng CASAN từ đầu trên Windows +# Áp dụng CASAN production trên Windows -Tài liệu này dành cho thành viên đã có một repository dự án nhưng repository đó **chưa có CASAN**. CASAN Core được clone riêng từ Gitea, sau đó DevKit cài runtime và policy vào repository dự án. +Tài liệu này dành cho repository hiện hữu chưa có CASAN. Windows dùng +PowerShell để cài CLI và Git for Windows/Git Bash để chạy các gate Bash. WSL2 +không bắt buộc. -Giá trị sau được installer thay theo dự án: +## 1. Yêu cầu -- Project ID: `__PROJECT_ID__` -- Project name: `__PROJECT_NAME__` - -## 1. Phạm vi hỗ trợ - -Luồng này áp dụng cho agent coding chạy tại project root: - -- Claude Code; -- Codex; -- GitHub Copilot Coding Agent; -- GitHub Copilot hoặc agent plugin trong VS Code có hỗ trợ repository instructions. - -CASAN CLI đầy đủ chạy trong WSL2. PowerShell chỉ đóng vai trò gọi wrapper WSL2. - -## 2. Cài WSL2 và công cụ nền - -Trong PowerShell Administrator, nếu máy chưa có WSL2: - -```powershell -wsl --install -d Ubuntu -``` - -Khởi động lại Windows nếu được yêu cầu. Sau đó mở PowerShell thường và cài công cụ trong Ubuntu: - -```powershell -wsl -d Ubuntu -- bash -lc 'sudo apt-get update && sudo apt-get install -y git python3 rsync' -wsl -d Ubuntu -- bash -lc 'git --version && python3 --version && rsync --version | head -1' -``` - -SSH key truy cập Gitea phải được cấu hình trong `~/.ssh` của WSL hoặc thông qua cơ chế quản lý key đã được tổ chức phê duyệt. Không đặt private key trong repository. - -## 3. Khai báo đường dẫn - -Thay hai đường dẫn Windows và URL Gitea theo môi trường thực tế: - -```powershell -$TargetProjectWin = 'C:\Projects\my-existing-project' -$CasanSourceWin = 'C:\Projects\.casan-source\casan-core' -$CasanRepo = 'ssh://git@://.git' - -$TargetProjectWsl = (wsl -d Ubuntu -- wslpath -a $TargetProjectWin).Trim() -$CasanSourceWsl = (wsl -d Ubuntu -- wslpath -a $CasanSourceWin).Trim() -``` +- PowerShell 5.1+ hoặc PowerShell 7; +- Python 3 trên `PATH`; +- Git for Windows, bao gồm `bash.exe`; +- quyền đọc CASAN release/checkout đã được tổ chức phê duyệt. Kiểm tra: ```powershell -wsl -d Ubuntu -- bash -lc "test -d '$TargetProjectWsl' && printf 'TARGET_OK=%s\n' '$TargetProjectWsl'" +python --version +git --version +Get-Command bash ``` -Nếu dự án là Git repository, commit hoặc lưu riêng thay đổi hiện có trước khi adoption: +## 2. Cài DevKit một lần trên máy + +Từ checkout CASAN: ```powershell -wsl -d Ubuntu -- bash -lc "cd '$TargetProjectWsl' && git status --short --branch" +pwsh .\install.ps1 -Level devkit ``` -Không dùng `git reset --hard` hoặc `git clean` để chuẩn bị cài đặt. - -## 4. Clone CASAN Core từ Gitea - -Clone lần đầu: +Mở terminal mới và kiểm tra: ```powershell -New-Item -ItemType Directory -Force -Path (Split-Path $CasanSourceWin -Parent) | Out-Null -wsl -d Ubuntu -- bash -lc "git clone '$CasanRepo' '$CasanSourceWsl'" +casan version ``` -Nếu đã clone, chỉ cập nhật bằng fast-forward khi working tree CASAN sạch: - -```powershell -wsl -d Ubuntu -- bash -lc "cd '$CasanSourceWsl' && git status --short --branch && git pull --ff-only origin main" -``` - -## 5. Cài CASAN vào repository dự án - -```powershell -wsl -d Ubuntu -- bash -lc "cd '$CasanSourceWsl' && bash packages/casan-devkit/install.sh --target '$TargetProjectWsl' --project '__PROJECT_ID__' --domain '__PROJECT_NAME__'" -``` - -Installer tạo hoặc cập nhật: - -- `packages/casan-harness/` — CASAN Core H1-H7; -- `bin/casan` — CLI; -- `bin/casan-chat` và `bin/casan-chat.ps1` — prompt entrypoint; -- `.casan/prompt-policy.json` — project binding; -- `apps/__PROJECT_ID__/domain/` — domain pack ban đầu; -- `AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md` — agent enforcement block; -- `.gitea/workflows/casan-prompt-enforcement.yml` — kiểm tra contract trên CI; -- `docs/casan/` — hướng dẫn đã render cho dự án. - -Installer giữ nội dung bên ngoài CASAN marker, tài liệu domain hiện hữu, project registry và workflow CI hiện hữu. - -## 6. Thay domain scaffold bằng context thật - -Hoàn thiện tối thiểu: +Runtime managed mặc định nằm dưới: ```text -apps/__PROJECT_ID__/domain/input/requirement.md -apps/__PROJECT_ID__/domain/input/architecture.md -apps/__PROJECT_ID__/domain/golden-runs/ -apps/__PROJECT_ID__/domain/traceability-map.json -apps/__PROJECT_ID__/domain/corpus/ +%LOCALAPPDATA%\casan\current ``` -Không đưa source tree lớn, binary, log, build output, credential hoặc dữ liệu nhạy cảm vào context mặc định. Chỉ khai báo những context root cần thiết và có chủ đích. +Global package phải là DevKit vì lệnh adoption `casan init` nằm trong DevKit. +Project vẫn mặc định áp dụng Level 1/Core. -## 7. Xác minh installation contract +## 3. Adopt repository ```powershell -wsl -d Ubuntu -- bash -lc "cd '$TargetProjectWsl' && bin/casan prompt verify" +Set-Location 'C:\Projects\my-existing-project' +casan init --project my-existing-project --client claude,codex +casan doctor +casan verify-harness +casan level show ``` -Kết quả bắt buộc: +Output phải hiển thị rõ: + +- project level: `Core (1)`; +- runtime mode: `Managed`; +- đường dẫn Core thực tế; +- version và integrity hash đã pin. + +Managed mode chỉ ghi config/lock/bootstrap và client hooks vào repository; Core +được dùng từ global install. + +## 4. Chế độ self-contained/air-gapped + +Nếu khách hàng yêu cầu Core nằm trong repository: + +```powershell +casan init --runtime vendored --project my-existing-project --client claude,codex +``` + +Core production-only được đặt tại: ```text -CASAN_PROMPT_ENFORCEMENT_VALID project=__PROJECT_ID__ mode=enforced +.casan\runtime\casan-core\ + bin\casan + packages\casan-harness\ + VERSION ``` -Nếu lệnh thất bại, dừng sử dụng agent và sửa đúng artifact được báo thiếu hoặc sai. +Folder này không chứa tests, legacy `level5`, internal CI runners hoặc +Platform-only helpers. Project hook, global launcher và local CLI đều resolve +runtime này theo `.casan\version.lock`. Nếu vendored Core bị thiếu hoặc sai +hash, CASAN fail closed và không fallback âm thầm sang global Core. -## 8. Chạy gate ban đầu +Chạy lại `casan init` giữ runtime mode hiện tại. Chuyển mode phải explicit: ```powershell -wsl -d Ubuntu -- bash -lc "cd '$TargetProjectWsl' && CASAN_DOMAIN_ROOT='apps/__PROJECT_ID__/domain' bin/casan gate" +casan init --runtime managed +casan init --runtime vendored ``` -Gate có thể fail khi domain pack chưa có requirement, golden run hoặc corpus thật. Không sửa report để đổi FAIL thành PASS; bổ sung đúng evidence còn thiếu. +## 5. Client integration -## 9. Gửi prompt qua CASAN +- Claude: CASAN merge hook vào `.claude\settings.json`. +- Codex: CASAN merge hook vào `.codex\hooks.json`; mở `/hooks` để review/trust. +- VS Code/Copilot: dùng `--client vscode-copilot`; route được chứng nhận là + explicit `@casan`, không phải toàn bộ Copilot Chat. -Từ project root trong PowerShell: +CASAN không xóa hook, agent, skill, instruction hoặc workflow không thuộc CASAN. + +## 6. CI + +Managed mode: runner phải cài đúng CASAN release đã pin trước khi chạy: ```powershell -Set-Location $TargetProjectWin -powershell -ExecutionPolicy Bypass -File bin\casan-chat.ps1 "Review the current requirements and identify missing acceptance criteria." +casan verify-harness +casan gate ``` -Chế độ tương tác: +Vendored mode có local CLI: ```powershell -powershell -ExecutionPolicy Bypass -File bin\casan-chat.ps1 +.\.casan\runtime\casan-core\bin\casan verify-harness +.\.casan\runtime\casan-core\bin\casan gate ``` -Mỗi lượt thành công phải có `certified=true`, `trace_id` và dòng `CASAN_PROMPT_TRACE_CERTIFIED ... gates=7`. +Luôn chạy `verify-harness` trước gate để phát hiện runtime drift/tamper. -## 10. Xác minh một prompt +## 7. Uninstall ```powershell -wsl -d Ubuntu -- bash -lc "cd '$TargetProjectWsl' && bin/casan prompt trace ''" +casan uninstall ``` -Kết quả hợp lệ: +Lệnh xóa CASAN hooks/config, CASAN-owned Gitea workflow, scaffold chưa chỉnh sửa +và toàn bộ vendored Core nếu có. Hook/workflow/file project được giữ lại. -```text -CASAN_PROMPT_TRACE_CERTIFIED project=__PROJECT_ID__ trace_id= gates=7 -``` - -## 11. Dùng với Claude Code, Codex và Copilot - -Mở agent tại đúng `$TargetProjectWin`. Agent phải đọc instruction tương ứng: - -- Codex: `AGENTS.md`; -- Claude Code: `CLAUDE.md`; -- GitHub Copilot: `.github/copilot-instructions.md`. - -Nếu plugin không hỗ trợ repository instructions hoặc tính năng đó đang tắt, không được coi prompt là đã enforce. Prompt trực tiếp không có CASAN trace không được gắn nhãn certified. - -Chi tiết role, codegen và approval nằm trong `docs/casan/CASAN_PROMPT_ENFORCEMENT.md`. - -## 12. Commit adoption vào repository dự án - -Sau khi review diff và chạy verify: +Xóa thêm runtime evidence: ```powershell -wsl -d Ubuntu -- bash -lc "cd '$TargetProjectWsl' && git status --short" +casan uninstall --purge ``` -Commit các artifact CASAN cần được chia sẻ cho team. Không commit `.specify/` runtime log nếu policy repository yêu cầu giữ telemetry ngoài Git. - -## 13. Nâng cấp CASAN +Chỉ gỡ extension dùng chung khi chắc chắn không project nào khác cần: ```powershell -wsl -d Ubuntu -- bash -lc "cd '$CasanSourceWsl' && git pull --ff-only origin main" -wsl -d Ubuntu -- bash -lc "cd '$CasanSourceWsl' && bash packages/casan-devkit/install.sh --target '$TargetProjectWsl' --project '__PROJECT_ID__' --domain '__PROJECT_NAME__'" -wsl -d Ubuntu -- bash -lc "cd '$TargetProjectWsl' && bin/casan prompt verify" +casan uninstall --remove-vscode-extension ``` ## Checklist bàn giao -- [ ] WSL2 có Git, Python 3 và rsync. -- [ ] CASAN Core được clone riêng từ Gitea và đang ở `main` mới nhất. -- [ ] Installer hoàn tất cho project `__PROJECT_ID__`. -- [ ] Domain pack đã dùng context/evidence thật. -- [ ] `bin/casan prompt verify` đạt. -- [ ] Agent coding được mở tại project root và đọc repository instructions. -- [ ] Prompt mẫu trả `certified=true` và trace H1-H7 xác minh được. -- [ ] H6 telemetry có `project_id=__PROJECT_ID__`. -- [ ] Workflow `casan-prompt-enforcement.yml` được commit và chạy trên push/PR. +- [ ] `casan version` chạy trong terminal mới. +- [ ] `casan level show` hiển thị đúng level, runtime mode và path. +- [ ] `.casan\version.lock` có version, runtime mode/path và hash. +- [ ] `casan doctor` đạt. +- [ ] `casan verify-harness` đạt. +- [ ] Codex hook đã được review/trust nếu chọn Codex. +- [ ] CI verify đúng runtime đã pin trước khi chạy gate. diff --git a/docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md b/docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md index 109448b..43518a7 100644 --- a/docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md +++ b/docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md @@ -105,4 +105,4 @@ Plan-20 **không** quảng bá `project_hook` như một sandbox tuyệt đối. exit gate. - Trace sinh trong observe mode luôn `observed_only`, **không** retroactively certified. -- Rollback chỉ tắt adapter; `bin/casan-chat` và core H1→H7 hiện tại vẫn hoạt động. +- Rollback chỉ tắt adapter; Core H1→H7 đã pin theo project vẫn hoạt động. diff --git a/docs/casan/CASAN_INSTALL_HYBRID.md b/docs/casan/CASAN_INSTALL_HYBRID.md index 31623d1..ec7139b 100644 --- a/docs/casan/CASAN_INSTALL_HYBRID.md +++ b/docs/casan/CASAN_INSTALL_HYBRID.md @@ -1,8 +1,14 @@ -# Cài CASAN kiểu tool (global install + `casan init`) — Plan-21 +# Cài CASAN production (managed hoặc vendored Core) — Plan-21 -Mô hình **hybrid**: cài harness **một lần** vào máy (`$CASAN_HOME`), sau đó mỗi -dự án chỉ chạy `casan init` để ghi **config riêng của dự án** — harness KHÔNG bị -copy vào từng repo. Giống trải nghiệm codegraph. +CASAN cài CLI/DevKit **một lần** vào máy (`$CASAN_HOME`). Mỗi project chạy +`casan init` và chọn một runtime contract rõ ràng: + +- `managed` (mặc định): Core nằm trong global install, project pin version/hash. +- `vendored`: Core production-only nằm tại `.casan/runtime/casan-core`, dành + cho offline, air-gapped hoặc repository cần self-contained. + +Capability level và runtime placement là hai khái niệm độc lập. Project mặc +định dùng Level 1/Core dù global package phải là DevKit để có lệnh `init`. ## 1. Cài đặt (một lần cho mỗi máy) @@ -63,10 +69,19 @@ casan init # mặc định project Level 1/core; interacti casan init --level 1 --project my-app --client claude casan init --level 2 --project my-app --client claude,codex casan init --project my-app --client vscode-copilot --vscode-install yes +casan init --runtime vendored --project offline-app --client claude,codex casan level show # xem level đã cài + level project casan level set 2 # đổi level project (không cần init lại) ``` +`init` và `level show` luôn in runtime mode/path. Project mới mặc định +`managed`; chạy lại init giữ nguyên mode đã chọn. Chuyển mode phải explicit: + +```bash +casan init --runtime managed +casan init --runtime vendored +``` + **Áp dụng cho dự án ĐÃ có vỏ (agents/skills/hook sẵn):** an toàn. - Mặc định `casan init` áp dụng **Level 1/core**: governance config + hooks, không thêm `.gitea` workflow hoặc domain-pack. Level cài global vẫn phải là @@ -89,7 +104,7 @@ nguyên checksum, sau đó prune thư mục rỗng. Workflow của project và s file đã chỉnh sửa được giữ lại. Thêm `--purge` để xóa cả runtime evidence `.specify/logs` và `.specify/state`. -`casan init` chỉ ghi **config per-project** (không copy harness): +`casan init` luôn ghi config per-project: | File | Vai trò | |---|---| @@ -101,6 +116,7 @@ file đã chỉnh sửa được giữ lại. Thêm `--purge` để xóa cả ru | `.claude/settings.json` | hook Claude Code (Plan-20) | | `.codex/hooks.json` | hook Codex theo schema hiện hành; cần review/trust bằng `/hooks` | | `.vscode/extensions.json` | recommendations cho IDE đã chọn | +| `.casan/runtime/casan-core/` | Chỉ mode `vendored`: CLI + Core runtime production-only | Tham số `--client` có thể lặp hoặc comma-separated: `claude`, `codex`, `vscode-copilot`, `all`, `none`. Khi chạy `casan init` trực @@ -154,7 +170,9 @@ Quy tắc migration: - Codex vẫn cần `/hooks` trust; Copilot built-in vẫn cần explicit `@casan`. Sau `init`, developer gõ prompt bình thường trong client — trace H1→H7 + H6 theo -Plan-20. Repo chỉ có mấy file config nhỏ; nâng cấp harness làm ở `$CASAN_HOME`. +Plan-20. Managed mode nâng cấp runtime ở `$CASAN_HOME`; vendored mode được nâng +cấp có chủ đích bằng cách chạy lại `casan init --runtime vendored` từ release +đã duyệt. ### Capability theo client @@ -185,9 +203,10 @@ casan uninstall Command này xóa CASAN project hooks, bootstrap và config nhưng giữ nguyên hook người dùng, CI/domain files, `.casan-bak`, VS Code extension dùng chung và -`.specify` evidence. Dùng `--purge` nếu chủ động muốn xóa runtime logs/state; -dùng `--remove-vscode-extension` nếu chắc chắn không project nào khác trên máy -còn dùng route `@casan`. +`.specify` evidence. Nếu project dùng vendored mode, toàn bộ +`.casan/runtime/casan-core` cũng bị xóa. Dùng `--purge` nếu chủ động muốn xóa +runtime logs/state; dùng `--remove-vscode-extension` nếu chắc chắn không project +nào khác trên máy còn dùng route `@casan`. ## 3. Pin + Verify (giữ đảm bảo bảo mật khi harness ở ngoài repo) @@ -209,18 +228,18 @@ khi tin bất kỳ trace nào là certified. > Bước làm mạnh tiếp theo (chưa bật mặc định): ký `.harness-hash` bằng khóa tổ > chức để verify cả *chữ ký* chứ không chỉ nội dung — dùng hạ tầng ký của Plan-16. -## 4. So sánh với mô hình vendored cũ +## 4. Chọn runtime mode -| | Vendored (`devkit/install.sh`) | Hybrid (`casan init`) | +| | Vendored (`casan init --runtime vendored`) | Managed (`casan init`) | |---|---|---| -| Repo | Nặng (copy cả harness) | Nhẹ (chỉ config) | -| Nâng cấp | Mỗi repo tự drift | 1 chỗ (`$CASAN_HOME`) | -| Bảo mật | Gate commit + ký trong repo | Gate global + **pin+verify** trong repo | -| CI/offline | Tự chứa | Cần cài harness trên runner (hoặc verify pin) | +| Repo | Tự chứa Core production-only | Nhẹ, chỉ config/lock/hooks | +| Nâng cấp | Explicit theo từng repo | Tập trung ở `$CASAN_HOME` | +| Bảo mật | Core local + **pin/hash verify** | Core global + **pin/hash verify** | +| CI/offline | Phù hợp air-gapped | Runner phải cài đúng CASAN release | +| Khuyến nghị | Khách hàng offline/regulated | Mặc định cho workstation và managed CI | -Cả hai vẫn dùng chung lõi harness + `casan-paths.sh` (tách `CASAN_HARNESS_ROOT` -= code, `CASAN_STATE_ROOT` = state trong repo, `CASAN_DOMAIN_ROOT` = dữ liệu dự -án). Chọn mô hình theo nhu cầu triển khai. +Cả hai dùng đúng cùng production allowlist và lõi harness; không mode nào mang +theo tests, legacy `level5`, internal runners hay Platform-only helpers. ## 5. Kiểm thử diff --git a/docs/casan/CASAN_PROMPT_ENFORCEMENT.md b/docs/casan/CASAN_PROMPT_ENFORCEMENT.md index 3b18bbb..f92bc66 100644 --- a/docs/casan/CASAN_PROMPT_ENFORCEMENT.md +++ b/docs/casan/CASAN_PROMPT_ENFORCEMENT.md @@ -1,131 +1,83 @@ -# CASAN Prompt Enforcement cho Agentic Coding +# CASAN Prompt Enforcement -Tài liệu này mô tả ranh giới bắt buộc khi dùng Claude Code, Codex, GitHub Copilot Coding Agent hoặc agent plugin trong VS Code với project `__PROJECT_ID__`. +CASAN thực thi governance qua integration native của từng client và một Core +runtime đã pin theo project. Không dùng `bin/casan-chat` làm entrypoint bắt buộc +cho project adoption mới. -## Contract +## Luồng production -Một task chỉ được gọi là **CASAN-certified** khi: +1. Developer gửi prompt trong client đã được project enable. +2. Hook project gọi `.casan/casan-hook.py`. +3. Bootstrap đọc `.casan/config.json` và `.casan/version.lock`. +4. Core runtime được resolve theo mode `managed` hoặc `vendored`. +5. Live hash phải khớp project pin trước khi adapter/gate được dispatch. +6. Trace/evidence được ghi vào `.specify` của đúng project. -1. prompt đi vào `bin/casan-chat` hoặc `bin/casan-chat.ps1`; -2. repository contract vượt qua `bin/casan prompt verify`; -3. runtime tạo đủ evidence H1-H7; -4. H7 trả `certified=true`; -5. H6 telemetry có đúng `project_id=__PROJECT_ID__`; -6. `bin/casan prompt trace ` xác minh thành công. +## Runtime mode -Prompt gõ trực tiếp vào cửa sổ agent mà không có CASAN trace không được coi là certified. +Managed Core, khuyến nghị mặc định: -## Các lớp enforcement - -### 1. Entrypoint - -- macOS/Linux/WSL2: `bin/casan-chat`; -- Windows PowerShell: `bin/casan-chat.ps1`; -- launcher kiểm tra contract trước khi nhận prompt; -- launcher chỉ trả exit code thành công khi trace của prompt đã được xác minh. - -### 2. Repository instructions - -Installer quản lý một block có marker trong: - -- `AGENTS.md` cho Codex; -- `CLAUDE.md` cho Claude Code; -- `.github/copilot-instructions.md` cho GitHub Copilot. - -Agent có đọc các instruction này phải từ chối thực hiện trực tiếp một task không đi qua CASAN và yêu cầu gửi lại qua launcher. - -### 3. CI contract - -`.gitea/workflows/casan-prompt-enforcement.yml` kiểm tra policy, launcher, instruction files, domain root và project binding. Workflow này độc lập, không ghi đè CI ứng dụng. - -### 4. Per-prompt evidence - -Mỗi prompt tạo trace tại: - -```text -.specify/logs/trace-events/.jsonl +```bash +casan init --project my-project --client claude,codex ``` -H6 runtime/token/cost/failure telemetry được ghi với `project_id=__PROJECT_ID__`. +Core nằm trong `$CASAN_HOME`, còn project commit config/lock/bootstrap/hooks. -## Sử dụng hàng ngày +Vendored Core cho offline/air-gapped: -Read-only/analysis mặc định: - -```powershell -powershell -ExecutionPolicy Bypass -File bin\casan-chat.ps1 "Analyze the current implementation against the approved requirement." +```bash +casan init --runtime vendored --project my-project --client claude,codex ``` -Chế độ tương tác: +Core production-only nằm tại `.casan/runtime/casan-core`. Nếu folder này bị +thiếu hoặc sai hash, CASAN fail closed; không fallback âm thầm sang global Core. -```powershell -powershell -ExecutionPolicy Bypass -File bin\casan-chat.ps1 +## Boundary theo client + +### Claude Code + +CASAN merge handler vào `.claude/settings.json`. Hook khác, agent, skill và key +không thuộc CASAN được giữ nguyên. + +### Codex + +CASAN merge handler vào `.codex/hooks.json`. Người dùng phải mở `/hooks`, review +và trust đúng hook hash của project. + +### VS Code / GitHub Copilot + +Route được chứng nhận là explicit `@casan`. CASAN không quảng bá rằng built-in +Copilot Chat hoặc mọi prompt bên ngoài route này đều được intercept. + +### Ngoài project + +Prompt gửi trực tiếp vào website AI bên ngoài project/runtime không nằm trong +boundary chứng nhận của CASAN. Kiểm soát website/proxy/identity ở cấp tổ chức là +lớp bổ sung, không phải chức năng của repository hook. + +## Kiểm tra + +```bash +casan doctor +casan verify-harness +casan level show ``` -## Coding task và quyền +CI phải verify runtime pin trước gate: -Launcher mặc định dùng role `viewer`; role này không được sửa file hoặc chạy command tùy ý. - -Người đã được cấp quyền tạo code draft có thể cấu hình phiên PowerShell: - -```powershell -$env:CASAN_CHAT_ROLE = 'project-admin' -$env:CASAN_CHAT_AGENT = 'codegen-draft' -$env:CASAN_CHAT_SKILL = 'sourcegen-draft' - -powershell -ExecutionPolicy Bypass -File bin\casan-chat.ps1 "Implement the approved task according to the current requirement and architecture." +```bash +casan verify-harness +casan gate ``` -`codegen-draft` yêu cầu approval. Không tự đặt `project-admin` nếu chưa được cấp quyền. Side effect chỉ được thực thi bằng registered action hoặc `bin/casan run` theo policy hiện hành. +Chỉ coi output là certified khi runtime integrity hợp lệ và trace H1–H7 tương +ứng vượt qua policy. -Xóa biến sau phiên làm việc: +## Gỡ integration -```powershell -Remove-Item Env:CASAN_CHAT_ROLE -ErrorAction SilentlyContinue -Remove-Item Env:CASAN_CHAT_AGENT -ErrorAction SilentlyContinue -Remove-Item Env:CASAN_CHAT_SKILL -ErrorAction SilentlyContinue +```bash +casan uninstall ``` -## Kết quả hợp lệ - -Một lượt thành công hiển thị tối thiểu: - -```text -CASAN decision=ANSWERED ... certified=true trace_id= -CASAN evidence=/.specify/logs/trace-events/.jsonl -CASAN_PROMPT_TRACE_CERTIFIED project=__PROJECT_ID__ trace_id= gates=7 -``` - -Xác minh lại: - -```powershell -wsl -d Ubuntu -- bash -lc "cd '' && bin/casan prompt trace ''" -``` - -## Khi bị chặn - -- `DENIED` hoặc `BLOCKED`: sửa prompt/context theo reason code; không bypass launcher. -- `REQUIRES_APPROVAL`: gửi proposal cho người có quyền phê duyệt. -- `CASAN_PROMPT_ENFORCEMENT_INVALID`: chạy lại installer từ CASAN Core mới nhất hoặc khôi phục managed artifact. -- `trace_project_attribution_missing`: không sử dụng kết quả; kiểm tra `project_id` và H6 telemetry. -- Plugin không đọc repository instructions: bật tính năng instruction hoặc chuyển sang công cụ được hỗ trợ. - -## Kiểm tra nhanh đầu ngày - -```powershell -wsl -d Ubuntu -- bash -lc "cd '' && bin/casan prompt verify" -``` - -Kỳ vọng: - -```text -CASAN_PROMPT_ENFORCEMENT_VALID project=__PROJECT_ID__ mode=enforced -``` - -## Điều không được làm - -- Không gọi output trực tiếp của agent là CASAN-certified khi thiếu trace. -- Không sửa/xóa evidence để thay đổi quyết định. -- Không đổi role hoặc agent để né approval. -- Không đưa secret, private key, token hoặc dữ liệu nhạy cảm vào prompt/context. -- Không chạy command side effect ngoài registered action hoặc CASAN harness. +Lệnh xóa CASAN hook/config và vendored Core nếu có, nhưng giữ hook/workflow/file +project. Dùng `--purge` để xóa thêm `.specify` logs/state. diff --git a/docs/packaging/ADOPTION_GUIDE.md b/docs/packaging/ADOPTION_GUIDE.md index 5f5bd2e..dda2eb3 100644 --- a/docs/packaging/ADOPTION_GUIDE.md +++ b/docs/packaging/ADOPTION_GUIDE.md @@ -1,33 +1,45 @@ # CASAN Adoption Guide -The installer also provisions the mandatory prompt-enforcement pack. After adoption, send project prompts through `bin/casan-chat` (or `bin/casan-chat.ps1` on Windows/WSL2) and run `bin/casan prompt verify`. The canonical from-scratch guides are [CASAN_ADOPTION_WINDOWS.md](../casan/CASAN_ADOPTION_WINDOWS.md) and [CASAN_PROMPT_ENFORCEMENT.md](../casan/CASAN_PROMPT_ENFORCEMENT.md); the installer renders both into the target repository. +CASAN separates machine installation from project adoption. Install the DevKit +once so the `casan` command is available, then enroll each repository with a +version/hash lock. Application teams never edit gate logic (H1→H7). -How a downstream project adopts the CASAN governance harness. Adoption is **config + -domain only** — you never edit gate logic (H1→H7). +## Option A — Managed Core (recommended) -## Option A — DevKit install (recommended) - -Clone CASAN Core from your Gitea repository once, and pull the latest `main` before each install or upgrade: +Install from an approved checkout or release: ```bash -git clone casan-core -cd casan-core -git pull --ff-only origin main +sh install.sh --level devkit +cd /path/to/my-project +casan init --project ticketing --client claude,codex +casan doctor +casan verify-harness ``` -Then run the installer from that CASAN checkout: +The project defaults to Level 1/Core with runtime mode `managed`. Core remains +under `$CASAN_HOME`; the repository receives `.casan` config/lock/bootstrap and +the selected client hooks. The CLI output states the resolved runtime path. + +Use this for developer workstations and managed CI runners. CI must install the +same release recorded by `.casan/version.lock` before running gates. + +## Option B — Vendored Core (offline/self-contained) ```bash -packages/casan-devkit/install.sh --target ../my-project --project ticketing --domain "Ticketing" +cd /path/to/my-project +casan init --runtime vendored --project ticketing --client claude,codex +casan doctor +casan verify-harness ``` -This copies the core harness + `bin/casan` into `../my-project`, scaffolds -`apps/ticketing/domain/` from the domain-pack template, installs the prompt entrypoints and -standalone `.gitea/workflows/casan-prompt-enforcement.yml`, and registers the project in -`project-registry.json`. Existing domain files, registry state, and project CI are preserved. -The copied harness follows the production allowlist and excludes CASAN's own tests, -legacy `level5/`, internal CI runners, and Platform-only helpers. -## Option A2 — New production project shell +This installs the production-only Core at +`.casan/runtime/casan-core/`, including a local `bin/casan`. It excludes tests, +legacy `level5`, internal runners and Platform helpers. Choose this for +air-gapped customers or repositories that must execute without a machine-level +runtime. Re-running init preserves the selected mode; switching mode requires +an explicit `--runtime managed|vendored`. + +## Option C — New production project shell ```bash packages/casan-devkit/install.sh \ @@ -41,16 +53,10 @@ This creates a strict-TypeScript NestJS/React monorepo, health bootstrap, tests, images, GitHub CI, a complete Domain Pack, versioned quality profile, project manifest, CASAN CLI, harness, and manifest-driven pipeline. Existing different files are never overwritten. -## Option B — Core tarball (harness-only / CI gate) -```bash -tar -xzf casan-core-v1.0.0.tar.gz -cp -R casan-core-v1.0.0/{packages,bin,VERSION} /path/to/project/ -``` -Then create `apps//domain/` yourself (see `DOMAIN_PACK_GUIDE.md`). -This harness-only option does not install the mandatory repository prompt-enforcement pack; -use Option A when every project prompt must be governed and certifiable. +The legacy direct installer is reserved for generating a new application shell; +do not use it merely to enroll an existing repository. -## Option C — Docker (no install into repo) +## Option D — Docker (no install into repo) ```bash docker run --rm -v "$PWD":/workspace -w /workspace casan-harness:1.0.0 casan gate ``` @@ -77,11 +83,13 @@ bin/casan pipeline --manifest apps//domain/project.manifest.json ``` ## Path model (what lives where) -- **Harness code** → `packages/casan-harness/` (never edited by adopters). +- **Managed Core** → `$CASAN_HOME/current/packages/casan-harness/`. +- **Vendored Core** → `.casan/runtime/casan-core/packages/casan-harness/`. +- **Project lock/config** → `.casan/config.json` and `.casan/version.lock`. - **Your domain data** → `apps//domain/` (via `CASAN_DOMAIN_ROOT`). - **Runtime state** → `.specify/` (logs, audit, governance — created on first run). -Paths resolve via `packages/casan-harness/scripts/bash/casan-paths.sh` (marker walk-up: -`.specify` or `packages/casan-harness`), so a freshly-extracted bundle works immediately. +The bootstrap resolves the mode/path from the project lock and verifies the live +Core hash before dispatch. ## Proving reuse Two+ projects sharing the same harness package/version → `bin/casan reuse` prints diff --git a/docs/packaging/PROMPT_ENFORCEMENT_GUIDE.md b/docs/packaging/PROMPT_ENFORCEMENT_GUIDE.md index a58ee5e..4cdf1de 100644 --- a/docs/packaging/PROMPT_ENFORCEMENT_GUIDE.md +++ b/docs/packaging/PROMPT_ENFORCEMENT_GUIDE.md @@ -1,69 +1,92 @@ # CASAN Prompt Enforcement for Adopted Projects -For the canonical agentic-coding guide installed into downstream repositories, see [`docs/casan/CASAN_PROMPT_ENFORCEMENT.md`](../casan/CASAN_PROMPT_ENFORCEMENT.md). For a full Windows installation starting from a repository with no CASAN files, see [`docs/casan/CASAN_ADOPTION_WINDOWS.md`](../casan/CASAN_ADOPTION_WINDOWS.md). +CASAN adopts existing repositories through native project hooks. The global +DevKit provides `casan init`; each project pins either a managed or vendored +Core runtime. -The DevKit installer configures an adopted repository so supported repository agents and team members use CASAN as the certified prompt boundary. +## Supported boundaries -## What is enforced +- Claude Code: project hooks in `.claude/settings.json`. +- Codex: project hooks in `.codex/hooks.json`, subject to explicit `/hooks` + review and trust. +- VS Code/Copilot: CASAN-owned explicit `@casan` route. -- `bin/casan-chat` is the macOS/Linux/WSL2 prompt entrypoint. -- `bin/casan-chat.ps1` is the Windows wrapper and executes the same entrypoint through WSL2. -- `.casan/prompt-policy.json` binds prompts and H6 telemetry to one `project_id`. -- `AGENTS.md`, `CLAUDE.md`, and `.github/copilot-instructions.md` tell supported repository agents to refuse direct prompt work and require resubmission through CASAN. -- The standalone `.gitea/workflows/casan-prompt-enforcement.yml` workflow verifies that the policy, launchers, instructions, domain root, and workflow contract are present and have not been stripped. Existing project CI is not overwritten. -- A certified prompt must produce a trace with passing H1-H7 gates and H6 telemetry attributed to the configured project. +CASAN does not claim to intercept arbitrary prompts typed into external web +sites or built-in Copilot Chat outside the explicit `@casan` route. -## Enforcement boundary +## Install -A repository cannot technically intercept text typed directly into an external ChatGPT, Claude, or Copilot website. Such conversations are outside the CASAN runtime and therefore are **not CASAN-certified**. The enforceable rule is: - -1. Use a CASAN-owned entrypoint for every project prompt. -2. Repository-aware agents must refuse direct execution when their instruction file is loaded. -3. Accept governed output only when its CASAN trace passes verification. - -For stronger organizational control, restrict direct external AI sites at the identity, proxy, or network layer. That control is outside the repository and complements CASAN rather than replacing its H1-H7 evidence. - -## Install or upgrade - -From a checked-out CASAN Core repository: +Install the DevKit once: ```bash -packages/casan-devkit/install.sh \ - --target "/absolute/path/to/existing-project" \ - --project "project-id" \ - --domain "Project display name" +sh install.sh --level devkit ``` -The installer is idempotent for the managed instruction blocks. Existing content outside the CASAN markers, project domain documents, project registry, and existing CI workflows is retained. - -## Send prompts - -macOS, Linux, or WSL2: +Adopt an existing project with the recommended managed Core: ```bash -bin/casan-chat "Review the current requirements and identify missing acceptance criteria" +cd /absolute/path/to/existing-project +casan init --project project-id --client claude,codex +casan doctor +casan verify-harness ``` -Windows PowerShell with WSL2: - -```powershell -powershell -ExecutionPolicy Bypass -File bin\casan-chat.ps1 "Review the current requirements" -``` - -Run without a prompt to enter interactive mode. - -## Verify - -Verify the repository contract: +For an offline/self-contained repository: ```bash -bin/casan prompt verify +casan init --runtime vendored --project project-id --client claude,codex ``` -Verify an individual governed result: +Vendored Core is installed under `.casan/runtime/casan-core` using the same +production allowlist as the global release. Tests, legacy `level5`, internal +runners and Platform-only helpers are excluded. + +## Runtime contract + +`.casan/config.json` records: + +- project ID and enforcement mode; +- selected client integrations; +- project capability level; +- runtime mode and path. + +`.casan/version.lock` records the exact Core version and integrity hash. +`casan verify-harness` recomputes the live hash; it does not trust a cached +value. Vendored mode fails closed if the local Core is absent instead of +silently falling back to global Core. + +## Existing repository safety + +`casan init` merges only CASAN handlers into supported client configuration. +Existing hooks, agents, skills, instructions and CI workflows are preserved. +Re-running init is idempotent and preserves the selected runtime mode unless +`--runtime managed|vendored` is explicitly supplied. + +## Verification ```bash -bin/casan prompt trace +casan doctor +casan verify-harness +casan level show ``` -Only the second command proves that the individual prompt completed H1-H7 and has matching H6 project telemetry. +For Codex, open `/hooks`, review the exact project hook and trust its hash. + +For CI, verify the pin before executing governance gates: + +```bash +casan verify-harness +casan gate +``` + +## Uninstall + +```bash +casan uninstall +``` + +This removes CASAN hooks/config, CASAN-owned Gitea workflows, unchanged CASAN +scaffold files and vendored Core. User-authored hooks/workflows and modified +project files are retained. Add `--purge` to remove `.specify` logs/state, and +use `--remove-vscode-extension` only when the shared extension is no longer +needed by any project. diff --git a/install.ps1 b/install.ps1 index df5cf66..3d48d9d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -98,7 +98,7 @@ if ((Test-Path (Join-Path $selfHome "current")) -or (Test-Path (Join-Path $selfH } elseif ($env:CASAN_HOME) { $CasanHome = $env:CASAN_HOME } else { $CasanHome = Join-Path $env:LOCALAPPDATA "casan" } $cur = Join-Path $CasanHome "current" if (-not (Test-Path $cur)) { Write-Error "casan: no install at $cur (run install.ps1)"; exit 1 } -$env:CASAN_HARNESS_ROOT = Join-Path $cur "packages\casan-harness" +$env:CASAN_GLOBAL_HARNESS_ROOT = Join-Path $cur "packages\casan-harness" $env:CASAN_DEVKIT_ROOT = Join-Path $cur "packages\casan-devkit" $env:CASAN_INSTALL_ROOT = $cur if (-not $env:CASAN_APP_ROOT) { diff --git a/install.sh b/install.sh index b5d1d53..e12200d 100755 --- a/install.sh +++ b/install.sh @@ -131,7 +131,7 @@ else fi CUR="$CASAN_HOME/current" [[ -d "$CUR" ]] || { echo "casan: no install at $CUR (run install.sh)" >&2; exit 1; } -export CASAN_HARNESS_ROOT="$CUR/packages/casan-harness" +export CASAN_GLOBAL_HARNESS_ROOT="$CUR/packages/casan-harness" export CASAN_DEVKIT_ROOT="$CUR/packages/casan-devkit" export CASAN_INSTALL_ROOT="$CUR" # Project (app) root = nearest ancestor of CWD carrying a .casan/.specify marker. diff --git a/packages/casan-devkit/casan-init.py b/packages/casan-devkit/casan-init.py index 2a568dc..f6363df 100755 --- a/packages/casan-devkit/casan-init.py +++ b/packages/casan-devkit/casan-init.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""`casan init` / `casan verify-harness` (Plan-21 hybrid adoption). +"""`casan init` / `casan verify-harness` (Plan-21 production adoption). -Adopt CASAN into an EXISTING project by writing only per-project config — the -shared harness stays under $CASAN_HOME and is NOT copied into the repo. This is -the codegraph-style flow: global install once, then `casan init` per project. +Adopt CASAN into an EXISTING project using an explicit runtime contract: +managed mode pins the shared Core under $CASAN_HOME; vendored mode installs the +same production-only Core under `.casan/runtime/casan-core`. What init writes into the target repo: .casan/config.json project id, enforcement/integration mode, clients @@ -13,6 +13,8 @@ What init writes into the target repo: .specify/ runtime state root marker (logs/traces/admissions) .claude/settings.json Plan-20 Claude Code hooks (--client claude|all) .codex/hooks.json+config Plan-20 Codex hooks (--client codex|all) + .casan/runtime/casan-core + optional self-contained Core (--runtime vendored) `verify` recomputes the resolved harness gate-code hash and compares it to version.lock — the pin+VERIFY half. Drift/tamper of the global harness relative @@ -107,6 +109,8 @@ def _render_init(result): ("Location", result["target"]), ("Level", "%s (%s)" % ( result["target_level_name"].capitalize(), result["target_level"])), + ("Runtime", "%s — %s" % ( + result["runtime_mode"].capitalize(), result["runtime_path"])), ("Mode", result["enforcement_mode"]), ("Clients", _client_names(result["clients"])), ("Files", "%d created or updated" % len(result["created"])), @@ -149,6 +153,10 @@ def _render_level(result): result.get("project_target_level_name"), result.get("project_target_level"))) if result.get("project_target_level") else "not initialized"), + ("Runtime", ("%s — %s" % ( + result.get("project_runtime_mode"), + result.get("project_runtime_path"))) + if result.get("project_target_level") else "not initialized"), ("Status", str(result.get("project_level_status") or "unknown")), ]) print() @@ -270,7 +278,10 @@ def _copy_tree_missing(src_dir, dst_dir, target, created): def resolve_harness(explicit): - for cand in (explicit, os.environ.get("CASAN_HARNESS_ROOT")): + for cand in ( + explicit, + os.environ.get("CASAN_HARNESS_ROOT"), + os.environ.get("CASAN_GLOBAL_HARNESS_ROOT")): if cand and os.path.isdir(os.path.join(cand, "scripts", "bash")): return os.path.abspath(cand) install = os.environ.get("CASAN_INSTALL_ROOT") @@ -290,8 +301,9 @@ def install_root(harness): def harness_version(harness): - for p in (os.path.join(install_root(harness), "VERSION"), - os.path.join(harness, "..", "..", "VERSION")): + local_root = os.path.abspath(os.path.join(harness, "..", "..")) + for p in (os.path.join(local_root, "VERSION"), + os.path.join(install_root(harness), "VERSION")): try: with open(p, "r", encoding="utf-8") as fh: v = fh.read().strip() @@ -317,17 +329,38 @@ def compute_harness_hash(harness): """For PINNING at init: use the value recorded at install time if present (it equals a live compute of the same files), else compute live. Verify must NOT use this — it must call compute_live() to detect drift.""" - recorded = os.path.join(install_root(harness), ".harness-hash") - try: - with open(recorded, "r", encoding="utf-8") as fh: - v = fh.read().strip() - if v: - return v, "recorded" - except (OSError, IOError): - pass + local_root = os.path.abspath(os.path.join(harness, "..", "..")) + for recorded in ( + os.path.join(local_root, ".harness-hash"), + os.path.join(install_root(harness), ".harness-hash")): + try: + with open(recorded, "r", encoding="utf-8") as fh: + v = fh.read().strip() + if v: + return v, "recorded" + except (OSError, IOError): + continue return compute_live(harness) +def resolve_project_harness(target, explicit=None): + lock = _load_json_or( + os.path.join(target, ".casan", "version.lock"), {}) + runtime_path = lock.get("runtime_path") + if lock.get("runtime_mode") == "vendored" and isinstance(runtime_path, str): + root = ( + None if os.path.isabs(runtime_path) + else _safe_project_path(target, runtime_path) + ) + if not root: + return None + candidate = os.path.join(root, "packages", "casan-harness") + if os.path.isdir(os.path.join(candidate, "scripts", "bash")): + return os.path.abspath(candidate) + return None + return resolve_harness(explicit) + + def _write(path, text, backups): with owner_writable(path): if os.path.exists(path): @@ -366,8 +399,10 @@ def _safe_project_path(target, relative): if not normalized or os.path.isabs(str(relative)): return None candidate = os.path.abspath(os.path.join(target, *normalized.split("/"))) + target_real = os.path.realpath(target) + candidate_real = os.path.realpath(candidate) try: - if os.path.commonpath((target, candidate)) != target: + if os.path.commonpath((target_real, candidate_real)) != target_real: return None except ValueError: return None @@ -387,6 +422,110 @@ def _file_sha256(path): return digest.hexdigest() +def _install_vendored_core(target, source_harness): + """Atomically install the production Core runtime inside one project.""" + source_root = os.path.abspath(os.path.join(source_harness, "..", "..")) + copier = os.path.join(source_root, "scripts", "copy-runtime.py") + source_bin = os.path.join(source_root, "bin", "casan") + source_version = os.path.join(source_root, "VERSION") + for required in (source_bin, source_version): + if not os.path.isfile(required): + raise RuntimeError( + "vendored runtime source is incomplete; missing %s" % required) + + destination = _safe_project_path( + target, ".casan/runtime/casan-core") + if not destination: + raise RuntimeError( + "vendored runtime path escapes the project through a symlink") + parent = os.path.dirname(destination) + os.makedirs(parent, exist_ok=True) + staging = tempfile.mkdtemp(prefix=".casan-core-", dir=parent) + try: + if os.path.isfile(copier): + result = subprocess.run( + [ + sys.executable, + copier, + "--source-root", source_root, + "--destination-root", staging, + "--component", "harness", + "--clean", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if result.returncode != 0: + raise RuntimeError( + result.stderr.strip() or result.stdout.strip() or + "production runtime copy failed") + else: + shutil.copytree( + source_harness, + os.path.join(staging, "packages", "casan-harness")) + vendored_harness = os.path.join( + staging, "packages", "casan-harness") + forbidden = [ + relative for relative in ( + "tests", + "level5", + "scripts/bash/ci-harness-gate.sh", + "scripts/bash/security-gate.sh", + "scripts/bash/dashboard-server.py", + ) + if os.path.exists(os.path.join( + vendored_harness, *relative.split("/"))) + ] + if forbidden: + raise RuntimeError( + "source is not a production runtime; forbidden paths: %s" % + ", ".join(forbidden)) + os.makedirs(os.path.join(staging, "bin"), exist_ok=True) + shutil.copy2(source_bin, os.path.join(staging, "bin", "casan")) + shutil.copy2(source_version, os.path.join(staging, "VERSION")) + with open(os.path.join(staging, ".casan-level"), "w", + encoding="utf-8") as fh: + fh.write("core\n") + vendored_hash, hash_source = compute_live(vendored_harness) + if hash_source == "error": + raise RuntimeError( + "cannot verify staged vendored Core: %s" % vendored_hash) + with open(os.path.join(staging, ".harness-hash"), "w", + encoding="utf-8") as fh: + fh.write(vendored_hash + "\n") + if os.path.isdir(destination) and not os.path.islink(destination): + shutil.rmtree(destination) + elif os.path.exists(destination) or os.path.islink(destination): + os.unlink(destination) + os.replace(staging, destination) + staging = None + finally: + if staging and os.path.isdir(staging): + shutil.rmtree(staging) + + file_count = sum( + len(files) for _root, _dirs, files in os.walk(destination)) + return destination, file_count + + +def _remove_vendored_core(target): + destination = _safe_project_path( + target, ".casan/runtime/casan-core") + if not destination: + return False + if not os.path.exists(destination) and not os.path.islink(destination): + return False + with owner_writable(destination): + if os.path.isdir(destination) and not os.path.islink(destination): + shutil.rmtree(destination) + else: + os.unlink(destination) + _prune_empty_parents(destination, target) + return True + + def _backup_once(path, backups): if backups is None: return @@ -831,8 +970,38 @@ def cmd_init(args): except ValueError as error: sys.stderr.write("casan init: %s\n" % error) return 64 - version = harness_version(harness) - hhash, hsource = compute_harness_hash(harness) + + previous_config = _load_json_or( + os.path.join(target, ".casan", "config.json"), {}) + previous_runtime_mode = previous_config.get("runtime_mode") + runtime_mode = ( + args.runtime or + (previous_runtime_mode + if previous_runtime_mode in ("managed", "vendored") else "managed") + ) + runtime_removed = False + runtime_files = 0 + if runtime_mode == "vendored": + try: + runtime_root, runtime_files = _install_vendored_core( + target, harness) + except (OSError, RuntimeError) as error: + sys.stderr.write( + "casan init: cannot install vendored Core runtime: %s\n" % + error) + return 1 + active_harness = os.path.join( + runtime_root, "packages", "casan-harness") + runtime_path = os.path.relpath( + runtime_root, target).replace(os.sep, "/") + else: + runtime_removed = _remove_vendored_core(target) + active_harness = harness + runtime_root = install_root(harness) + runtime_path = runtime_root + + version = harness_version(active_harness) + hhash, hsource = compute_harness_hash(active_harness) created = [] backups = [] previous_manifest = _load_json_or( @@ -889,7 +1058,11 @@ def cmd_init(args): "vscode-native-copilot": "unsupported_global_interception", }, "harness_version": version, - "adoption_model": "hybrid-global", + "adoption_model": ( + "managed-global" if runtime_mode == "managed" + else "vendored-project"), + "runtime_mode": runtime_mode, + "runtime_path": runtime_path, "target_level": lvl, "target_level_name": lvl_name, } @@ -903,7 +1076,9 @@ def cmd_init(args): "harness_hash": hhash, "hash_algo": "sha256", "hash_source": hsource, - "install_root": install_root(harness), + "install_root": runtime_root, + "runtime_mode": runtime_mode, + "runtime_path": runtime_path, "recorded_at": now_iso(), } p = os.path.join(cfg_dir, "version.lock") @@ -924,7 +1099,7 @@ def cmd_init(args): _write(p, "\n".join(env_lines), backups); created_add(p) # Stable project-local bootstrap. It loads the config above, resolves the - # global harness and verifies version.lock before dispatching an adapter. + # managed/vendored Core and verifies version.lock before dispatching. bootstrap_source = os.path.join(devkit_root(), "templates", "project", "casan-hook.py") bootstrap_target = os.path.join(cfg_dir, "casan-hook.py") @@ -1067,6 +1242,10 @@ def cmd_init(args): "target_level_name": lvl_name, "harness_version": version, "harness_hash": hhash, + "runtime_mode": runtime_mode, + "runtime_path": runtime_path, + "runtime_files": runtime_files, + "runtime_removed": runtime_removed, "enforcement_mode": args.mode, "clients": clients, "created": created, @@ -1076,8 +1255,11 @@ def cmd_init(args): "level_extras": level_extras, "level_removed": level_removed, "level_retained": level_retained, - "note": ("harness NOT copied into repo (hybrid model); selected client hooks " - "MERGED and unselected CASAN hooks removed; run `casan doctor`"), + "note": ( + "managed runtime is referenced by version/hash lock" + if runtime_mode == "managed" else + "production-only Core runtime vendored under .casan/runtime/casan-core" + ), } _emit_json_or_human(args, result, _render_init) if legacy_migration.get("manual_review"): @@ -1096,11 +1278,11 @@ def cmd_init(args): def cmd_verify(args): - harness = resolve_harness(args.harness) - if not harness: - sys.stderr.write("casan verify-harness: cannot locate the harness.\n") - return 1 target = os.path.abspath(args.target or os.getcwd()) + harness = resolve_project_harness(target, args.harness) + if not harness: + sys.stderr.write("casan verify-harness: cannot locate the project runtime.\n") + return 1 lock_path = os.path.join(target, ".casan", "version.lock") if not os.path.exists(lock_path): sys.stderr.write("casan verify-harness: no .casan/version.lock (run `casan init` first).\n") @@ -1120,8 +1302,8 @@ def cmd_verify(args): } _emit_json_or_human(args, result, _render_verify) if not ok: - sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved harness does not match the " - "project pin. The global harness changed or was tampered.\n") + sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved Core runtime does not match " + "the project pin. The runtime changed or was tampered.\n") return 3 return 0 @@ -1144,6 +1326,8 @@ def cmd_level(args): "installed_level": installed, "project_target_level": tl, "project_target_level_name": cfg.get("target_level_name"), + "project_runtime_mode": cfg.get("runtime_mode", "managed"), + "project_runtime_path": cfg.get("runtime_path"), "project_level_status": status_map.get(tl, "unknown") if tl else None, "levels": { "1 core": "implemented — harness + gates + CLI", @@ -1217,7 +1401,7 @@ def cmd_doctor(args): else: clients = config.get("clients") or [] - harness = resolve_harness(args.harness) + harness = resolve_project_harness(target, args.harness) lock = _load_json_or(os.path.join(target, ".casan", "version.lock"), {}) expected = lock.get("harness_hash") actual, source = compute_live(harness) if harness else ("unavailable", "error") @@ -1502,6 +1686,8 @@ def cmd_uninstall(args): removed, retained = _remove_owned_project_artifacts( target, owned_files, owned_file_hashes) + if _remove_vendored_core(target): + removed.append(".casan/runtime/casan-core") for relative in ( ".casan/config.json", @@ -1567,6 +1753,11 @@ def main(argv=None): "(default auto: install when `code` is available)")) pi.add_argument("--level", default="core", help="packaging level to adopt: 1|core (default), 2|devkit, 3|platform (preview), 4|enterprise (refused)") + pi.add_argument( + "--runtime", choices=["managed", "vendored"], + help=("Core runtime placement: managed uses the pinned global install " + "(default for new adoption); vendored copies a production-only " + "Core into the project; re-init preserves the current mode")) pi.add_argument("--mode", choices=["observe", "enforce"], default="enforce", help="agentic policy mode (default: enforce; use observe for a telemetry-only pilot)") pi.add_argument("--integration-mode", dest="integration_mode", diff --git a/packages/casan-devkit/templates/project/casan-hook.py b/packages/casan-devkit/templates/project/casan-hook.py index 67cb56c..68d2fe5 100644 --- a/packages/casan-devkit/templates/project/casan-hook.py +++ b/packages/casan-devkit/templates/project/casan-hook.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -"""Project-local bootstrap for CASAN's globally installed agentic adapters. +"""Project-local bootstrap for CASAN's managed or vendored agentic adapters. This file is intentionally small and stdlib-only. It is the stable command -target committed by `casan init`; the policy implementation remains in the -versioned global CASAN installation. On every hook invocation it: +target committed by `casan init`; the policy implementation is resolved from +the project lock. On every hook invocation it: 1. locates the project and loads `.casan/config.json`; 2. applies the project's enforcement/integration settings to the process; -3. resolves the pinned global harness and verifies its live integrity hash; +3. resolves the pinned managed/vendored Core and verifies its live integrity hash; 4. dispatches stdin/stdout to the selected client adapter. The bootstrap never calls a model. @@ -46,8 +46,22 @@ def find_project_root(start): return None -def harness_candidates(lock): +def harness_candidates(lock, project_root): values = [] + runtime_path = lock.get("runtime_path") + if lock.get("runtime_mode") == "vendored": + if not isinstance(runtime_path, str) or os.path.isabs(runtime_path): + return values + normalized = os.path.normpath(runtime_path) + candidate = os.path.abspath(os.path.join(project_root, normalized)) + try: + inside_project = os.path.commonpath( + (project_root, candidate)) == project_root + except ValueError: + inside_project = False + if not inside_project: + return values + return [os.path.join(candidate, "packages", "casan-harness")] explicit = os.environ.get("CASAN_HARNESS_ROOT") if explicit: values.append(explicit) @@ -69,8 +83,8 @@ def harness_candidates(lock): return values -def resolve_harness(lock): - for candidate in harness_candidates(lock): +def resolve_harness(lock, project_root): + for candidate in harness_candidates(lock, project_root): root = os.path.abspath(os.path.expanduser(candidate)) if os.path.isfile(os.path.join(root, "scripts", "python", "agentic_bridge.py")): @@ -178,7 +192,7 @@ def main(argv=None): "vscode-copilot": "vscode"}.get(item, item) for item in enabled) lock = load_json(os.path.join(root, ".casan", "version.lock")) - harness = resolve_harness(lock) + harness = resolve_harness(lock, root) if not harness: return emit_failure(client, event, "pinned global harness not found", enforce) diff --git a/packages/casan-devkit/tests/hybrid-install-tests.sh b/packages/casan-devkit/tests/hybrid-install-tests.sh index 89af608..10b28b5 100755 --- a/packages/casan-devkit/tests/hybrid-install-tests.sh +++ b/packages/casan-devkit/tests/hybrid-install-tests.sh @@ -181,11 +181,14 @@ PID=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.jso MODE=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["enforcement_mode"])' 2>/dev/null) [[ "$MODE" == "enforce" ]] && pass "production init defaults to enforce mode" || fail "default mode is not enforce ($MODE)" DEFAULT_LEVEL=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["target_level"])' 2>/dev/null) +DEFAULT_RUNTIME=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["runtime_mode"])' 2>/dev/null) [[ "$DEFAULT_LEVEL" == "1" ]] \ + && [[ "$DEFAULT_RUNTIME" == "managed" ]] \ + && [ ! -d "$PROJ2/.casan/runtime" ] \ && [ ! -d "$PROJ2/.gitea" ] \ && [ ! -d "$PROJ2/apps" ] \ - && pass "existing-project init defaults to core without CI/domain scaffold" \ - || fail "default init did not stay at core (level=$DEFAULT_LEVEL)" + && pass "existing-project init defaults to managed Core without vendoring" \ + || fail "default init mode is unclear (level=$DEFAULT_LEVEL runtime=$DEFAULT_RUNTIME)" echo "===== ⑦ init MERGES into an existing shell (agents/skills/hooks preserved) =====" EXP="$WORK/existing"; mkdir -p "$EXP/.claude/agents" "$EXP/.claude/skills" "$EXP/.codex" @@ -254,6 +257,53 @@ LVL_DOWN=$( ( cd "$L2" && "$DKC" level show --json ) | python3 -c 'import json,s && pass "default init downgrades existing DevKit adoption to clean core" \ || fail "default init left Level 2 artifacts after core downgrade" +VENDORED="$WORK/vendored-core"; mkdir -p "$VENDORED" +VENDORED_OUT=$(cd "$VENDORED" && "$DKC" init --runtime vendored --project vendored-core --client claude --non-interactive) +VENDORED_ROOT="$VENDORED/.casan/runtime/casan-core" +echo "$VENDORED_OUT" | grep -q "Runtime.*Vendored" \ + && pass "vendored init clearly reports runtime placement" \ + || fail "vendored init output does not explain runtime mode ($VENDORED_OUT)" +[ -x "$VENDORED_ROOT/bin/casan" ] \ + && [ -f "$VENDORED_ROOT/packages/casan-harness/scripts/bash/casan-harness.sh" ] \ + && [ ! -d "$VENDORED_ROOT/packages/casan-harness/tests" ] \ + && [ ! -d "$VENDORED_ROOT/packages/casan-harness/level5" ] \ + && pass "vendored mode installs only production Core inside the project" \ + || fail "vendored Core layout is incomplete or contains source-only files" +( cd "$VENDORED" && "$VENDORED_ROOT/bin/casan" version >/dev/null ) \ + && pass "project-vendored Core includes a working local CLI" \ + || fail "project-vendored CLI cannot resolve its Core runtime" +( cd "$VENDORED" && "$DKC" verify-harness >/dev/null ) \ + && pass "global launcher resolves and verifies the project-vendored Core" \ + || fail "vendored Core is not honored by the global launcher" +( cd "$VENDORED" && "$DKC" doctor --client claude >/dev/null ) \ + && pass "project hooks execute against the verified vendored Core" \ + || fail "project hook cannot execute against vendored Core" +mv "$VENDORED_ROOT" "$VENDORED_ROOT.missing" +VENDORED_MISSING_RC=0 +( cd "$VENDORED" && "$DKC" version >/dev/null 2>&1 ) || VENDORED_MISSING_RC=$? +mv "$VENDORED_ROOT.missing" "$VENDORED_ROOT" +[ "$VENDORED_MISSING_RC" -ne 0 ] \ + && pass "vendored contract fails closed instead of falling back to global Core" \ + || fail "missing vendored Core silently fell back to the global runtime" +( cd "$VENDORED" && "$DKC" init --project vendored-core --client none --non-interactive >/dev/null ) +[ -d "$VENDORED_ROOT/packages/casan-harness" ] \ + && pass "re-init preserves the project's selected vendored runtime mode" \ + || fail "re-init silently changed the project's runtime mode" +( cd "$VENDORED" && "$DKC" init --runtime managed --project vendored-core --client none --non-interactive >/dev/null ) +[ ! -d "$VENDORED/.casan/runtime" ] \ + && pass "switching back to managed mode removes the old vendored runtime" \ + || fail "managed re-init left a stale vendored runtime" + +VENDORED_ESCAPE="$WORK/vendored-escape"; VENDORED_OUTSIDE="$WORK/vendored-outside" +mkdir -p "$VENDORED_ESCAPE/.casan" "$VENDORED_OUTSIDE" +ln -s "$VENDORED_OUTSIDE" "$VENDORED_ESCAPE/.casan/runtime" +VENDORED_ESCAPE_RC=0 +( cd "$VENDORED_ESCAPE" && "$DKC" init --runtime vendored --project vendored-escape --client none --non-interactive >/dev/null 2>&1 ) || VENDORED_ESCAPE_RC=$? +[ "$VENDORED_ESCAPE_RC" -ne 0 ] \ + && [ ! -e "$VENDORED_OUTSIDE/casan-core" ] \ + && pass "vendored install rejects a runtime path escaping through symlink" \ + || fail "vendored install followed an unsafe project symlink" + echo "===== ⑨ SAFETY: init refuses to adopt a CASAN source hub into itself =====" HUB="$WORK/fakehub" mkdir -p "$HUB/packages/casan-harness/scripts/bash" \ @@ -370,6 +420,9 @@ INIT_OUT=$(cd "$UN" && "$DKC" init --level devkit --project uninstall-project -- echo "$INIT_OUT" | grep -q "CASAN initialized" \ && pass "init defaults to concise human-readable output" \ || fail "init human output missing ($INIT_OUT)" +echo "$INIT_OUT" | grep -q "Runtime.*Managed" \ + && pass "init output clearly identifies managed runtime and path" \ + || fail "init output hides runtime placement ($INIT_OUT)" if echo "$INIT_OUT" | head -1 | grep -q '^[[:space:]]*{'; then fail "init still defaults to raw JSON" else @@ -432,6 +485,14 @@ PY && pass "legacy uninstall preserves user Gitea workflow and removes CASAN workflow" \ || fail "uninstall removed a user workflow or retained the CASAN workflow" +UN_VENDORED="$WORK/uninstall-vendored"; mkdir -p "$UN_VENDORED" +( cd "$UN_VENDORED" && "$DKC" init --runtime vendored --project uninstall-vendored --client none --non-interactive >/dev/null ) +( cd "$UN_VENDORED" && "$DKC" uninstall >/dev/null ) +[ ! -d "$UN_VENDORED/.casan/runtime" ] \ + && [ ! -f "$UN_VENDORED/.casan/config.json" ] \ + && pass "uninstall removes the complete project-vendored Core" \ + || fail "uninstall left vendored Core or active project config" + echo "" echo "===== HYBRID INSTALL SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1