feat: add production init wizard UX
This commit is contained in:
@@ -1,24 +1,45 @@
|
|||||||
# CASAN
|
# CASAN
|
||||||
|
|
||||||
CASAN là governance harness cho agentic coding. CASAN được cài một lần trên máy
|
CASAN là governance harness cho agentic coding. CASAN bổ sung policy gates,
|
||||||
developer, sau đó được liên kết vào từng repository bằng project hooks. CASAN
|
audit trail, evidence và kiểm tra integrity vào lifecycle của coding agent,
|
||||||
không thay thế IDE, coding agent hoặc workflow phát triển của project.
|
nhưng không thay thế IDE, model, agent, skill, slash command hay workflow hiện
|
||||||
|
có của repository.
|
||||||
|
|
||||||
Project có thể tiếp tục dùng nguyên trạng slash commands, agents, skills, review
|
Mô hình production mặc định:
|
||||||
loops và cấu trúc source hiện hữu. CASAN không áp đặt một pipeline hoặc số bước
|
|
||||||
cố định.
|
- Cài **DevKit một lần trên máy** để có launcher và lệnh quản trị.
|
||||||
|
- Chạy `casan init` trong từng repository.
|
||||||
|
- Project mới dùng **Core level + Managed runtime + Enforce mode** nếu người
|
||||||
|
dùng không chọn khác.
|
||||||
|
- Project offline, air-gapped hoặc cần tự chứa runtime có thể chọn
|
||||||
|
**Vendored runtime**.
|
||||||
|
|
||||||
|
`casan init` chỉ thêm lớp tích hợp cần thiết. Runtime phát hành không chứa test
|
||||||
|
suite, test scripts, internal CI runners, `level5`, dashboard lab, source docs
|
||||||
|
hay release tooling.
|
||||||
|
|
||||||
## Trạng thái sản phẩm
|
## Trạng thái sản phẩm
|
||||||
|
|
||||||
| Thành phần | Trạng thái | Phạm vi |
|
| Thành phần | Trạng thái | Phạm vi |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Core — Level 1 | Implemented | H1–H7 harness, hooks, policy gates, audit, evidence và CLI |
|
| Core — Level 1 | Implemented | H1–H7 harness, hooks, policy gates, audit, evidence và CLI |
|
||||||
| DevKit — Level 2 | Implemented | Core + `casan init`, domain-pack và CI template |
|
| DevKit — Level 2 | Implemented | Core + adoption tooling, domain-pack và CI template |
|
||||||
| Platform — Level 3 | Preview | Control Panel được deploy riêng, không được cài vào project bằng `casan init` |
|
| Platform — Level 3 | Preview | Control Panel deploy riêng; `casan init` chỉ áp dụng nền Level 2 |
|
||||||
| Enterprise — Level 4 | Chưa phát hành | Installer chủ động từ chối |
|
| Enterprise — Level 4 | Chưa phát hành | CLI chủ động từ chối, không giả lập tính năng |
|
||||||
|
|
||||||
Đối với repository đã có sẵn vỏ dự án, nên dùng **Level 1 cho project**. Chỉ
|
## Bốn quyết định cần phân biệt
|
||||||
chọn Level 2 khi project thực sự cần domain-pack và CI template của CASAN.
|
|
||||||
|
CASAN tách riêng bốn khái niệm để cấu hình rõ ràng:
|
||||||
|
|
||||||
|
| Quyết định | Lựa chọn | Mặc định production |
|
||||||
|
|---|---|---|
|
||||||
|
| Gói cài trên máy | `core`, `devkit`, `platform` | `devkit`, vì `casan init` thuộc DevKit |
|
||||||
|
| Capability của project | `--level core`, `devkit`, `platform` | `core` |
|
||||||
|
| Vị trí Core runtime | `--runtime managed`, `vendored` | `managed` |
|
||||||
|
| Cách thực thi policy | `--mode enforce`, `observe` | `enforce` |
|
||||||
|
|
||||||
|
`--level core` không có nghĩa Core phải nằm trong repository. Level mô tả
|
||||||
|
capability; runtime mô tả vị trí. Đây là hai quyết định độc lập.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -28,15 +49,15 @@ chọn Level 2 khi project thực sự cần domain-pack và CI template của C
|
|||||||
- Windows: PowerShell 5.1+, Python 3 và Git for Windows/Git Bash.
|
- Windows: PowerShell 5.1+, Python 3 và Git for Windows/Git Bash.
|
||||||
- Client tương ứng nếu cần: Claude Code, Codex hoặc VS Code.
|
- Client tương ứng nếu cần: Claude Code, Codex hoặc VS Code.
|
||||||
|
|
||||||
### 1. Cài CASAN một lần trên máy
|
### 1. Cài DevKit một lần trên máy
|
||||||
|
|
||||||
Từ checkout hoặc release bundle của CASAN:
|
Từ checkout hoặc release bundle đã được duyệt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS/Linux
|
# macOS/Linux
|
||||||
sh install.sh --level devkit
|
sh install.sh --level devkit
|
||||||
|
|
||||||
# Nếu launcher chưa nằm trên PATH
|
# Chỉ cần nếu terminal hiện tại chưa nhận launcher
|
||||||
export PATH="${CASAN_HOME:-$HOME/.casan}/bin:$PATH"
|
export PATH="${CASAN_HOME:-$HOME/.casan}/bin:$PATH"
|
||||||
|
|
||||||
casan version
|
casan version
|
||||||
@@ -50,74 +71,195 @@ pwsh .\install.ps1
|
|||||||
casan version
|
casan version
|
||||||
```
|
```
|
||||||
|
|
||||||
Gói global `devkit` được dùng vì nó chứa lệnh adoption `casan init`. Harness
|
Vị trí mặc định:
|
||||||
được cài mặc định tại:
|
|
||||||
|
|
||||||
- macOS/Linux: `~/.casan`
|
- macOS/Linux: `~/.casan`
|
||||||
- Windows: `%LOCALAPPDATA%\casan`
|
- Windows: `%LOCALAPPDATA%\casan`
|
||||||
|
|
||||||
Bản cài là runtime allowlist tối giản: không mang theo test suites, internal CI
|
Có thể đặt `CASAN_HOME` trước khi cài nếu tổ chức dùng một vị trí quản lý khác.
|
||||||
runners, thư mục legacy `level5`, Platform dashboard/local lab, source docs hay
|
`casan init` không hỏi một đường dẫn runtime tùy ý: Managed luôn dùng
|
||||||
release tooling. Source repository vẫn giữ tests để kiểm chứng chính CASAN.
|
installation đã resolve từ `CASAN_HOME`; Vendored luôn dùng
|
||||||
|
`.casan/runtime/casan-core` trong project.
|
||||||
|
|
||||||
### 2. Adopt vào repository hiện hữu
|
### 2. Khởi tạo CASAN trong repository
|
||||||
|
|
||||||
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
|
```bash
|
||||||
cd <project-root>
|
cd <project-root>
|
||||||
|
casan init
|
||||||
# Production mặc định: managed Core
|
|
||||||
casan init --client claude,codex --mode enforce
|
|
||||||
|
|
||||||
casan doctor
|
|
||||||
casan verify-harness
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`casan init` có menu chọn client khi chạy tương tác. Trong automation nên chỉ
|
Với project mới và terminal tương tác, wizard hỏi hai lựa chọn:
|
||||||
định rõ `--client`. Project hiện hữu mặc định dùng Level 1 (`core`); chỉ truyền
|
|
||||||
`--level devkit` khi muốn CASAN bổ sung CI template và domain-pack:
|
```text
|
||||||
|
Core runtime placement
|
||||||
|
|
||||||
|
1) Managed (Recommended)
|
||||||
|
Use the shared CASAN installation, pinned by version and hash.
|
||||||
|
Best for developer workstations and managed CI.
|
||||||
|
|
||||||
|
2) Vendored
|
||||||
|
Copy production-only Core into .casan/runtime/casan-core.
|
||||||
|
Best for offline, air-gapped, or self-contained repositories.
|
||||||
|
|
||||||
|
Select runtime [1]:
|
||||||
|
|
||||||
|
Client integrations
|
||||||
|
|
||||||
|
1) Claude Code
|
||||||
|
2) Codex
|
||||||
|
3) GitHub Copilot in VS Code via explicit @casan route
|
||||||
|
|
||||||
|
Select clients (comma-separated) [1,2]:
|
||||||
|
```
|
||||||
|
|
||||||
|
Nhấn Enter để dùng Managed và Claude Code + Codex. Nhập sai sẽ được hướng dẫn
|
||||||
|
chọn lại. Sau khi hoàn tất, output terminal là bản tóm tắt dễ đọc gồm project,
|
||||||
|
level, runtime, mode, client, file thay đổi và next steps.
|
||||||
|
|
||||||
|
Project đã init không bị hỏi lại runtime: CASAN giữ nguyên mode hiện tại. Muốn
|
||||||
|
đổi, truyền rõ `--runtime managed` hoặc `--runtime vendored`.
|
||||||
|
|
||||||
|
### 3. Kiểm tra readiness
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
casan init --level core --client claude
|
casan doctor
|
||||||
casan init --level core --client codex
|
casan verify-harness
|
||||||
casan init --level core --client claude,codex
|
casan level show
|
||||||
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
|
Với Codex, mở `/hooks`, review và trust đúng project hook sau lần init hoặc khi
|
||||||
thực tế. Chạy lại `init` giữ mode hiện tại; chỉ đổi khi truyền rõ
|
bootstrap hash thay đổi.
|
||||||
`--runtime managed` hoặc `--runtime vendored`.
|
|
||||||
|
|
||||||
Với Codex, sau init phải mở `/hooks`, kiểm tra và trust đúng project hook hash.
|
### 4. Dùng workflow hiện có
|
||||||
|
|
||||||
### 3. Dùng project bình thường
|
Tiếp tục dùng chat, agents, skills và slash commands của project như bình
|
||||||
|
thường. Không cần gọi một “CASAN agent” hoặc chạy một pipeline CASAN riêng.
|
||||||
|
|
||||||
Không cần gọi CASAN agent hoặc CASAN pipeline. Tiếp tục dùng workflow hiện hữu,
|
CASAN tham gia tại lifecycle của client đã enable:
|
||||||
ví dụ `/bd:boss`, `/bd:generation`, `/bd:review`, hoặc chat bình thường không
|
|
||||||
chỉ định agent.
|
|
||||||
|
|
||||||
CASAN tự tham gia vào lifecycle của client đã enable:
|
|
||||||
|
|
||||||
1. `UserPromptSubmit`: admission và quét prompt.
|
1. `UserPromptSubmit`: admission và quét prompt.
|
||||||
2. `PreToolUse`: kiểm tra tool input và chặn side effect không hợp lệ.
|
2. `PreToolUse`: kiểm tra tool input, chặn side effect không hợp lệ.
|
||||||
3. `PostToolUse`: ghi evidence của tool result.
|
3. `PostToolUse`: ghi evidence của tool result.
|
||||||
4. `Stop`: finalize trace, telemetry và trạng thái certification.
|
4. `Stop`: finalize trace, telemetry và trạng thái certification.
|
||||||
|
|
||||||
CASAN bridge không gọi model lần thứ hai. Claude Code/Codex vẫn là model
|
CASAN bridge không gọi model lần thứ hai. Claude Code hoặc Codex vẫn là model
|
||||||
executor duy nhất.
|
executor duy nhất.
|
||||||
|
|
||||||
|
## Dùng trong script và CI
|
||||||
|
|
||||||
|
Automation phải khai báo quyết định rõ ràng:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init \
|
||||||
|
--non-interactive \
|
||||||
|
--level core \
|
||||||
|
--runtime managed \
|
||||||
|
--mode enforce \
|
||||||
|
--client claude,codex
|
||||||
|
```
|
||||||
|
|
||||||
|
`--non-interactive` và `--json` không bao giờ chờ input. Nếu không truyền
|
||||||
|
`--runtime`, project mới mặc định Managed; nếu không truyền `--client`, mặc
|
||||||
|
định Claude Code + Codex để tương thích các bản trước.
|
||||||
|
|
||||||
|
Output mặc định dành cho người đọc. Chỉ dùng JSON khi một chương trình cần xử
|
||||||
|
lý dữ liệu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --non-interactive --client codex --json
|
||||||
|
casan doctor --json
|
||||||
|
casan verify-harness --json
|
||||||
|
casan level show --json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chọn runtime
|
||||||
|
|
||||||
|
### Managed — khuyến nghị cho đa số tổ chức
|
||||||
|
|
||||||
|
Managed dùng Core trong global installation và pin version/hash theo từng
|
||||||
|
project.
|
||||||
|
|
||||||
|
Ưu điểm:
|
||||||
|
|
||||||
|
- Repository nhẹ, không nhân bản runtime.
|
||||||
|
- Nâng cấp và rollback tập trung.
|
||||||
|
- Phù hợp workstation và CI runner được quản lý.
|
||||||
|
- Nhiều project có thể dùng chung một CASAN installation.
|
||||||
|
|
||||||
|
Yêu cầu:
|
||||||
|
|
||||||
|
- Máy developer/runner phải cài release CASAN mà project đã pin.
|
||||||
|
- Không thay runtime global tùy tiện; luôn chạy `casan verify-harness`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --runtime managed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vendored — cho môi trường tự chứa
|
||||||
|
|
||||||
|
Vendored copy Core production-only vào
|
||||||
|
`.casan/runtime/casan-core`. Bootstrap và launcher trong project resolve runtime
|
||||||
|
này trước, không âm thầm fallback sang global nếu runtime bị thiếu.
|
||||||
|
|
||||||
|
Ưu điểm:
|
||||||
|
|
||||||
|
- Có thể hoạt động offline hoặc air-gapped.
|
||||||
|
- Runtime đi cùng đúng project.
|
||||||
|
- Phù hợp repository được đóng gói thành một deliverable độc lập.
|
||||||
|
|
||||||
|
Đánh đổi:
|
||||||
|
|
||||||
|
- Repository/artifact lớn hơn.
|
||||||
|
- Mỗi project phải chủ động nhận bản vá và upgrade.
|
||||||
|
- Tổ chức phải quản lý quyền phân phối runtime theo license.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --runtime vendored
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bảng quyết định
|
||||||
|
|
||||||
|
| Tình huống | Runtime nên dùng |
|
||||||
|
|---|---|
|
||||||
|
| Máy developer và CI do công ty quản lý | Managed |
|
||||||
|
| Nhiều repository dùng chung CASAN | Managed |
|
||||||
|
| Cần rollout/rollback tập trung | Managed |
|
||||||
|
| Offline hoặc air-gapped | Vendored |
|
||||||
|
| Deliverable phải tự chứa toàn bộ Core | Vendored |
|
||||||
|
| Không thể đảm bảo CASAN đã cài trên runner | Vendored |
|
||||||
|
|
||||||
|
## Chọn project level
|
||||||
|
|
||||||
|
| Level | Dùng khi | Nội dung thêm vào project |
|
||||||
|
|---|---|---|
|
||||||
|
| `core` | Mặc định cho repository đã có cấu trúc | Governance Core, bootstrap, hooks và config |
|
||||||
|
| `devkit` | Cần CASAN domain-pack và CI template | Core + `.gitea/workflows/casan-ci.yml` + domain scaffold |
|
||||||
|
| `platform` | Đánh giá Platform preview | Áp dụng nền Level 2; Control Panel vẫn deploy riêng |
|
||||||
|
| `enterprise` | Chưa khả dụng | CLI từ chối |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --level core
|
||||||
|
casan init --level devkit
|
||||||
|
```
|
||||||
|
|
||||||
|
Level 1 Core không đưa test folders, test scripts, `level5` hoặc source-only
|
||||||
|
components vào project. Chỉ chọn DevKit khi thực sự cần scaffold bổ sung.
|
||||||
|
|
||||||
|
## Enforcement mode
|
||||||
|
|
||||||
|
| Mode | Dùng cho | Hành vi |
|
||||||
|
|---|---|---|
|
||||||
|
| `enforce` | Production | Side effect fail-closed; turn đủ evidence có thể certified |
|
||||||
|
| `observe` | Pilot và thu telemetry | Ghi nhận nhưng không chặn như production; luôn `observed_only` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --mode enforce
|
||||||
|
casan init --mode observe
|
||||||
|
```
|
||||||
|
|
||||||
|
Không gọi một turn là CASAN-certified nếu không có trace tương ứng hoặc trace bị
|
||||||
|
đánh dấu `observed_only`/`non_certified`.
|
||||||
|
|
||||||
## Client support
|
## Client support
|
||||||
|
|
||||||
| Client | Chat bình thường tự qua CASAN | Bước bắt buộc |
|
| Client | Chat bình thường tự qua CASAN | Bước bắt buộc |
|
||||||
@@ -126,224 +268,300 @@ executor duy nhất.
|
|||||||
| Codex CLI/extension | Có | Mở `/hooks`, review và trust hook hash |
|
| Codex CLI/extension | Có | Mở `/hooks`, review và trust hook hash |
|
||||||
| GitHub Copilot Chat | Không | Cài CASAN VSIX và gửi `@casan <prompt>` |
|
| GitHub Copilot Chat | Không | Cài CASAN VSIX và gửi `@casan <prompt>` |
|
||||||
|
|
||||||
|
Các cách chọn client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --client claude
|
||||||
|
casan init --client codex
|
||||||
|
casan init --client claude,codex
|
||||||
|
casan init --client vscode-copilot --vscode-install yes
|
||||||
|
casan init --client all
|
||||||
|
casan init --client none
|
||||||
|
```
|
||||||
|
|
||||||
GitHub Copilot không cung cấp public API để extension intercept toàn bộ built-in
|
GitHub Copilot không cung cấp public API để extension intercept toàn bộ built-in
|
||||||
chat. Chỉ route explicit `@casan` mới là CASAN-owned. Một backend tự gọi LLM API
|
chat. Chỉ route explicit `@casan` mới là CASAN-owned. Backend tự gọi LLM API
|
||||||
cũng không đi qua IDE hooks và cần adapter riêng.
|
cũng không đi qua IDE hooks và cần adapter riêng.
|
||||||
|
|
||||||
## Kiến trúc runtime
|
## Kiến trúc runtime
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
U["Developer"] --> C1["Claude Code"]
|
U["Developer"] --> C["Claude Code / Codex / @casan"]
|
||||||
U --> C2["Codex"]
|
C --> B["Project bootstrap<br/>.casan/casan-hook.py"]
|
||||||
U --> C3["VS Code: @casan"]
|
B --> R{"Runtime mode"}
|
||||||
|
R -- "Managed" --> GM["CASAN_HOME/current"]
|
||||||
C1 --> E1["Project hook events"]
|
R -- "Vendored" --> GV[".casan/runtime/casan-core"]
|
||||||
C2 --> E1
|
GM --> V{"Version + hash<br/>match version.lock?"}
|
||||||
C3 --> E2["CASAN-owned VSIX route"]
|
GV --> V
|
||||||
|
V -- "No" --> D["Deny or degrade<br/>according to enforcement mode"]
|
||||||
E1 --> B[".casan/casan-hook.py"]
|
|
||||||
E2 --> B
|
|
||||||
B --> V{"Global harness<br/>matches version.lock?"}
|
|
||||||
V -- "No" --> D["Deny or degrade<br/>according to mode"]
|
|
||||||
V -- "Yes" --> A["Client adapter"]
|
V -- "Yes" --> A["Client adapter"]
|
||||||
A --> G["Agentic bridge"]
|
A --> G["Agentic bridge"]
|
||||||
|
G --> L1["Admission"]
|
||||||
subgraph TURN["Per-turn lifecycle"]
|
L1 --> L2["Pre-tool gate"]
|
||||||
direction LR
|
L2 --> L3["Post-tool evidence"]
|
||||||
L1["Admission<br/>H1 + H4"] --> L2["Pre-tool gate<br/>H2 + H4"]
|
L3 --> L4["Finalize trace"]
|
||||||
L2 --> L3["Post-tool evidence<br/>H5"]
|
L4 --> S[".specify/logs + state"]
|
||||||
L3 --> L4["Finalize<br/>H3 + H5 + H6 + H7"]
|
L4 --> O["Native client result"]
|
||||||
end
|
|
||||||
|
|
||||||
G --> L1
|
|
||||||
L4 --> S["Project runtime state<br/>.specify/logs + state"]
|
|
||||||
L4 --> R["Native client result"]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cấu trúc cài đặt thực tế
|
Runtime resolution fail-closed: một project pin Vendored nhưng thiếu
|
||||||
|
`.casan/runtime/casan-core` sẽ báo lỗi và yêu cầu restore bằng release DevKit đã
|
||||||
|
duyệt, không tự chuyển sang Managed.
|
||||||
|
|
||||||
CASAN mặc định dùng managed runtime: policy code nằm ở global installation,
|
## Cấu trúc cài đặt
|
||||||
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
|
### Global installation
|
||||||
flowchart TB
|
|
||||||
subgraph M["Developer machine"]
|
|
||||||
H["CASAN_HOME"]
|
|
||||||
CUR["current<br/>symlink hoặc junction"]
|
|
||||||
VER["versions/<version>"]
|
|
||||||
CLI["bin/casan"]
|
|
||||||
HAR["packages/casan-harness"]
|
|
||||||
DEV["packages/casan-devkit"]
|
|
||||||
|
|
||||||
H --> CUR --> VER
|
```text
|
||||||
H --> CLI
|
CASAN_HOME/
|
||||||
VER --> HAR
|
├── bin/casan
|
||||||
VER --> DEV
|
├── current -> versions/<version>
|
||||||
end
|
└── versions/<version>/
|
||||||
|
├── VERSION
|
||||||
|
├── bin/casan
|
||||||
|
├── packages/casan-harness/
|
||||||
|
└── packages/casan-devkit/
|
||||||
|
```
|
||||||
|
|
||||||
INIT["casan init"] --> CFG
|
Release runtime là allowlist production-only. Tests và release tooling vẫn nằm
|
||||||
CLI --> INIT
|
trong source repository để kiểm chứng CASAN, nhưng không được copy vào
|
||||||
DEV --> INIT
|
installation hoặc project runtime.
|
||||||
|
|
||||||
subgraph P["Existing project"]
|
### Project sau `casan init`
|
||||||
CFG[".casan/<br/>config.json<br/>version.lock<br/>agentic.env<br/>init-manifest.json"]
|
|
||||||
BOOT[".casan/casan-hook.py"]
|
|
||||||
STATE[".specify/<br/>logs/<br/>state/<br/>.gitignore"]
|
|
||||||
CLIENTS["Client config khi được chọn<br/>.claude/settings.json<br/>.codex/hooks.json<br/>.vscode/extensions.json"]
|
|
||||||
L2["Level 2 only<br/>.gitea/workflows/casan-ci.yml<br/>apps/<project-id>/domain/"]
|
|
||||||
OWNED["Project-owned<br/>source, agents, skills,<br/>commands, hooks và CI khác"]
|
|
||||||
end
|
|
||||||
|
|
||||||
INIT --> BOOT
|
```text
|
||||||
INIT --> STATE
|
<project>/
|
||||||
INIT --> CLIENTS
|
├── .casan/
|
||||||
INIT -. "chỉ khi --level devkit" .-> L2
|
│ ├── config.json
|
||||||
INIT -. "không thay đổi" .-> OWNED
|
│ ├── version.lock
|
||||||
CFG --> BOOT
|
│ ├── casan-hook.py
|
||||||
HAR -. "runtime policy" .-> BOOT
|
│ ├── agentic.env
|
||||||
|
│ ├── init-manifest.json
|
||||||
|
│ └── runtime/casan-core/ # chỉ khi --runtime vendored
|
||||||
|
├── .specify/
|
||||||
|
│ ├── .gitignore
|
||||||
|
│ ├── logs/ # runtime, không commit
|
||||||
|
│ └── state/ # runtime, không commit
|
||||||
|
├── .claude/settings.json # khi enable Claude
|
||||||
|
├── .codex/hooks.json # khi enable Codex
|
||||||
|
├── .vscode/extensions.json # merge theo client
|
||||||
|
└── .gitea/workflows/casan-ci.yml # chỉ Level 2, nếu chưa có
|
||||||
```
|
```
|
||||||
|
|
||||||
### File nào được thay đổi
|
### File nào được thay đổi
|
||||||
|
|
||||||
| Path | Hành vi |
|
| Path | Hành vi |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `.casan/config.json` | Lưu project id, mode và danh sách client |
|
| `.casan/config.json` | Project id, level, runtime mode, enforcement mode và clients |
|
||||||
| `.casan/version.lock` | Pin version và SHA-256 của global harness |
|
| `.casan/version.lock` | Pin version và SHA-256 của Core runtime đã resolve |
|
||||||
| `.casan/casan-hook.py` | Bootstrap stdlib, verify pin rồi dispatch adapter |
|
| `.casan/casan-hook.py` | Stdlib bootstrap, verify pin rồi dispatch adapter |
|
||||||
| `.casan/agentic.env` | Compatibility/reference flags; runtime đọc `config.json` |
|
| `.casan/agentic.env` | Compatibility/reference flags; runtime đọc `config.json` |
|
||||||
| `.casan/init-manifest.json` | Ghi file đã tạo và backup |
|
| `.casan/init-manifest.json` | Danh sách file CASAN quản lý, checksum và backup |
|
||||||
| `.specify/logs`, `.specify/state` | Runtime trace, audit và state; không commit |
|
| `.casan/runtime/casan-core/` | Core production-only; chỉ có ở Vendored |
|
||||||
|
| `.specify/logs`, `.specify/state` | Trace, audit và state runtime; không commit |
|
||||||
| `.claude/settings.json` | Merge CASAN handlers khi enable Claude |
|
| `.claude/settings.json` | Merge CASAN handlers khi enable Claude |
|
||||||
| `.codex/hooks.json` | Merge CASAN handlers khi enable Codex |
|
| `.codex/hooks.json` | Merge CASAN handlers khi enable Codex |
|
||||||
| `.vscode/extensions.json` | Merge extension recommendations theo client |
|
| `.vscode/extensions.json` | Merge extension recommendations theo client |
|
||||||
| `.gitea/workflows/casan-ci.yml` | Chỉ Level 2, chỉ tạo khi chưa có |
|
| `.gitea/workflows/casan-ci.yml` | Chỉ Level 2; chỉ tạo khi chưa có |
|
||||||
| `apps/<project-id>/domain/` | Chỉ Level 2, chỉ bổ sung file còn thiếu |
|
| `apps/<project-id>/domain/` | Chỉ Level 2; chỉ bổ sung file còn thiếu |
|
||||||
|
|
||||||
Trước lần thay đổi đầu tiên, init tạo backup `<file>.casan-bak` cho file hiện
|
Trước lần thay đổi đầu tiên, init tạo `<file>.casan-bak` cho file hiện hữu.
|
||||||
hữu. `init-manifest.json` ghi lại các file và backup liên quan.
|
`init-manifest.json` giúp re-init và uninstall chỉ quản lý đúng tài sản CASAN.
|
||||||
|
|
||||||
### Nội dung luôn được giữ nguyên
|
### Nội dung luôn được bảo toàn
|
||||||
|
|
||||||
- Source code và cấu trúc ứng dụng.
|
- Source code và cấu trúc ứng dụng.
|
||||||
- `.claude/agents`, `.claude/skills`, `.claude/commands`.
|
- `.claude/agents`, `.claude/skills`, `.claude/commands`.
|
||||||
- Agents, skills, prompts và instructions trong `.github/`.
|
- Agents, skills, prompts và instructions trong `.github/`.
|
||||||
- Hook và JSON key không thuộc CASAN.
|
- Hook, JSON key và VS Code recommendation không thuộc CASAN.
|
||||||
- CI/workflow hiện hữu.
|
- CI/workflow hiện hữu.
|
||||||
- Vendored `packages/casan-harness` của project cũ; chỉ xóa sau khi đã migration
|
- Scaffold file đã được người dùng chỉnh sửa.
|
||||||
toàn bộ CI và scripts sang global harness.
|
- Backup `.casan-bak`.
|
||||||
|
|
||||||
Nếu target chính là CASAN source hub, init từ chối để tránh self-adoption. Không
|
CASAN source hub tự từ chối self-adoption. Không dùng `--force` trừ khi chủ động
|
||||||
dùng `--force` trừ khi chủ động muốn kiểm thử trường hợp này.
|
kiểm thử trường hợp này.
|
||||||
|
|
||||||
## Chọn mode
|
## Nên commit gì
|
||||||
|
|
||||||
| Mode | Dùng cho | Certification |
|
Thông thường nên commit:
|
||||||
|---|---|---|
|
|
||||||
| `enforce` | Mặc định production | Side effect fail-closed; turn đủ evidence có thể certified |
|
- `.casan/config.json`
|
||||||
| `observe` | Pilot và thu telemetry | Không chặn như production; luôn `observed_only` |
|
- `.casan/version.lock`
|
||||||
|
- `.casan/casan-hook.py`
|
||||||
|
- `.casan/agentic.env`
|
||||||
|
- `.casan/init-manifest.json`
|
||||||
|
- các client config đã merge
|
||||||
|
- Level 2 workflow/domain files sau khi review
|
||||||
|
|
||||||
|
Không commit:
|
||||||
|
|
||||||
|
- `.specify/logs/`
|
||||||
|
- `.specify/state/`
|
||||||
|
- file `.casan-bak`
|
||||||
|
|
||||||
|
Với Managed, không có runtime để commit. Với Vendored, tổ chức phải đưa
|
||||||
|
`.casan/runtime/casan-core` vào cùng deliverable bằng Git hoặc artifact channel
|
||||||
|
đã duyệt; nếu bỏ runtime này, project sẽ fail-closed. Quyết định phân phối phải
|
||||||
|
tuân theo license và policy nội bộ.
|
||||||
|
|
||||||
|
## Cấu hình lại
|
||||||
|
|
||||||
|
Chạy lại init với **toàn bộ danh sách client mong muốn**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
casan init --level core --client claude,codex --mode enforce
|
# Chỉ giữ Claude
|
||||||
|
casan init --client claude
|
||||||
|
|
||||||
|
# Tắt CASAN IDE integrations nhưng giữ config và evidence
|
||||||
|
casan init --client none
|
||||||
|
|
||||||
|
# Chuyển runtime có chủ đích
|
||||||
|
casan init --runtime vendored
|
||||||
|
casan init --runtime managed
|
||||||
```
|
```
|
||||||
|
|
||||||
Không gọi một turn là CASAN-certified nếu không có trace tương ứng hoặc trace bị
|
Client bị bỏ khỏi danh sách sẽ được gỡ CASAN handler; hook và config không thuộc
|
||||||
đánh dấu `observed_only`/`non_certified`.
|
CASAN vẫn được giữ. `--client none` không tự gỡ VS Code extension dùng chung.
|
||||||
|
|
||||||
## Kiểm tra, cấu hình lại và nâng cấp
|
## Nâng cấp và rollback
|
||||||
|
|
||||||
Kiểm tra project:
|
### Managed
|
||||||
|
|
||||||
```bash
|
1. Cài release CASAN đã duyệt trên workstation/runner.
|
||||||
casan doctor
|
2. Chạy lại `casan init --runtime managed` trong từng project để cập nhật
|
||||||
casan verify-harness
|
bootstrap và pin.
|
||||||
casan level show
|
|
||||||
```
|
|
||||||
|
|
||||||
- Output mặc định được tối ưu để đọc trực tiếp trong terminal. Thêm `--json`
|
|
||||||
sau command khi cần payload đầy đủ cho CI hoặc script, ví dụ
|
|
||||||
`casan doctor --json`.
|
|
||||||
- `doctor`: kiểm tra config, bootstrap, hook schema, adapter smoke test, VSIX và
|
|
||||||
cảnh báo trust.
|
|
||||||
- `verify-harness`: tính lại live hash và so với project pin; drift trả exit
|
|
||||||
code `3`.
|
|
||||||
- `level show`: hiển thị package level đã cài và target level của project.
|
|
||||||
|
|
||||||
Đổi danh sách client bằng cách chạy lại init với **toàn bộ danh sách mong muốn**.
|
|
||||||
CASAN handler của client bị bỏ khỏi danh sách sẽ được gỡ, còn hook khác được giữ:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
casan init --level core --client claude
|
|
||||||
|
|
||||||
# Tắt toàn bộ IDE integration của CASAN nhưng giữ config/state
|
|
||||||
casan init --level core --client none
|
|
||||||
```
|
|
||||||
|
|
||||||
`--client none` không uninstall VSIX đã cài trên máy; nếu không còn dùng route
|
|
||||||
`@casan`, gỡ extension `fpt-casan.casan-governed-chat` trong VS Code.
|
|
||||||
|
|
||||||
Gỡ CASAN khỏi project:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Gỡ project hooks và config CASAN; giữ hook người dùng và runtime evidence
|
|
||||||
casan uninstall
|
|
||||||
|
|
||||||
# Đồng thời xóa .specify/logs và .specify/state
|
|
||||||
casan uninstall --purge
|
|
||||||
|
|
||||||
# Chỉ dùng khi extension dùng chung không còn cần trên máy
|
|
||||||
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.
|
|
||||||
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:
|
|
||||||
|
|
||||||
1. Chạy lại installer từ release đã duyệt.
|
|
||||||
2. Chạy lại `casan init` trong từng project để cập nhật bootstrap và pin.
|
|
||||||
3. Chạy `casan doctor` và `casan verify-harness`.
|
3. Chạy `casan doctor` và `casan verify-harness`.
|
||||||
4. Với Codex, review/trust lại hook nếu hash thay đổi.
|
4. Với Codex, review/trust lại hook nếu hash thay đổi.
|
||||||
|
|
||||||
Trong production, không bỏ qua `HARNESS_INTEGRITY_DRIFT`.
|
### Vendored
|
||||||
|
|
||||||
## CI
|
1. Cài hoặc giải nén DevKit release đã duyệt trên máy thực hiện upgrade.
|
||||||
|
2. Chạy `casan init --runtime vendored`; CASAN refresh Core production-only
|
||||||
|
trong project.
|
||||||
|
3. Review thay đổi runtime, chạy `doctor` và `verify-harness`.
|
||||||
|
4. Phát hành lại deliverable Vendored.
|
||||||
|
|
||||||
Runner phải cài cùng release CASAN mà project đã pin. Gate tối thiểu:
|
Rollback dùng đúng release đã duyệt trước đó, chạy lại init và verify. Không bỏ
|
||||||
|
qua `HARNESS_INTEGRITY_DRIFT` trong production.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Gỡ hooks, config, CASAN workflow/scaffold còn nguyên checksum và Vendored Core
|
||||||
|
casan uninstall
|
||||||
|
|
||||||
|
# Đồng thời xóa runtime evidence
|
||||||
|
casan uninstall --purge
|
||||||
|
|
||||||
|
# Chỉ khi VSIX dùng chung không còn cần trên máy
|
||||||
|
casan uninstall --remove-vscode-extension
|
||||||
|
```
|
||||||
|
|
||||||
|
`casan uninstall`:
|
||||||
|
|
||||||
|
- gỡ CASAN handlers nhưng giữ handlers của project;
|
||||||
|
- xóa CASAN-owned `.gitea` workflow và dọn thư mục cha nếu đã rỗng;
|
||||||
|
- xóa `.casan/runtime/casan-core` nếu dùng Vendored;
|
||||||
|
- chỉ xóa scaffold file còn đúng checksum;
|
||||||
|
- giữ file người dùng đã sửa và `.casan-bak`;
|
||||||
|
- giữ `.specify/logs` và `.specify/state` trừ khi có `--purge`;
|
||||||
|
- không mặc định gỡ VSIX vì extension có thể được project khác dùng chung.
|
||||||
|
|
||||||
|
Sau uninstall, output liệt kê rõ mục đã xóa và mục được giữ lại.
|
||||||
|
|
||||||
|
## CI production
|
||||||
|
|
||||||
|
Gate tối thiểu:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
casan verify-harness
|
casan verify-harness
|
||||||
casan gate
|
casan gate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Managed runner phải cài đúng approved release trước khi chạy gate. Vendored
|
||||||
|
runner phải nhận `.casan/runtime/casan-core` cùng checkout/artifact; global
|
||||||
|
launcher vẫn có thể được dùng, nhưng runtime policy được resolve từ project.
|
||||||
|
|
||||||
Level 2 tạo `.gitea/workflows/casan-ci.yml` như một template nếu file chưa tồn
|
Level 2 tạo `.gitea/workflows/casan-ci.yml` như một template nếu file chưa tồn
|
||||||
tại. Template phải được review theo runner và mô hình cài đặt của tổ chức trước
|
tại. Luôn review template theo runner, secret model và runtime mode của tổ chức
|
||||||
khi enable; CASAN không ghi đè workflow hiện hữu.
|
trước khi enable. CASAN không ghi đè workflow hiện hữu.
|
||||||
|
|
||||||
Đảm bảo `.specify/logs/` và `.specify/state/` không được commit. Init chỉ tạo
|
Đảm bảo `.specify/logs/` và `.specify/state/` không được commit. Init chỉ tạo
|
||||||
`.specify/.gitignore` khi file đó chưa tồn tại.
|
`.specify/.gitignore` khi file đó chưa tồn tại.
|
||||||
|
|
||||||
|
## CLI tham khảo
|
||||||
|
|
||||||
|
| Lệnh | Mục đích |
|
||||||
|
|---|---|
|
||||||
|
| `casan init` | Adopt hoặc cấu hình lại CASAN trong project |
|
||||||
|
| `casan uninstall` | Gỡ CASAN-owned project integration an toàn |
|
||||||
|
| `casan doctor` | Kiểm tra config, bootstrap, pin, adapters và client readiness |
|
||||||
|
| `casan verify-harness` | So live Core hash với project pin |
|
||||||
|
| `casan level show` | Hiển thị installed level, project level và runtime |
|
||||||
|
| `casan gate` | Chạy production checks từ project manifest |
|
||||||
|
| `casan verify` | Verify audit chain, tool audit và policy bundle |
|
||||||
|
| `casan prompt verify` | Verify prompt-enforcement contract |
|
||||||
|
| `casan prompt trace <trace-id>` | Verify H1–H7 certification của một turn |
|
||||||
|
| `casan version` | Hiển thị phiên bản |
|
||||||
|
| `casan help` | Hiển thị toàn bộ command |
|
||||||
|
|
||||||
|
Các trạng thái quan trọng:
|
||||||
|
|
||||||
|
- `doctor` trả non-zero khi project chưa sẵn sàng.
|
||||||
|
- `verify-harness` trả exit code `3` khi phát hiện drift.
|
||||||
|
- Cú pháp/giá trị CLI không hợp lệ trả exit code `64`.
|
||||||
|
- JSON là opt-in qua `--json`; lỗi vận hành vẫn được viết ngắn gọn ra stderr.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `casan` không có trên PATH
|
||||||
|
|
||||||
|
Mở terminal mới hoặc thêm `${CASAN_HOME:-$HOME/.casan}/bin` vào `PATH`.
|
||||||
|
|
||||||
|
### Managed báo integrity drift
|
||||||
|
|
||||||
|
Cài đúng release đã pin, chạy lại `casan init --runtime managed`, sau đó
|
||||||
|
`casan verify-harness`. Không sửa `version.lock` thủ công để né kiểm tra.
|
||||||
|
|
||||||
|
### Vendored báo runtime missing
|
||||||
|
|
||||||
|
Từ một approved DevKit installation, chạy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --runtime vendored
|
||||||
|
casan verify-harness
|
||||||
|
```
|
||||||
|
|
||||||
|
### Codex chưa chạy hook
|
||||||
|
|
||||||
|
Mở `/hooks`, review project hook và trust hash hiện tại.
|
||||||
|
|
||||||
|
### GitHub Copilot chat không tự qua CASAN
|
||||||
|
|
||||||
|
Dùng explicit route `@casan <prompt>` và kiểm tra
|
||||||
|
`fpt-casan.casan-governed-chat` đã được cài.
|
||||||
|
|
||||||
## Cấu trúc source repository CASAN
|
## Cấu trúc source repository CASAN
|
||||||
|
|
||||||
| Path | Trách nhiệm |
|
| Path | Trách nhiệm |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `bin/casan` | CLI entrypoint |
|
| `bin/casan` | CLI entrypoint |
|
||||||
| `install.sh`, `install.ps1` | Global installers |
|
| `install.sh`, `install.ps1` | Global installers |
|
||||||
| `packages/casan-harness/` | Runtime controls, adapters, policies, evidence và tests |
|
| `packages/casan-harness/` | Runtime controls, adapters, policies, evidence và source tests |
|
||||||
| `packages/casan-devkit/` | Hybrid adoption, project bootstrap và templates |
|
| `packages/casan-devkit/` | Project adoption tooling và templates |
|
||||||
| `packages/casan-control-panel/` | Platform UI/API preview, deploy riêng |
|
| `packages/casan-control-panel/` | Platform UI/API preview, deploy riêng |
|
||||||
| `packaging/levels.json` | Nguồn sự thật cho package level và maturity |
|
| `packaging/levels.json` | Nguồn sự thật cho package level và maturity |
|
||||||
| `infra/` | Local/production deployment references |
|
| `infra/` | Local/production deployment references |
|
||||||
| `docs/` | Security, operations, packaging và design records |
|
| `docs/` | Security, operations, packaging và design records |
|
||||||
| `apps/` | Demo/validation applications; không phải runtime dependency của `casan init` |
|
| `apps/` | Demo/validation applications; không phải runtime dependency của `casan init` |
|
||||||
|
|
||||||
|
Source tree có tests để phát triển sản phẩm. Release packaging dùng allowlist để
|
||||||
|
chỉ đưa production runtime cần thiết vào global install hoặc Vendored Core.
|
||||||
|
|
||||||
## Tài liệu chi tiết
|
## Tài liệu chi tiết
|
||||||
|
|
||||||
- [Hybrid installation và migration](docs/casan/CASAN_INSTALL_HYBRID.md)
|
- [Production installation và migration](docs/casan/CASAN_INSTALL_HYBRID.md)
|
||||||
- [Agentic client security boundary](docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md)
|
- [Agentic client security boundary](docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md)
|
||||||
- [Windows client setup](docs/casan/CASAN_AGENTIC_CLIENTS_WINDOWS.md)
|
- [Windows client setup](docs/casan/CASAN_AGENTIC_CLIENTS_WINDOWS.md)
|
||||||
- [Packaging levels](docs/packaging/CASAN_PACKAGING_PLAN.md)
|
- [Packaging levels](docs/packaging/CASAN_PACKAGING_PLAN.md)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ casan — CASAN governance harness CLI ($(version))
|
|||||||
Usage: casan <command> [args]
|
Usage: casan <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
init [--runtime managed|vendored] Adopt CASAN Core into this project
|
init [--runtime managed|vendored] Adopt/reconfigure CASAN (interactive wizard by default)
|
||||||
uninstall [--purge] Remove CASAN from this project (preserves user config)
|
uninstall [--purge] Remove CASAN from this project (preserves user config)
|
||||||
doctor [--client ...] Verify configured hooks, pin, adapters, and VS Code route
|
doctor [--client ...] Verify configured hooks, pin, adapters, and VS Code route
|
||||||
level <show|set 1..4> Show / change the project's packaging level
|
level <show|set 1..4> Show / change the project's packaging level
|
||||||
|
|||||||
@@ -60,17 +60,19 @@ pwsh .\install.ps1
|
|||||||
|
|
||||||
cd C:\work\my-project
|
cd C:\work\my-project
|
||||||
|
|
||||||
# Menu chọn Claude / Codex / VS Code Copilot
|
# Project mới: menu chọn Managed/Vendored, sau đó chọn client
|
||||||
casan init
|
casan init
|
||||||
|
|
||||||
# Hoặc non-interactive, chọn chính xác client
|
# Hoặc non-interactive, chọn chính xác runtime và client
|
||||||
casan init --client claude,codex --mode enforce
|
casan init --non-interactive --runtime managed --client claude,codex --mode enforce
|
||||||
casan init --client vscode-copilot --mode enforce --vscode-install yes
|
casan init --non-interactive --runtime vendored --client vscode-copilot --mode enforce --vscode-install yes
|
||||||
```
|
```
|
||||||
|
|
||||||
`init` merge CASAN handlers vào config hiện hữu, ghi bootstrap
|
`init` merge CASAN handlers vào config hiện hữu, ghi bootstrap
|
||||||
`.casan\casan-hook.py`, pin harness global, và tạo VSIX `@casan` khi Copilot
|
`.casan\casan-hook.py`, pin Core runtime đã resolve, và tạo VSIX `@casan` khi
|
||||||
được chọn. Bootstrap tự load `config.json`; không cần source env thủ công.
|
Copilot được chọn. Managed dùng global installation; Vendored copy Core
|
||||||
|
production-only vào `.casan\runtime\casan-core`. Bootstrap tự load
|
||||||
|
`config.json`; không cần source env thủ công.
|
||||||
|
|
||||||
> Đường dẫn có dấu cách được xử lý đúng (ví dụ `C:\Users\Nguyen Van A\project`).
|
> Đường dẫn có dấu cách được xử lý đúng (ví dụ `C:\Users\Nguyen Van A\project`).
|
||||||
|
|
||||||
@@ -117,5 +119,5 @@ cho automation tương thích cũ.
|
|||||||
| Codex không chạy hook | Chưa qua trust review | Mở repo trong Codex, chấp nhận trust; chạy lại doctor |
|
| Codex không chạy hook | Chưa qua trust review | Mở repo trong Codex, chấp nhận trust; chạy lại doctor |
|
||||||
| Turn hiện `observed_only` | Đang ở observe mode hoặc client ngoài allowlist | Chạy lại `casan init --mode enforce --client ...` |
|
| Turn hiện `observed_only` | Đang ở observe mode hoặc client ngoài allowlist | Chạy lại `casan init --mode enforce --client ...` |
|
||||||
| Token/cost là `null` | Client chưa cấp nguồn usage đáng tin | Đúng theo thiết kế — không bịa số; `telemetry_quality=partial` |
|
| Token/cost là `null` | Client chưa cấp nguồn usage đáng tin | Đúng theo thiết kế — không bịa số; `telemetry_quality=partial` |
|
||||||
| `casan doctor` báo integrity/bootstrap missing | Global install hoặc project init chưa đầy đủ | Chạy lại `install.ps1`, rồi `casan init` |
|
| `casan doctor` báo integrity/bootstrap missing | Runtime đã chọn hoặc project init chưa đầy đủ | Managed: chạy lại `install.ps1`; Vendored: chạy `casan init --runtime vendored`; sau đó chạy doctor |
|
||||||
| Mọi turn hiện `observed_only` trên Windows | Không có `bash` (thiếu Git Bash) → gate H4/H2 không chạy | Cài Git for Windows hoặc set `CASAN_AGENTIC_BASH`; `doctor` sẽ báo `gates_runnable=true` |
|
| Mọi turn hiện `observed_only` trên Windows | Không có `bash` (thiếu Git Bash) → gate H4/H2 không chạy | Cài Git for Windows hoặc set `CASAN_AGENTIC_BASH`; `doctor` sẽ báo `gates_runnable=true` |
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ cục bộ), `CASAN_DIST_URL` (tải tarball), `CASAN_NO_PATH_LINK=1` (không t
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd <dự-án-của-bạn>
|
cd <dự-án-của-bạn>
|
||||||
casan init # mặc định project Level 1/core; interactive chọn client
|
casan init # project mới: interactive chọn runtime rồi client
|
||||||
# hoặc chọn level áp dụng cho project:
|
# hoặc chọn level áp dụng cho project:
|
||||||
casan init --level 1 --project my-app --client claude
|
casan init --level 1 --project my-app --client claude
|
||||||
casan init --level 2 --project my-app --client claude,codex
|
casan init --level 2 --project my-app --client claude,codex
|
||||||
@@ -74,8 +74,14 @@ casan level show # xem level đã cài + level project
|
|||||||
casan level set 2 # đổi level project (không cần init lại)
|
casan level set 2 # đổi level project (không cần init lại)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Project mới chạy trong terminal tương tác sẽ được hỏi vị trí Core trước:
|
||||||
|
`Managed (Recommended)` hoặc `Vendored`, sau đó mới chọn client. Nhấn Enter dùng
|
||||||
|
Managed và Claude+Codex. Nhập sai sẽ được hỏi lại thay vì kết thúc bằng payload
|
||||||
|
khó đọc.
|
||||||
|
|
||||||
`init` và `level show` luôn in runtime mode/path. Project mới mặc định
|
`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:
|
`managed`; chạy lại init giữ nguyên mode đã chọn và không hỏi lại runtime.
|
||||||
|
Chuyển mode phải explicit:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
casan init --runtime managed
|
casan init --runtime managed
|
||||||
@@ -111,7 +117,7 @@ file đã chỉnh sửa được giữ lại. Thêm `--purge` để xóa cả ru
|
|||||||
| `.casan/config.json` | project id, enforcement/integration mode, clients |
|
| `.casan/config.json` | project id, enforcement/integration mode, clients |
|
||||||
| `.casan/version.lock` | **pin** harness version + hash gate-code |
|
| `.casan/version.lock` | **pin** harness version + hash gate-code |
|
||||||
| `.casan/agentic.env` | feature flags bridge Plan-20 |
|
| `.casan/agentic.env` | feature flags bridge Plan-20 |
|
||||||
| `.casan/casan-hook.py` | bootstrap stdlib: load config, resolve + verify harness global, dispatch adapter |
|
| `.casan/casan-hook.py` | bootstrap stdlib: load config, resolve + verify Core managed/vendored, dispatch adapter |
|
||||||
| `.specify/` | thư mục state runtime (logs/trace/admission) |
|
| `.specify/` | thư mục state runtime (logs/trace/admission) |
|
||||||
| `.claude/settings.json` | hook Claude Code (Plan-20) |
|
| `.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` |
|
| `.codex/hooks.json` | hook Codex theo schema hiện hành; cần review/trust bằng `/hooks` |
|
||||||
@@ -120,8 +126,16 @@ file đã chỉnh sửa được giữ lại. Thêm `--purge` để xóa cả ru
|
|||||||
|
|
||||||
Tham số `--client` có thể lặp hoặc comma-separated:
|
Tham số `--client` có thể lặp hoặc comma-separated:
|
||||||
`claude`, `codex`, `vscode-copilot`, `all`, `none`. Khi chạy `casan init` trực
|
`claude`, `codex`, `vscode-copilot`, `all`, `none`. Khi chạy `casan init` trực
|
||||||
tiếp trong terminal, CLI hiển thị menu chọn. Trong automation không có TTY,
|
tiếp trong terminal, CLI hiển thị menu chọn. Trong automation, dùng
|
||||||
mặc định tương thích ngược là `claude,codex`.
|
`--non-interactive` và khai báo rõ runtime/client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --non-interactive --level core --runtime managed \
|
||||||
|
--mode enforce --client claude,codex
|
||||||
|
```
|
||||||
|
|
||||||
|
`--non-interactive`, `--json` hoặc môi trường không có TTY không bao giờ chờ
|
||||||
|
input. Khi không truyền lựa chọn, mặc định là Managed và `claude,codex`.
|
||||||
|
|
||||||
Các command dành cho người vận hành (`init`, `doctor`, `verify-harness`,
|
Các command dành cho người vận hành (`init`, `doctor`, `verify-harness`,
|
||||||
`level show`, `uninstall`) mặc định in bản tóm tắt dễ đọc. Thêm `--json` sau
|
`level show`, `uninstall`) mặc định in bản tóm tắt dễ đọc. Thêm `--json` sau
|
||||||
@@ -139,8 +153,9 @@ telemetry-only. `--integration-mode` nhận
|
|||||||
hiện tại.
|
hiện tại.
|
||||||
|
|
||||||
Bootstrap `.casan/casan-hook.py` tự đọc `config.json`; developer không còn phải
|
Bootstrap `.casan/casan-hook.py` tự đọc `config.json`; developer không còn phải
|
||||||
source `.casan/agentic.env` trước khi mở IDE. Mỗi invocation kiểm tra live hash
|
source `.casan/agentic.env` trước khi mở IDE. Mỗi invocation resolve Core theo
|
||||||
của harness global so với `version.lock` trước khi chạy adapter.
|
runtime mode rồi kiểm tra live hash so với `version.lock` trước khi chạy
|
||||||
|
adapter.
|
||||||
|
|
||||||
### Project đã có `.claude`, `.github`, agents, skills hoặc CASAN vendored
|
### Project đã có `.claude`, `.github`, agents, skills hoặc CASAN vendored
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ What init writes into the target repo:
|
|||||||
optional self-contained Core (--runtime vendored)
|
optional self-contained Core (--runtime vendored)
|
||||||
|
|
||||||
`verify` recomputes the resolved harness gate-code hash and compares it to
|
`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
|
version.lock — the pin+VERIFY half. Drift/tamper of the managed or vendored
|
||||||
to what the project pinned is caught here (preserves the Plan-16 "gates are
|
runtime relative to what the project pinned is caught here (preserves the
|
||||||
trusted code" guarantee even though the harness lives outside the repo).
|
Plan-16 "gates are trusted code" guarantee).
|
||||||
|
|
||||||
stdlib-only. Resolves the harness via CASAN_HARNESS_ROOT (set by the global
|
stdlib-only. Resolves the harness via CASAN_HARNESS_ROOT (set by the global
|
||||||
launcher) or --harness.
|
launcher) or --harness.
|
||||||
@@ -64,6 +64,7 @@ CLIENT_LABELS = {
|
|||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"vscode-copilot": "VS Code / @casan",
|
"vscode-copilot": "VS Code / @casan",
|
||||||
}
|
}
|
||||||
|
RUNTIME_MODES = ("managed", "vendored")
|
||||||
|
|
||||||
|
|
||||||
def _color(code, text):
|
def _color(code, text):
|
||||||
@@ -806,25 +807,61 @@ def migrate_vendored_prompt_contract(target, clients, backups):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def select_clients(values, interactive):
|
def _normalize_runtime_mode(value):
|
||||||
"""Normalize repeatable/comma-separated selections.
|
aliases = {
|
||||||
|
"1": "managed",
|
||||||
|
"global": "managed",
|
||||||
|
"shared": "managed",
|
||||||
|
"2": "vendored",
|
||||||
|
"local": "vendored",
|
||||||
|
"offline": "vendored",
|
||||||
|
"self-contained": "vendored",
|
||||||
|
"selfcontained": "vendored",
|
||||||
|
}
|
||||||
|
normalized = aliases.get(str(value).strip().lower(),
|
||||||
|
str(value).strip().lower())
|
||||||
|
if normalized not in RUNTIME_MODES:
|
||||||
|
raise ValueError(
|
||||||
|
"unknown runtime %r (choose managed or vendored)" % value)
|
||||||
|
return normalized
|
||||||
|
|
||||||
A plain interactive `casan init` presents the requested IDE menu. In
|
|
||||||
non-interactive automation, the historical Claude+Codex default is kept.
|
def select_runtime_mode(explicit, previous, interactive):
|
||||||
|
"""Resolve Core placement without surprising existing projects.
|
||||||
|
|
||||||
|
An explicit flag always wins. Re-init keeps a valid existing selection.
|
||||||
|
Only a new interactive adoption opens the placement wizard; automation
|
||||||
|
defaults to managed and never waits for input.
|
||||||
"""
|
"""
|
||||||
if not values:
|
if explicit:
|
||||||
if interactive:
|
return _normalize_runtime_mode(explicit)
|
||||||
|
if previous in RUNTIME_MODES:
|
||||||
|
return previous
|
||||||
|
if not interactive:
|
||||||
|
return "managed"
|
||||||
|
|
||||||
|
while True:
|
||||||
|
sys.stderr.write(
|
||||||
|
"\nCore runtime placement\n\n"
|
||||||
|
" 1) Managed (Recommended)\n"
|
||||||
|
" Use the shared CASAN installation, pinned by version and hash.\n"
|
||||||
|
" Best for developer workstations and managed CI.\n\n"
|
||||||
|
" 2) Vendored\n"
|
||||||
|
" Copy production-only Core into .casan/runtime/casan-core.\n"
|
||||||
|
" Best for offline, air-gapped, or self-contained repositories.\n\n"
|
||||||
|
"Select runtime [1]: ")
|
||||||
|
sys.stderr.flush()
|
||||||
|
answer = sys.stdin.readline()
|
||||||
|
if not answer or not answer.strip():
|
||||||
|
return "managed"
|
||||||
|
try:
|
||||||
|
return _normalize_runtime_mode(answer)
|
||||||
|
except ValueError:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"\nEnable CASAN integrations (comma-separated numbers):\n"
|
"Invalid selection. Enter 1 for Managed or 2 for Vendored.\n")
|
||||||
" 1) Claude Code (CLI + official VS Code extension)\n"
|
|
||||||
" 2) Codex (CLI + official VS Code extension)\n"
|
|
||||||
" 3) GitHub Copilot in VS Code via explicit @casan route\n"
|
def _normalize_clients(values):
|
||||||
"Selection [1,2]: ")
|
|
||||||
sys.stderr.flush()
|
|
||||||
answer = sys.stdin.readline().strip() or "1,2"
|
|
||||||
values = [answer]
|
|
||||||
else:
|
|
||||||
values = ["claude,codex"]
|
|
||||||
aliases = {
|
aliases = {
|
||||||
"1": "claude",
|
"1": "claude",
|
||||||
"2": "codex",
|
"2": "codex",
|
||||||
@@ -852,6 +889,35 @@ def select_clients(values, interactive):
|
|||||||
return selected
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def select_clients(values, interactive):
|
||||||
|
"""Normalize repeatable/comma-separated selections.
|
||||||
|
|
||||||
|
A plain interactive `casan init` presents the requested IDE menu. In
|
||||||
|
non-interactive automation, the historical Claude+Codex default is kept.
|
||||||
|
"""
|
||||||
|
if values:
|
||||||
|
return _normalize_clients(values)
|
||||||
|
if not interactive:
|
||||||
|
return _normalize_clients(["claude,codex"])
|
||||||
|
|
||||||
|
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"
|
||||||
|
" 3) GitHub Copilot in VS Code via explicit @casan route\n\n"
|
||||||
|
"Select clients (comma-separated) [1,2]: ")
|
||||||
|
sys.stderr.flush()
|
||||||
|
answer = sys.stdin.readline()
|
||||||
|
values = [answer.strip() or "1,2"] if answer else ["1,2"]
|
||||||
|
try:
|
||||||
|
return _normalize_clients(values)
|
||||||
|
except ValueError:
|
||||||
|
sys.stderr.write(
|
||||||
|
"Invalid selection. Enter 1, 2, 3, a comma-separated list, "
|
||||||
|
"`all`, or `none`.\n")
|
||||||
|
|
||||||
|
|
||||||
def merge_vscode_recommendations(target_file, clients, backups):
|
def merge_vscode_recommendations(target_file, clients, backups):
|
||||||
existed = os.path.exists(target_file)
|
existed = os.path.exists(target_file)
|
||||||
doc = _load_json_or(target_file, {}) if existed else {}
|
doc = _load_json_or(target_file, {}) if existed else {}
|
||||||
@@ -965,20 +1031,18 @@ def cmd_init(args):
|
|||||||
preview = (lvl == 3) # platform is a separate preview SERVICE; init applies the L2 base
|
preview = (lvl == 3) # platform is a separate preview SERVICE; init applies the L2 base
|
||||||
apply_devkit = (lvl >= 2)
|
apply_devkit = (lvl >= 2)
|
||||||
|
|
||||||
try:
|
|
||||||
clients = select_clients(args.client, sys.stdin.isatty() and not args.non_interactive)
|
|
||||||
except ValueError as error:
|
|
||||||
sys.stderr.write("casan init: %s\n" % error)
|
|
||||||
return 64
|
|
||||||
|
|
||||||
previous_config = _load_json_or(
|
previous_config = _load_json_or(
|
||||||
os.path.join(target, ".casan", "config.json"), {})
|
os.path.join(target, ".casan", "config.json"), {})
|
||||||
previous_runtime_mode = previous_config.get("runtime_mode")
|
previous_runtime_mode = previous_config.get("runtime_mode")
|
||||||
runtime_mode = (
|
interactive = (
|
||||||
args.runtime or
|
sys.stdin.isatty() and not args.non_interactive and not args.json)
|
||||||
(previous_runtime_mode
|
try:
|
||||||
if previous_runtime_mode in ("managed", "vendored") else "managed")
|
runtime_mode = select_runtime_mode(
|
||||||
)
|
args.runtime, previous_runtime_mode, interactive)
|
||||||
|
clients = select_clients(args.client, interactive)
|
||||||
|
except ValueError as error:
|
||||||
|
sys.stderr.write("casan init: %s\n" % error)
|
||||||
|
return 64
|
||||||
runtime_removed = False
|
runtime_removed = False
|
||||||
runtime_files = 0
|
runtime_files = 0
|
||||||
if runtime_mode == "vendored":
|
if runtime_mode == "vendored":
|
||||||
@@ -1734,10 +1798,25 @@ def cmd_uninstall(args):
|
|||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
parser = argparse.ArgumentParser(prog="casan-init", description="CASAN hybrid adoption")
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="casan-init",
|
||||||
|
description="Adopt CASAN governance into an existing project")
|
||||||
sub = parser.add_subparsers(dest="cmd")
|
sub = parser.add_subparsers(dest="cmd")
|
||||||
|
|
||||||
pi = sub.add_parser("init", help="adopt CASAN into the current project (config only)")
|
pi = sub.add_parser(
|
||||||
|
"init",
|
||||||
|
help="adopt or reconfigure CASAN in the current project",
|
||||||
|
description=(
|
||||||
|
"Adopt CASAN governance into an existing project. New interactive "
|
||||||
|
"projects are guided through runtime and client selection; existing "
|
||||||
|
"projects preserve their current runtime unless --runtime is set."),
|
||||||
|
epilog=(
|
||||||
|
"examples:\n"
|
||||||
|
" casan init\n"
|
||||||
|
" casan init --runtime managed --client claude,codex\n"
|
||||||
|
" casan init --runtime vendored --client codex\n"
|
||||||
|
" casan init --non-interactive --runtime managed --client none"),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
pi.add_argument("--target", help="project root (default: cwd)")
|
pi.add_argument("--target", help="project root (default: cwd)")
|
||||||
pi.add_argument("--project", help="project id (^[a-z][a-z0-9-]{1,62}$; default: dir name)")
|
pi.add_argument("--project", help="project id (^[a-z][a-z0-9-]{1,62}$; default: dir name)")
|
||||||
pi.add_argument(
|
pi.add_argument(
|
||||||
@@ -1746,7 +1825,8 @@ def main(argv=None):
|
|||||||
"vscode-copilot, all, none. Interactive init shows a menu."))
|
"vscode-copilot, all, none. Interactive init shows a menu."))
|
||||||
pi.add_argument(
|
pi.add_argument(
|
||||||
"--non-interactive", action="store_true",
|
"--non-interactive", action="store_true",
|
||||||
help="do not prompt; defaults to the backward-compatible claude,codex set")
|
help=("do not prompt; new projects default to managed runtime and the "
|
||||||
|
"backward-compatible claude,codex client set"))
|
||||||
pi.add_argument(
|
pi.add_argument(
|
||||||
"--vscode-install", choices=["auto", "yes", "no"], default="auto",
|
"--vscode-install", choices=["auto", "yes", "no"], default="auto",
|
||||||
help=("install the local CASAN @casan VSIX when vscode-copilot is selected "
|
help=("install the local CASAN @casan VSIX when vscode-copilot is selected "
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ assert not (root / "packages/casan-harness/level5").exists()
|
|||||||
assert not (root / "packages/casan-harness/scripts/bash/ci-harness-gate.sh").exists()
|
assert not (root / "packages/casan-harness/scripts/bash/ci-harness-gate.sh").exists()
|
||||||
for relative in ("docs/casan/CASAN_ADOPTION_WINDOWS.md", "docs/casan/CASAN_PROMPT_ENFORCEMENT.md"):
|
for relative in ("docs/casan/CASAN_ADOPTION_WINDOWS.md", "docs/casan/CASAN_PROMPT_ENFORCEMENT.md"):
|
||||||
text = (root / relative).read_text(encoding="utf-8")
|
text = (root / relative).read_text(encoding="utf-8")
|
||||||
assert "sample-project" in text
|
assert "casan init" in text
|
||||||
assert "Sample Project" in text or relative.endswith("CASAN_PROMPT_ENFORCEMENT.md")
|
assert "managed" in text.lower() and "vendored" in text.lower()
|
||||||
assert "__PROJECT_ID__" not in text and "__PROJECT_NAME__" not in text
|
assert "__PROJECT_ID__" not in text and "__PROJECT_NAME__" not in text
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,38 @@ assert registry["projects"] == []
|
|||||||
PY
|
PY
|
||||||
"$CASAN" version >/dev/null 2>&1 && pass "casan version works via launcher" || fail "casan version failed"
|
"$CASAN" version >/dev/null 2>&1 && pass "casan version works via launcher" || fail "casan version failed"
|
||||||
python3 - "$CASAN_HOME/current/packages/casan-devkit/casan-init.py" <<'PY' \
|
python3 - "$CASAN_HOME/current/packages/casan-devkit/casan-init.py" <<'PY' \
|
||||||
&& pass "client selector accepts menu numbers, aliases, repeats, and all" \
|
&& pass "init wizard resolves runtime and client choices safely" \
|
||||||
|| fail "client selector normalization failed"
|
|| fail "init wizard selection failed"
|
||||||
import importlib.util,sys
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
|
||||||
spec=importlib.util.spec_from_file_location("casan_init",sys.argv[1])
|
spec=importlib.util.spec_from_file_location("casan_init",sys.argv[1])
|
||||||
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
||||||
|
|
||||||
|
# Explicit flags and existing project configuration never open a prompt.
|
||||||
|
assert m.select_runtime_mode("vendored",None,True)=="vendored"
|
||||||
|
assert m.select_runtime_mode(None,"vendored",True)=="vendored"
|
||||||
|
assert m.select_runtime_mode(None,None,False)=="managed"
|
||||||
|
|
||||||
|
# Interactive defaults, aliases, validation, and retry behavior.
|
||||||
|
old_stdin=m.sys.stdin
|
||||||
|
try:
|
||||||
|
m.sys.stdin=io.StringIO("\n")
|
||||||
|
with contextlib.redirect_stderr(io.StringIO()):
|
||||||
|
assert m.select_runtime_mode(None,None,True)=="managed"
|
||||||
|
m.sys.stdin=io.StringIO("9\n2\n")
|
||||||
|
with contextlib.redirect_stderr(io.StringIO()) as errors:
|
||||||
|
assert m.select_runtime_mode(None,None,True)=="vendored"
|
||||||
|
assert "Invalid selection" in errors.getvalue()
|
||||||
|
m.sys.stdin=io.StringIO("bogus\n3\n")
|
||||||
|
with contextlib.redirect_stderr(io.StringIO()) as errors:
|
||||||
|
assert m.select_clients(None,True)==["vscode-copilot"]
|
||||||
|
assert "Invalid selection" in errors.getvalue()
|
||||||
|
finally:
|
||||||
|
m.sys.stdin=old_stdin
|
||||||
|
|
||||||
assert m.select_clients(["1,3"],False)==["claude","vscode-copilot"]
|
assert m.select_clients(["1,3"],False)==["claude","vscode-copilot"]
|
||||||
assert m.select_clients(["codex","copilot"],False)==["codex","vscode-copilot"]
|
assert m.select_clients(["codex","copilot"],False)==["codex","vscode-copilot"]
|
||||||
assert m.select_clients(["all"],False)==["claude","codex","vscode-copilot"]
|
assert m.select_clients(["all"],False)==["claude","codex","vscode-copilot"]
|
||||||
|
|||||||
Reference in New Issue
Block a user