feat: harden CASAN production install lifecycle
This commit is contained in:
@@ -29,18 +29,18 @@ policies:
|
|||||||
owner: agentops-team
|
owner: agentops-team
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- id: l5-tool-registry
|
- id: runtime-tool-registry
|
||||||
path: packages/casan-harness/level5/tool-registry.yaml
|
path: packages/casan-harness/config/tool-registry.yaml
|
||||||
owner: platform-team
|
owner: platform-team
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- id: l5-fallback
|
- id: runtime-fallback
|
||||||
path: packages/casan-harness/level5/model-fallback.yaml
|
path: packages/casan-harness/config/model-fallback.yaml
|
||||||
owner: platform-team
|
owner: platform-team
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- id: l5-drift
|
- id: runtime-drift
|
||||||
path: packages/casan-harness/level5/drift-policy.yaml
|
path: packages/casan-harness/config/drift-policy.yaml
|
||||||
owner: quality-team
|
owner: quality-team
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -27,20 +27,20 @@
|
|||||||
"sha256": "3b8b7d1b71dd654bf600c5366b012daf7dea6415e71b50688872526bfac5af33"
|
"sha256": "3b8b7d1b71dd654bf600c5366b012daf7dea6415e71b50688872526bfac5af33"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bytes": 2392,
|
"bytes": 2395,
|
||||||
"path": "packages/casan-harness/level5/tool-registry.yaml",
|
"path": "packages/casan-harness/config/tool-registry.yaml",
|
||||||
"sha256": "41be3e6a813e731213642f546184ec9e37601c93cd5e2eb9ee4833ad2d6c4634"
|
"sha256": "38221f5fb802368d7f6b6420529a529a7b425c1b63d21420c0264577275fea56"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bytes": 722,
|
"bytes": 714,
|
||||||
"path": "packages/casan-harness/level5/model-fallback.yaml",
|
"path": "packages/casan-harness/config/model-fallback.yaml",
|
||||||
"sha256": "731f98e9ca8182af9183de3711c0376c2e5daf38c769f27a5556d438aa53e2d7"
|
"sha256": "b010426af5fc3a3e8a7be54ea954713e97750ccfbe78006a66afd569aa2adc06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bytes": 389,
|
"bytes": 392,
|
||||||
"path": "packages/casan-harness/level5/drift-policy.yaml",
|
"path": "packages/casan-harness/config/drift-policy.yaml",
|
||||||
"sha256": "3d5ff7e5f06338d1a1673758ea33907ba0d5b19907ec8034d0e25f33ca248195"
|
"sha256": "59b5fe3ad38cef7a299f59788265d67a01ca4be0d1cea19a920378afb405490c"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generated_at": "2026-07-08T09:20:55Z"
|
"generated_at": "2026-07-24T04:05:50Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
*Bај–№Oк\Ѓ;YYВxсEфwI&°:Uf?ґ"DЋрЦБЗ`U"юТе)ЌюЏPњi‘~¬Џ?цъ~Ы‹ ЉЎІsB=ѕјПђ‚ЈґЅoм>UпFЪDС¶_+|N^M9ьесZЄ'МНKн‡пЌUѕ“џu@ЏПЎ°␍lА€ЃЏJъРчўВ'Н=ВмEт‰S[}•№FхПЙzЁ—>\PЫииХ-ЏQ6цфLЂу:Eџс‘ЈFЂБJА6ЉЃ¬кJe»И«ЖЩIПе=*Яц`я?В€ЯМёі4Зє^$ЬСэіTжrЎќ0Н> 3*D3К#’
|
^Ô«âqv_‚GZ¬áäſҾÕ+êç¹Ù-¬P_␍ãQ�÷ÄüQeÍt)?¯È‚3MXjøéWn␍ŒÝL¶>•µVßnSÁ¹¦þ
|
||||||
|
Ïšj9N<ëìÏlj6ŠÀ…¬g\>€ºH�P“ƒšc-™à‰`anë}Û+?m=ÆÈtBÚhzv;ðéí„PEå
|
||||||
|
?.Ë>®<ú‡ =ƆÇÙœ?üU/úíÚ\׸´ç¶·|©õ›‘,ª@2�0ÅŽ‹Ûv€æwé$Q iÇEèE¤á)†®ÿo k'Qÿ`bò5¹Ó‘ÆK ±HËí)³_b»�ˆû¥«x:I
|
||||||
@@ -30,7 +30,7 @@ COPY --from=builder /app/packages/casan-control-panel/backend/dist ./packages/ca
|
|||||||
COPY packages/casan-harness/scripts ./packages/casan-harness/scripts
|
COPY packages/casan-harness/scripts ./packages/casan-harness/scripts
|
||||||
COPY packages/casan-harness/config ./packages/casan-harness/config
|
COPY packages/casan-harness/config ./packages/casan-harness/config
|
||||||
COPY packages/casan-harness/security ./packages/casan-harness/security
|
COPY packages/casan-harness/security ./packages/casan-harness/security
|
||||||
COPY packages/casan-harness/level5/project-registry.json ./packages/casan-harness/level5/project-registry.json
|
COPY packages/casan-harness/config/project-registry.json ./packages/casan-harness/config/project-registry.json
|
||||||
COPY packages/casan-devkit ./packages/casan-devkit
|
COPY packages/casan-devkit ./packages/casan-devkit
|
||||||
|
|
||||||
# Read-only, build-time workspace snapshots. Goal orchestration resolves only roots
|
# Read-only, build-time workspace snapshots. Goal orchestration resolves only roots
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ Gói global `devkit` được dùng vì nó chứa lệnh adoption `casan init`.
|
|||||||
- 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
|
||||||
|
runners, thư mục legacy `level5`, Platform dashboard/local lab, source docs hay
|
||||||
|
release tooling. Source repository vẫn giữ tests để kiểm chứng chính CASAN.
|
||||||
|
|
||||||
### 2. Adopt vào repository hiện hữu
|
### 2. Adopt vào repository hiện hữu
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -239,6 +243,9 @@ casan verify-harness
|
|||||||
casan level show
|
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à
|
- `doctor`: kiểm tra config, bootstrap, hook schema, adapter smoke test, VSIX và
|
||||||
cảnh báo trust.
|
cảnh báo trust.
|
||||||
- `verify-harness`: tính lại live hash và so với project pin; drift trả exit
|
- `verify-harness`: tính lại live hash và so với project pin; drift trả exit
|
||||||
@@ -258,6 +265,23 @@ 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
|
`--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.
|
`@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` không tự xóa CI/domain template vì các file này có thể đã trở thành
|
||||||
|
source code của project, không tự xóa `.casan-bak`, và không mặc định gỡ VS Code
|
||||||
|
extension dùng chung cho các project khác.
|
||||||
|
|
||||||
Khi nâng cấp CASAN:
|
Khi nâng cấp CASAN:
|
||||||
|
|
||||||
1. Chạy lại installer từ release đã duyệt.
|
1. Chạy lại installer từ release đã duyệt.
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ if [[ -z "${HARNESS:-}" || ! -d "$HARNESS" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
BASH_DIR="$HARNESS/scripts/bash"
|
BASH_DIR="$HARNESS/scripts/bash"
|
||||||
TESTS_DIR="$HARNESS/tests"
|
|
||||||
CASAN_APP_ROOT="${CASAN_APP_ROOT:-$(cd "$HARNESS/../.." && pwd)}"
|
CASAN_APP_ROOT="${CASAN_APP_ROOT:-$(cd "$HARNESS/../.." && pwd)}"
|
||||||
VERSION_FILE="$_self/../VERSION"
|
VERSION_FILE="$_self/../VERSION"
|
||||||
[[ -f "$VERSION_FILE" ]] || VERSION_FILE="$HARNESS/../../VERSION"
|
[[ -f "$VERSION_FILE" ]] || VERSION_FILE="$HARNESS/../../VERSION"
|
||||||
@@ -43,12 +42,12 @@ Usage: casan <command> [args]
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
init [--client ...] [--project id] Adopt CASAN and select Claude/Codex/VS Code integrations
|
init [--client ...] [--project id] Adopt CASAN and select Claude/Codex/VS Code integrations
|
||||||
|
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
|
||||||
verify-harness Verify the resolved harness matches the project pin
|
verify-harness Verify the resolved harness matches the project pin
|
||||||
run <in> <out> [action] [-- cmd...] Run a step through the harness (H4→H5→H6→exec→H4-out)
|
run <in> <out> [action] [-- cmd...] Run a step through the harness (H4→H5→H6→exec→H4-out)
|
||||||
gate Run the full CI harness gate (all suites)
|
gate Run production checks from the project manifest
|
||||||
test Run the core harness test suite (run-casan4)
|
|
||||||
verify Verify audit chain + tool audit + policy bundle
|
verify Verify audit chain + tool audit + policy bundle
|
||||||
reuse Verify multi-project harness reuse (registry)
|
reuse Verify multi-project harness reuse (registry)
|
||||||
project validate [--manifest path] Validate a project manifest and quality profile
|
project validate [--manifest path] Validate a project manifest and quality profile
|
||||||
@@ -56,7 +55,7 @@ Commands:
|
|||||||
prompt verify Verify the adopted prompt-enforcement contract
|
prompt verify Verify the adopted prompt-enforcement contract
|
||||||
prompt trace <trace-id> Verify that a prompt trace is H1-H7 certified
|
prompt trace <trace-id> Verify that a prompt trace is H1-H7 certified
|
||||||
pipeline [--manifest path] Run the manifest-driven SRS→test pipeline
|
pipeline [--manifest path] Run the manifest-driven SRS→test pipeline
|
||||||
dashboard [port] Serve the AgentOps dashboard (default 8787)
|
dashboard [port] Serve the AgentOps dashboard (Platform only)
|
||||||
version Print version
|
version Print version
|
||||||
help This help
|
help This help
|
||||||
|
|
||||||
@@ -72,6 +71,9 @@ case "$cmd" in
|
|||||||
init)
|
init)
|
||||||
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: init requires the casan-devkit package" >&2; exit 1; }
|
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: init requires the casan-devkit package" >&2; exit 1; }
|
||||||
exec python3 "$DEVKIT_ROOT/casan-init.py" init "$@" ;;
|
exec python3 "$DEVKIT_ROOT/casan-init.py" init "$@" ;;
|
||||||
|
uninstall)
|
||||||
|
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: uninstall requires the casan-devkit package" >&2; exit 1; }
|
||||||
|
exec python3 "$DEVKIT_ROOT/casan-init.py" uninstall "$@" ;;
|
||||||
verify-harness)
|
verify-harness)
|
||||||
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: verify-harness requires the casan-devkit package" >&2; exit 1; }
|
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: verify-harness requires the casan-devkit package" >&2; exit 1; }
|
||||||
exec python3 "$DEVKIT_ROOT/casan-init.py" verify "$@" ;;
|
exec python3 "$DEVKIT_ROOT/casan-init.py" verify "$@" ;;
|
||||||
@@ -89,11 +91,12 @@ case "$cmd" in
|
|||||||
esac ;;
|
esac ;;
|
||||||
run) exec bash "$BASH_DIR/casan-harness.sh" "$@" ;;
|
run) exec bash "$BASH_DIR/casan-harness.sh" "$@" ;;
|
||||||
gate)
|
gate)
|
||||||
if [[ -n "${CASAN_PROJECT_MANIFEST:-}${CASAN_PROJECT_ID:-}" ]]; then
|
if [[ -z "${CASAN_PROJECT_MANIFEST:-}${CASAN_PROJECT_ID:-}" \
|
||||||
exec bash "$BASH_DIR/project-gate.sh" "$@"
|
&& ! -f "$CASAN_APP_ROOT/apps/okr/domain/project.manifest.json" ]]; then
|
||||||
|
echo "casan: gate requires CASAN_PROJECT_MANIFEST or CASAN_PROJECT_ID" >&2
|
||||||
|
exit 64
|
||||||
fi
|
fi
|
||||||
exec bash "$BASH_DIR/ci-harness-gate.sh" "$@" ;;
|
exec bash "$BASH_DIR/project-gate.sh" "$@" ;;
|
||||||
test) exec bash "$TESTS_DIR/run-casan4-harness-tests.sh" "$@" ;;
|
|
||||||
verify)
|
verify)
|
||||||
rc=0
|
rc=0
|
||||||
bash "$BASH_DIR/verify-audit-chain.sh" "$@" || rc=$?
|
bash "$BASH_DIR/verify-audit-chain.sh" "$@" || rc=$?
|
||||||
@@ -125,7 +128,12 @@ case "$cmd" in
|
|||||||
RUNNER="$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs"
|
RUNNER="$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs"
|
||||||
[[ -f "$RUNNER" ]] || { echo "casan: pipeline runner is not installed" >&2; exit 1; }
|
[[ -f "$RUNNER" ]] || { echo "casan: pipeline runner is not installed" >&2; exit 1; }
|
||||||
exec node "$RUNNER" "$@" ;;
|
exec node "$RUNNER" "$@" ;;
|
||||||
dashboard) exec bash "$BASH_DIR/dashboard-serve.sh" "$@" ;;
|
dashboard)
|
||||||
|
[[ -f "$BASH_DIR/dashboard-serve.sh" ]] || {
|
||||||
|
echo "casan: dashboard requires the Platform bundle" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
exec bash "$BASH_DIR/dashboard-serve.sh" "$@" ;;
|
||||||
version|-v|--version) version ;;
|
version|-v|--version) version ;;
|
||||||
help|-h|--help) usage ;;
|
help|-h|--help) usage ;;
|
||||||
*) echo "casan: unknown command '$cmd'" >&2; usage >&2; exit 64 ;;
|
*) echo "casan: unknown command '$cmd'" >&2; usage >&2; exit 64 ;;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ services:
|
|||||||
- ./apps/okr:/app/apps/okr
|
- ./apps/okr:/app/apps/okr
|
||||||
- ./apps/service-desk:/app/apps/service-desk
|
- ./apps/service-desk:/app/apps/service-desk
|
||||||
- ./apps/projects:/app/apps/projects
|
- ./apps/projects:/app/apps/projects
|
||||||
- ./packages/casan-harness/level5/project-registry.json:/app/packages/casan-harness/level5/project-registry.json
|
- ./packages/casan-harness/config/project-registry.json:/app/packages/casan-harness/config/project-registry.json
|
||||||
expose:
|
expose:
|
||||||
- "3010"
|
- "3010"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ Installer sẽ:
|
|||||||
- Tạo launcher `casan` (tự định vị install của chính nó) và đưa lên PATH
|
- Tạo launcher `casan` (tự định vị install của chính nó) và đưa lên PATH
|
||||||
(`~/.local/bin` hoặc `$CASAN_HOME/bin`).
|
(`~/.local/bin` hoặc `$CASAN_HOME/bin`).
|
||||||
|
|
||||||
|
Installer dùng allowlist `packaging/runtime-layout.json`. Runtime global không
|
||||||
|
chứa `tests/`, legacy `level5/`, internal test/CI runners, Platform dashboard/local
|
||||||
|
lab, source docs hay bản sao installer. Policy cần thiết đã được chuẩn hóa vào
|
||||||
|
`packages/casan-harness/config/`.
|
||||||
|
|
||||||
> Windows cần **Git for Windows (Git Bash)** để *chạy* harness (xem
|
> Windows cần **Git for Windows (Git Bash)** để *chạy* harness (xem
|
||||||
> [CASAN_AGENTIC_CLIENTS_WINDOWS.md](CASAN_AGENTIC_CLIENTS_WINDOWS.md)) — không
|
> [CASAN_AGENTIC_CLIENTS_WINDOWS.md](CASAN_AGENTIC_CLIENTS_WINDOWS.md)) — không
|
||||||
> cần WSL2. Cả hai OS cần `python3`.
|
> cần WSL2. Cả hai OS cần `python3`.
|
||||||
@@ -94,6 +99,16 @@ Tham số `--client` có thể lặp hoặc comma-separated:
|
|||||||
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 không có TTY,
|
||||||
mặc định tương thích ngược là `claude,codex`.
|
mặc định tương thích ngược là `claude,codex`.
|
||||||
|
|
||||||
|
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
|
||||||
|
command để lấy payload đầy đủ cho automation, ví dụ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
casan init --client claude,codex --json
|
||||||
|
casan doctor --json
|
||||||
|
casan level show --json
|
||||||
|
```
|
||||||
|
|
||||||
`--mode observe|enforce` mặc định `enforce`; dùng `observe` chỉ cho pilot
|
`--mode observe|enforce` mặc định `enforce`; dùng `observe` chỉ cho pilot
|
||||||
telemetry-only. `--integration-mode` nhận
|
telemetry-only. `--integration-mode` nhận
|
||||||
`project_hook|managed_hook|casan_owned`; `--target <dir>` mặc định là thư mục
|
`project_hook|managed_hook|casan_owned`; `--target <dir>` mặc định là thư mục
|
||||||
@@ -154,6 +169,18 @@ casan doctor --client codex
|
|||||||
casan doctor --client vscode-copilot
|
casan doctor --client vscode-copilot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Gỡ khỏi project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`.
|
||||||
|
|
||||||
## 3. Pin + Verify (giữ đảm bảo bảo mật khi harness ở ngoài repo)
|
## 3. Pin + Verify (giữ đảm bảo bảo mật khi harness ở ngoài repo)
|
||||||
|
|
||||||
Vì harness không nằm trong repo, dự án **pin** version + hash gate-code lúc
|
Vì harness không nằm trong repo, dự án **pin** version + hash gate-code lúc
|
||||||
@@ -163,7 +190,8 @@ Vì harness không nằm trong repo, dự án **pin** version + hash gate-code l
|
|||||||
casan verify-harness
|
casan verify-harness
|
||||||
```
|
```
|
||||||
|
|
||||||
- Khớp → `status: ok` (rc 0).
|
- Khớp → hiển thị `Harness integrity verified` (rc 0); `--json` trả
|
||||||
|
`"status": "ok"`.
|
||||||
- Harness global bị đổi/tamper so với pin → `HARNESS_INTEGRITY_DRIFT` (rc 3).
|
- Harness global bị đổi/tamper so với pin → `HARNESS_INTEGRITY_DRIFT` (rc 3).
|
||||||
|
|
||||||
`verify-harness` **luôn tính lại hash từ file thật** (không tin hash cache), nên
|
`verify-harness` **luôn tính lại hash từ file thật** (không tin hash cache), nên
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ apps/<app-slug>/
|
|||||||
└── test/ # test riêng
|
└── test/ # test riêng
|
||||||
```
|
```
|
||||||
|
|
||||||
Sau đó thêm project vào `packages/casan-harness/level5/project-registry.json` với `domain_root` trỏ tới `apps/<app-slug>/domain`, rồi chạy:
|
Sau đó thêm project vào `packages/casan-harness/config/project-registry.json` với `domain_root` trỏ tới `apps/<app-slug>/domain`, rồi chạy:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
packages/casan-harness/scripts/bash/verify-harness-reuse.sh
|
packages/casan-harness/scripts/bash/verify-harness-reuse.sh
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ This copies the core harness + `bin/casan` into `../my-project`, scaffolds
|
|||||||
`apps/ticketing/domain/` from the domain-pack template, installs the prompt entrypoints and
|
`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
|
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.
|
`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
|
## Option A2 — New production project shell
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ contents + maturity: [`packaging/levels.json`](../../packaging/levels.json).
|
|||||||
|
|
||||||
| Lvl | Package names | Status | What it is |
|
| Lvl | Package names | Status | What it is |
|
||||||
|---|---|:--:|---|
|
|---|---|:--:|---|
|
||||||
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | H1–H7 harness, security + action gates, evidence pack, audit, cost/telemetry, hardening tests, policy/config defaults, `bin/casan` CLI |
|
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | Minimal H1–H7 production runtime: security + action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, `bin/casan` CLI |
|
||||||
| **2 — DevKit / Adoption Kit** | `casan-devkit`, `casan-project-kit` | ✅ implemented | Level 1 + project templates, domain-pack scaffold, Gitea workflow template, harness Dockerfile, install script, adoption/CI/domain-pack guides |
|
| **2 — DevKit / Adoption Kit** | `casan-devkit`, `casan-project-kit` | ✅ implemented | Level 1 + project templates, domain-pack scaffold, Gitea workflow template, harness Dockerfile, install script, adoption/CI/domain-pack guides |
|
||||||
| **3 — Platform Components** | `casan-platform`, `casan-control-panel` | 🟡 preview | Control Panel, Dashboard, Run History, governed chat MVP and **Evidence Pack Viewer**. Attack Battery Viewer, Gitea evidence publishing, and managed rollout are still pending. |
|
| **3 — Platform Components** | `casan-platform`, `casan-control-panel` | 🟡 preview | Control Panel, Dashboard, Run History, governed chat MVP and **Evidence Pack Viewer**. Attack Battery Viewer, Gitea evidence publishing, and managed rollout are still pending. |
|
||||||
| **4 — Enterprise / Governed Console** | `casan-enterprise`, `casan-governed-console` | 📋 future | Promotion layer requiring managed deployment, KMS/Object Lock operations, HA/DR/SLA, external review and compliance/support evidence. |
|
| **4 — Enterprise / Governed Console** | `casan-enterprise`, `casan-governed-console` | 📋 future | Promotion layer requiring managed deployment, KMS/Object Lock operations, HA/DR/SLA, external review and compliance/support evidence. |
|
||||||
@@ -38,6 +38,26 @@ Docker images (see `DOCKER_GUIDE.md`): `casan-harness:X.Y.Z` ✅ · `casan-platf
|
|||||||
Every bundle carries `BUNDLE-MANIFEST.txt` + `SHA256SUMS`; preview bundles also carry
|
Every bundle carries `BUNDLE-MANIFEST.txt` + `SHA256SUMS`; preview bundles also carry
|
||||||
`PREVIEW-INCOMPLETE.txt`.
|
`PREVIEW-INCOMPLETE.txt`.
|
||||||
|
|
||||||
|
## Production runtime boundary
|
||||||
|
|
||||||
|
`packaging/runtime-layout.json` is the canonical allowlist used by macOS/Linux,
|
||||||
|
Windows, vendored adoption, project scaffolding, and release packaging.
|
||||||
|
|
||||||
|
Production Core/DevKit artifacts never include:
|
||||||
|
|
||||||
|
- `packages/casan-harness/tests/` or `packages/casan-devkit/tests/`;
|
||||||
|
- internal CI/adversarial runners (`ci-harness-gate.sh`, `security-gate.sh`,
|
||||||
|
`test-integrity.py`, cloud smoke);
|
||||||
|
- the legacy `packages/casan-harness/level5/` directory;
|
||||||
|
- Platform-only dashboard/local-infrastructure helpers;
|
||||||
|
- source documentation, installer copies, Dockerfiles, caches, or release tooling
|
||||||
|
inside the installed version directory.
|
||||||
|
|
||||||
|
Policies formerly under `level5/` live in `packages/casan-harness/config/`.
|
||||||
|
Test fixtures live under the source-only `tests/fixtures/`. The source hub retains
|
||||||
|
its tests so CASAN itself remains verifiable; only the install/release boundary is
|
||||||
|
minimal.
|
||||||
|
|
||||||
## Who adopts what
|
## Who adopts what
|
||||||
- **Governance-harness-only / BJT initial / CI gate** → `casan-core`.
|
- **Governance-harness-only / BJT initial / CI gate** → `casan-core`.
|
||||||
- **New project adopting CASAN** → `casan-devkit` (install.sh scaffolds domain + CI).
|
- **New project adopting CASAN** → `casan-devkit` (install.sh scaffolds domain + CI).
|
||||||
@@ -54,6 +74,7 @@ packages/casan-enterprise/ # L4 structure-only (future; README)
|
|||||||
bin/casan # CLI
|
bin/casan # CLI
|
||||||
scripts/package-release.sh # release packager
|
scripts/package-release.sh # release packager
|
||||||
packaging/levels.json # bundle contents + maturity (source of truth)
|
packaging/levels.json # bundle contents + maturity (source of truth)
|
||||||
|
packaging/runtime-layout.json # exact production runtime allowlist
|
||||||
docs/packaging/ # this plan + adoption/CI/domain-pack/gitea/docker guides
|
docs/packaging/ # this plan + adoption/CI/domain-pack/gitea/docker guides
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+16
-22
@@ -11,37 +11,31 @@ cp packages/casan-devkit/templates/gitea-workflow/ci.yml .gitea/workflows/casan-
|
|||||||
|
|
||||||
## 2. What it runs
|
## 2. What it runs
|
||||||
```yaml
|
```yaml
|
||||||
- bash packages/casan-harness/scripts/bash/ci-harness-gate.sh # all governance suites
|
- casan verify-harness
|
||||||
- bash packages/casan-harness/scripts/bash/verify-audit-chain.sh
|
- casan gate
|
||||||
- bash packages/casan-harness/scripts/bash/sign-policy-bundle.sh verify
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Environment knobs
|
`CASAN_PROJECT_MANIFEST` must point to the project's manifest. Build and test
|
||||||
| Var | Default | Use |
|
commands come from that reviewed manifest; the production package does not contain
|
||||||
|---|---|---|
|
CASAN's own source-hub test suites.
|
||||||
| `CASAN_CI_RUN_FRONTEND` | 0 | 1 if you have a frontend workspace |
|
|
||||||
| `CASAN_CI_RUN_BACKEND` | 0 | 1 to run backend tests |
|
|
||||||
| `CASAN_CI_RUN_INFRA_LAB` | 0 | 1 to run the Docker infra lab |
|
|
||||||
| `CASAN_CI_STEP_TIMEOUT_SEC` | 600 | raise to 1200 — some suites are model-backed and run ~450s; 600 flakes under load |
|
|
||||||
| `CASAN_CI_SUITE_FILTER` | — | regex to run a subset of suites |
|
|
||||||
| `CASAN_DOMAIN_ROOT` | apps/okr/domain | point at your project's domain |
|
|
||||||
|
|
||||||
## 4. Runner
|
## 3. Runner
|
||||||
Uses `runs-on: ci-runner` (a self-hosted Gitea Actions runner). To set one up on your host,
|
Uses `runs-on: ci-runner` (a self-hosted Gitea Actions runner). To set one up on your host,
|
||||||
see `.gitea/vps-setup-runbook.md` and `scripts/setup-ci-runner.sh`. The runner needs
|
see `.gitea/vps-setup-runbook.md` and `scripts/setup-ci-runner.sh`. The runner needs
|
||||||
`bash`, `python3`, `openssl` (and `node`/`npm` only if you enable apps/okr/frontend/backend tests).
|
`bash`, `python3`, `openssl`, the approved global CASAN release, and the toolchain
|
||||||
|
declared by the project manifest.
|
||||||
|
|
||||||
## 5. Expected result
|
## 4. Expected result
|
||||||
`CI_GATE_SUMMARY PASS=<n> FAIL=0 SKIP=<k>`. Any FAIL fails the job (exit 1). The gate is
|
`PROJECT_GATE_SUMMARY ... FAIL=0`. Any FAIL fails the job (exit 1). The gate is
|
||||||
**fail-closed**: missing signatures/policy in enforced mode (`CASAN_PROFILE=prod` or
|
**fail-closed**: missing signatures/policy in enforced mode (`CASAN_PROFILE=prod` or
|
||||||
`CASAN_VERIFY_STRICT=1`) also fail.
|
`CASAN_VERIFY_STRICT=1`) also fail.
|
||||||
|
|
||||||
## 6. This repo's own CI
|
## 5. This repo's own CI
|
||||||
The source hub's active pipeline is `.gitea/workflows/ci.yml` (frontend tests → security
|
The source hub's active pipeline still runs internal regression and adversarial suites.
|
||||||
gate → deploy). It already runs the gate from the promoted root layout
|
Those suites validate CASAN itself and are intentionally excluded from customer/runtime
|
||||||
(`packages/casan-harness/...`). Use it as a worked example.
|
artifacts.
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
- Keep the gate green as an invariant; every new control must ship a fail-able adversarial test.
|
- Keep the project gate green as an invariant.
|
||||||
- Do not stage runtime log artifacts (`.specify/logs`, evidence) produced by gate runs.
|
- Do not stage runtime log artifacts (`.specify/logs`, evidence) produced by gate runs.
|
||||||
- For a quick local pre-push check: `bin/casan gate` (or a filtered subset).
|
- For a quick local pre-push check: `casan gate`.
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ apps/<project>/domain/
|
|||||||
export CASAN_PROJECT_MANIFEST=apps/<project>/domain/project.manifest.json
|
export CASAN_PROJECT_MANIFEST=apps/<project>/domain/project.manifest.json
|
||||||
bin/casan gate # runs domain-dependent suites against your pack
|
bin/casan gate # runs domain-dependent suites against your pack
|
||||||
```
|
```
|
||||||
`domain_root` is also recorded per-project in `packages/casan-harness/level5/project-registry.json`
|
`domain_root` is also recorded per-project in `packages/casan-harness/config/project-registry.json`
|
||||||
so `bin/casan reuse` can prove multi-project reuse.
|
so `bin/casan reuse` can prove multi-project reuse.
|
||||||
|
|
||||||
## Reference example
|
## Reference example
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ curl -fsSL -XPOST -H "Authorization: token $GITEA_TOKEN" \
|
|||||||
## 5. Recommended cadence
|
## 5. Recommended cadence
|
||||||
- Tag `vX.Y.Z` on `main` → CI green → build bundles → publish `core` + `devkit` (always),
|
- Tag `vX.Y.Z` on `main` → CI green → build bundles → publish `core` + `devkit` (always),
|
||||||
`platform` as `-preview`, `all-in-one-demo` for demos. Never publish `enterprise`.
|
`platform` as `-preview`, `all-in-one-demo` for demos. Never publish `enterprise`.
|
||||||
- Keep `VERSION` and `packages/casan-harness/level5/harness-package.json` version in lockstep.
|
- Keep `VERSION` and `packages/casan-harness/config/harness-package.json` version in lockstep.
|
||||||
|
|
||||||
## Automated release (recommended — already wired)
|
## Automated release (recommended — already wired)
|
||||||
`.gitea/workflows/release.yml` does all of the above automatically on a version tag. You
|
`.gitea/workflows/release.yml` does all of the above automatically on a version tag. You
|
||||||
@@ -80,7 +80,7 @@ never hand a token to anyone — it lives in a CI secret.
|
|||||||
```bash
|
```bash
|
||||||
# bump the version everywhere first
|
# bump the version everywhere first
|
||||||
echo 1.0.1 > VERSION # must match the tag
|
echo 1.0.1 > VERSION # must match the tag
|
||||||
# (also bump packages/casan-harness/level5/harness-package.json "version")
|
# (also bump packages/casan-harness/config/harness-package.json "version")
|
||||||
git commit -am "release v1.0.1"
|
git commit -am "release v1.0.1"
|
||||||
git tag -a v1.0.1 -m "CASAN v1.0.1"
|
git tag -a v1.0.1 -m "CASAN v1.0.1"
|
||||||
git push origin main --follow-tags
|
git push origin main --follow-tags
|
||||||
|
|||||||
+8
-10
@@ -55,22 +55,20 @@ Log "source : $Source"
|
|||||||
Log "version : $version"
|
Log "version : $version"
|
||||||
Log "install : $CasanHome"
|
Log "install : $CasanHome"
|
||||||
|
|
||||||
# ── 2) Copy harness + devkit + CLI into a versioned dir ──────────────────────
|
# ── 2) Copy the production runtime allowlist into a versioned dir ─────────────
|
||||||
$dest = Join-Path $CasanHome "versions\$version"
|
$dest = Join-Path $CasanHome "versions\$version"
|
||||||
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
|
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $dest 'packages'), (Join-Path $dest 'bin') | Out-Null
|
New-Item -ItemType Directory -Force -Path (Join-Path $dest 'packages'), (Join-Path $dest 'bin') | Out-Null
|
||||||
|
|
||||||
function Copy-Tree($rel) {
|
$runtimeCopier = Join-Path $Source 'scripts\copy-runtime.py'
|
||||||
$src = Join-Path $Source $rel
|
if (-not (Test-Path $runtimeCopier)) { Die 'missing production runtime copier: scripts/copy-runtime.py' }
|
||||||
if (-not (Test-Path $src)) { return }
|
& $py.Source $runtimeCopier --source-root $Source --destination-root $dest --component harness --component devkit --clean
|
||||||
$dst = Join-Path $dest $rel
|
if ($LASTEXITCODE -ne 0) { Die 'failed to assemble the production runtime.' }
|
||||||
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
|
||||||
Copy-Item -Recurse -Force (Join-Path $src '*') $dst
|
|
||||||
}
|
|
||||||
Copy-Tree 'packages\casan-harness'
|
|
||||||
Copy-Tree 'packages\casan-devkit'
|
|
||||||
Copy-Item -Force (Join-Path $Source 'bin\casan') (Join-Path $dest 'bin\casan')
|
Copy-Item -Force (Join-Path $Source 'bin\casan') (Join-Path $dest 'bin\casan')
|
||||||
Set-Content -Path (Join-Path $dest 'VERSION') -Value $version -Encoding ASCII
|
Set-Content -Path (Join-Path $dest 'VERSION') -Value $version -Encoding ASCII
|
||||||
|
Set-Content -Path (Join-Path $dest '.casan-level') -Value 'devkit' -Encoding ASCII
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $dest 'packaging') | Out-Null
|
||||||
|
Copy-Item -Force (Join-Path $Source 'packaging\runtime-layout.json') (Join-Path $dest 'packaging\runtime-layout.json')
|
||||||
|
|
||||||
Get-ChildItem -Path $dest -Recurse -Directory -Filter '__pycache__' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
Get-ChildItem -Path $dest -Recurse -Directory -Filter '__pycache__' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Get-ChildItem -Path $dest -Recurse -File -Filter '*.pyc' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
|
Get-ChildItem -Path $dest -Recurse -File -Filter '*.pyc' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
|
||||||
|
|||||||
+14
-19
@@ -75,34 +75,29 @@ log "version : $VERSION"
|
|||||||
log "level : $LEVEL (core=harness+CLI, devkit=+adoption tooling)"
|
log "level : $LEVEL (core=harness+CLI, devkit=+adoption tooling)"
|
||||||
log "install : $CASAN_HOME"
|
log "install : $CASAN_HOME"
|
||||||
|
|
||||||
# ── 2) Copy harness + devkit + CLI into a versioned dir ──────────────────────
|
# ── 2) Copy the production runtime allowlist into a versioned dir ─────────────
|
||||||
DEST="$CASAN_HOME/versions/$VERSION"
|
DEST="$CASAN_HOME/versions/$VERSION"
|
||||||
rm -rf "$DEST"
|
rm -rf "$DEST"
|
||||||
mkdir -p "$DEST/packages" "$DEST/bin"
|
mkdir -p "$DEST/packages" "$DEST/bin"
|
||||||
|
|
||||||
copy_tree() { # <relpath>
|
RUNTIME_COPIER="$SRC/scripts/copy-runtime.py"
|
||||||
[ -e "$SRC/$1" ] || return 0
|
[ -f "$RUNTIME_COPIER" ] || die "missing production runtime copier: scripts/copy-runtime.py"
|
||||||
if command -v rsync >/dev/null 2>&1; then
|
python3 "$RUNTIME_COPIER" \
|
||||||
rsync -a --exclude='__pycache__' --exclude='*.pyc' --exclude='.DS_Store' \
|
--source-root "$SRC" --destination-root "$DEST" --component harness --clean \
|
||||||
--exclude='*.log' "$SRC/$1/" "$DEST/$1/"
|
|| die "failed to assemble the production harness runtime."
|
||||||
else
|
|
||||||
mkdir -p "$DEST/$1"
|
# L1 core: production harness + CLI.
|
||||||
cp -R "$SRC/$1/." "$DEST/$1/"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
# L1 core: harness + CLI + installer + level manifest.
|
|
||||||
copy_tree "packages/casan-harness"
|
|
||||||
cp "$SRC/bin/casan" "$DEST/bin/casan"
|
cp "$SRC/bin/casan" "$DEST/bin/casan"
|
||||||
chmod +x "$DEST/bin/casan"
|
chmod +x "$DEST/bin/casan"
|
||||||
cp "$SRC/VERSION" "$DEST/VERSION" 2>/dev/null || printf '%s\n' "$VERSION" > "$DEST/VERSION"
|
cp "$SRC/VERSION" "$DEST/VERSION" 2>/dev/null || printf '%s\n' "$VERSION" > "$DEST/VERSION"
|
||||||
for f in install.sh install.ps1; do [ -f "$SRC/$f" ] && cp "$SRC/$f" "$DEST/$f"; done
|
mkdir -p "$DEST/packaging"
|
||||||
mkdir -p "$DEST/packaging"; [ -f "$SRC/packaging/levels.json" ] && cp "$SRC/packaging/levels.json" "$DEST/packaging/levels.json"
|
cp "$SRC/packaging/runtime-layout.json" "$DEST/packaging/runtime-layout.json"
|
||||||
|
|
||||||
# L2 devkit: adoption tooling (templates, domain-pack, casan init, package-release).
|
# L2 devkit: production adoption tooling only.
|
||||||
if [ "$LEVEL" = "devkit" ]; then
|
if [ "$LEVEL" = "devkit" ]; then
|
||||||
copy_tree "packages/casan-devkit"
|
python3 "$RUNTIME_COPIER" \
|
||||||
mkdir -p "$DEST/scripts"; [ -f "$SRC/scripts/package-release.sh" ] && cp "$SRC/scripts/package-release.sh" "$DEST/scripts/package-release.sh"
|
--source-root "$SRC" --destination-root "$DEST" --component devkit --clean \
|
||||||
[ -d "$SRC/docs/packaging" ] && { mkdir -p "$DEST/docs"; cp -R "$SRC/docs/packaging" "$DEST/docs/packaging"; }
|
|| die "failed to assemble the production DevKit runtime."
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "$LEVEL" > "$DEST/.casan-level"
|
printf '%s\n' "$LEVEL" > "$DEST/.casan-level"
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const CONNECTIONS_CLI = join(HARNESS_BIN, 'model-connections.py');
|
|||||||
const ORCHESTRATOR_CLI = join(HARNESS_BIN, 'goal-orchestrator.py');
|
const ORCHESTRATOR_CLI = join(HARNESS_BIN, 'goal-orchestrator.py');
|
||||||
const PATCH_EXECUTOR_CLI = join(HARNESS_BIN, 'goal-patch-executor.py');
|
const PATCH_EXECUTOR_CLI = join(HARNESS_BIN, 'goal-patch-executor.py');
|
||||||
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
|
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
|
||||||
const PROJECT_REGISTRY = join(APP_ROOT, 'packages', 'casan-harness', 'level5', 'project-registry.json');
|
const PROJECT_REGISTRY = join(APP_ROOT, 'packages', 'casan-harness', 'config', 'project-registry.json');
|
||||||
const PROJECT_SCAFFOLDER = join(APP_ROOT, 'packages', 'casan-devkit', 'project-scaffold.py');
|
const PROJECT_SCAFFOLDER = join(APP_ROOT, 'packages', 'casan-devkit', 'project-scaffold.py');
|
||||||
|
|
||||||
function parseJson<T>(value: string): T | null {
|
function parseJson<T>(value: string): T | null {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { GoalsService } from '../src/goals/goals.service.js';
|
|||||||
|
|
||||||
const admin = { actor: 'goal-admin', role: 'org-admin', project: 'default', tenant: 'goal-test' };
|
const admin = { actor: 'goal-admin', role: 'org-admin', project: 'default', tenant: 'goal-test' };
|
||||||
const root = resolve(import.meta.dirname, '../../../..');
|
const root = resolve(import.meta.dirname, '../../../..');
|
||||||
const registry = resolve(root, 'packages/casan-harness/level5/project-registry.json');
|
const registry = resolve(root, 'packages/casan-harness/config/project-registry.json');
|
||||||
|
|
||||||
test('goal project selector exposes only active allowlisted registry entries', () => {
|
test('goal project selector exposes only active allowlisted registry entries', () => {
|
||||||
const result = new GoalsService().projects(admin);
|
const result = new GoalsService().projects(admin);
|
||||||
|
|||||||
@@ -56,6 +56,141 @@ VSCODE_EXTENSION_IDS = {
|
|||||||
}
|
}
|
||||||
PROMPT_MARKER_START = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
|
PROMPT_MARKER_START = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
|
||||||
PROMPT_MARKER_END = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
|
PROMPT_MARKER_END = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
|
||||||
|
CLIENT_LABELS = {
|
||||||
|
"claude": "Claude Code",
|
||||||
|
"codex": "Codex",
|
||||||
|
"vscode-copilot": "VS Code / @casan",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _color(code, text):
|
||||||
|
if (not sys.stdout.isatty() or os.environ.get("NO_COLOR") is not None or
|
||||||
|
os.environ.get("TERM") == "dumb"):
|
||||||
|
return text
|
||||||
|
return "\033[%sm%s\033[0m" % (code, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _mark(ok):
|
||||||
|
return _color("32" if ok else "31", "✓" if ok else "✗")
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_mark():
|
||||||
|
return _color("33", "!")
|
||||||
|
|
||||||
|
|
||||||
|
def _heading(text):
|
||||||
|
print(_color("1;36", text))
|
||||||
|
|
||||||
|
|
||||||
|
def _details(rows):
|
||||||
|
width = max((len(label) for label, _value in rows), default=0)
|
||||||
|
for label, value in rows:
|
||||||
|
print(" %-*s %s" % (width, label, value))
|
||||||
|
|
||||||
|
|
||||||
|
def _client_names(clients):
|
||||||
|
return ", ".join(CLIENT_LABELS.get(client, client) for client in clients) or "None"
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_json_or_human(args, payload, renderer):
|
||||||
|
if getattr(args, "json", False):
|
||||||
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
renderer(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_init(result):
|
||||||
|
_heading("%s CASAN initialized" % _mark(True))
|
||||||
|
_details([
|
||||||
|
("Project", result["project_id"]),
|
||||||
|
("Location", result["target"]),
|
||||||
|
("Level", "%s (%s)" % (
|
||||||
|
result["target_level_name"].capitalize(), result["target_level"])),
|
||||||
|
("Mode", result["enforcement_mode"]),
|
||||||
|
("Clients", _client_names(result["clients"])),
|
||||||
|
("Files", "%d created or updated" % len(result["created"])),
|
||||||
|
("Harness", result["harness_version"]),
|
||||||
|
])
|
||||||
|
print()
|
||||||
|
print(_color("1", "Integrations"))
|
||||||
|
for path, status in result["hook_merges"].items():
|
||||||
|
ok = status not in (None, "absent")
|
||||||
|
print(" %s %-26s %s" % (_mark(ok), path, status or "unavailable"))
|
||||||
|
extension = result["vscode_extension"]
|
||||||
|
if extension.get("status") not in ("skipped",):
|
||||||
|
ok = extension.get("status") == "installed"
|
||||||
|
print(" %s %-26s %s" % (
|
||||||
|
_mark(ok), "VS Code extension", extension.get("status")))
|
||||||
|
print()
|
||||||
|
print(_color("1", "Next steps"))
|
||||||
|
print(" 1. Run `casan doctor`")
|
||||||
|
if "codex" in result["clients"]:
|
||||||
|
print(" 2. In Codex, open `/hooks` and trust this project's hook.")
|
||||||
|
|
||||||
|
|
||||||
|
def _render_verify(result):
|
||||||
|
ok = result["status"] == "ok"
|
||||||
|
_heading("%s Harness integrity %s" % (
|
||||||
|
_mark(ok), "verified" if ok else "check failed"))
|
||||||
|
_details([
|
||||||
|
("Version", str(result.get("harness_version_now") or "unknown")),
|
||||||
|
("Location", str(result.get("harness_root") or "unavailable")),
|
||||||
|
("Expected", str(result.get("expected") or "unavailable")),
|
||||||
|
("Actual", str(result.get("actual") or "unavailable")),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def _render_level(result):
|
||||||
|
_heading("CASAN packaging level")
|
||||||
|
_details([
|
||||||
|
("Installed", str(result.get("installed_level") or "unknown")),
|
||||||
|
("Project", ("%s (%s)" % (
|
||||||
|
result.get("project_target_level_name"),
|
||||||
|
result.get("project_target_level")))
|
||||||
|
if result.get("project_target_level") else "not initialized"),
|
||||||
|
("Status", str(result.get("project_level_status") or "unknown")),
|
||||||
|
])
|
||||||
|
print()
|
||||||
|
for level, description in result["levels"].items():
|
||||||
|
print(" %-14s %s" % (level, description))
|
||||||
|
|
||||||
|
|
||||||
|
def _render_doctor(result):
|
||||||
|
ready = result["status"] == "ready"
|
||||||
|
_heading("CASAN Doctor")
|
||||||
|
print(" %s Harness integrity" % _mark(result["integrity"]["ok"]))
|
||||||
|
print(" %s Project bootstrap" % _mark(result["bootstrap"]["ok"]))
|
||||||
|
for client, item in result["client_checks"].items():
|
||||||
|
label = CLIENT_LABELS.get(client, client)
|
||||||
|
print(" %s %s" % (_mark(item.get("ready", False)), label))
|
||||||
|
reason = item.get("smoke", {}).get("reason")
|
||||||
|
if reason and not item.get("ready"):
|
||||||
|
print(" %s" % reason)
|
||||||
|
if result["warnings"]:
|
||||||
|
print()
|
||||||
|
print(_color("1;33", "Warnings"))
|
||||||
|
for warning in result["warnings"]:
|
||||||
|
print(" %s %s" % (_warn_mark(), warning))
|
||||||
|
print()
|
||||||
|
status = _color("1;32", "READY") if ready else _color("1;31", "NOT READY")
|
||||||
|
print("Status: %s" % status)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_uninstall(result):
|
||||||
|
_heading("%s CASAN removed from project" % _mark(True))
|
||||||
|
_details([
|
||||||
|
("Project", result.get("project_id") or "unknown"),
|
||||||
|
("Location", result["target"]),
|
||||||
|
("Hooks", "%d integration files cleaned" % len(result["hook_changes"])),
|
||||||
|
("Files", "%d CASAN-owned files removed" % len(result["removed"])),
|
||||||
|
("Evidence", "removed" if result["purged"] else "retained"),
|
||||||
|
("VS Code extension", result["vscode_extension"]["status"]),
|
||||||
|
])
|
||||||
|
if result["retained"]:
|
||||||
|
print()
|
||||||
|
print(_color("1", "Retained for safety"))
|
||||||
|
for path in result["retained"]:
|
||||||
|
print(" - %s" % path)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -225,6 +360,8 @@ def _load_json_or(path, default):
|
|||||||
|
|
||||||
|
|
||||||
def _backup_once(path, backups):
|
def _backup_once(path, backups):
|
||||||
|
if backups is None:
|
||||||
|
return
|
||||||
bak = path + ".casan-bak"
|
bak = path + ".casan-bak"
|
||||||
if os.path.exists(path) and not os.path.exists(bak):
|
if os.path.exists(path) and not os.path.exists(bak):
|
||||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||||
@@ -670,10 +807,22 @@ def cmd_init(args):
|
|||||||
hhash, hsource = compute_harness_hash(harness)
|
hhash, hsource = compute_harness_hash(harness)
|
||||||
created = []
|
created = []
|
||||||
backups = []
|
backups = []
|
||||||
|
previous_manifest = _load_json_or(
|
||||||
|
os.path.join(target, ".casan", "init-manifest.json"), {})
|
||||||
|
owned_files = {
|
||||||
|
str(path).replace("\\", "/")
|
||||||
|
for path in previous_manifest.get("owned_files", [])
|
||||||
|
if isinstance(path, str)
|
||||||
|
}
|
||||||
|
|
||||||
def created_add(p):
|
def created_add(p):
|
||||||
created.append(os.path.relpath(p, target))
|
created.append(os.path.relpath(p, target))
|
||||||
|
|
||||||
|
def mark_owned_if_absent(p):
|
||||||
|
if not os.path.exists(p):
|
||||||
|
owned_files.add(
|
||||||
|
os.path.relpath(p, target).replace(os.sep, "/"))
|
||||||
|
|
||||||
# ── .casan/config.json ──
|
# ── .casan/config.json ──
|
||||||
cfg_dir = os.path.join(target, ".casan")
|
cfg_dir = os.path.join(target, ".casan")
|
||||||
cfg = {
|
cfg = {
|
||||||
@@ -696,6 +845,7 @@ def cmd_init(args):
|
|||||||
"target_level_name": lvl_name,
|
"target_level_name": lvl_name,
|
||||||
}
|
}
|
||||||
p = os.path.join(cfg_dir, "config.json")
|
p = os.path.join(cfg_dir, "config.json")
|
||||||
|
mark_owned_if_absent(p)
|
||||||
_write(p, json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", backups); created_add(p)
|
_write(p, json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", backups); created_add(p)
|
||||||
|
|
||||||
# ── .casan/version.lock (pin) ──
|
# ── .casan/version.lock (pin) ──
|
||||||
@@ -708,6 +858,7 @@ def cmd_init(args):
|
|||||||
"recorded_at": now_iso(),
|
"recorded_at": now_iso(),
|
||||||
}
|
}
|
||||||
p = os.path.join(cfg_dir, "version.lock")
|
p = os.path.join(cfg_dir, "version.lock")
|
||||||
|
mark_owned_if_absent(p)
|
||||||
_write(p, json.dumps(lock, ensure_ascii=False, indent=2) + "\n", backups); created_add(p)
|
_write(p, json.dumps(lock, ensure_ascii=False, indent=2) + "\n", backups); created_add(p)
|
||||||
|
|
||||||
# ── .casan/agentic.env (Plan-20 flags) ──
|
# ── .casan/agentic.env (Plan-20 flags) ──
|
||||||
@@ -720,6 +871,7 @@ def cmd_init(args):
|
|||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
p = os.path.join(cfg_dir, "agentic.env")
|
p = os.path.join(cfg_dir, "agentic.env")
|
||||||
|
mark_owned_if_absent(p)
|
||||||
_write(p, "\n".join(env_lines), backups); created_add(p)
|
_write(p, "\n".join(env_lines), backups); created_add(p)
|
||||||
|
|
||||||
# Stable project-local bootstrap. It loads the config above, resolves the
|
# Stable project-local bootstrap. It loads the config above, resolves the
|
||||||
@@ -730,6 +882,7 @@ def cmd_init(args):
|
|||||||
if not os.path.isfile(bootstrap_source):
|
if not os.path.isfile(bootstrap_source):
|
||||||
sys.stderr.write("casan init: missing project hook bootstrap template\n")
|
sys.stderr.write("casan init: missing project hook bootstrap template\n")
|
||||||
return 1
|
return 1
|
||||||
|
mark_owned_if_absent(bootstrap_target)
|
||||||
with open(bootstrap_source, "r", encoding="utf-8") as fh:
|
with open(bootstrap_source, "r", encoding="utf-8") as fh:
|
||||||
_write(bootstrap_target, fh.read(), backups)
|
_write(bootstrap_target, fh.read(), backups)
|
||||||
try:
|
try:
|
||||||
@@ -746,6 +899,7 @@ def cmd_init(args):
|
|||||||
os.makedirs(os.path.join(specify, "logs"), exist_ok=True)
|
os.makedirs(os.path.join(specify, "logs"), exist_ok=True)
|
||||||
gi = os.path.join(specify, ".gitignore")
|
gi = os.path.join(specify, ".gitignore")
|
||||||
if not os.path.exists(gi):
|
if not os.path.exists(gi):
|
||||||
|
mark_owned_if_absent(gi)
|
||||||
_write(gi, "# CASAN runtime state — do not commit\nlogs/\nstate/\n", backups); created_add(gi)
|
_write(gi, "# CASAN runtime state — do not commit\nlogs/\nstate/\n", backups); created_add(gi)
|
||||||
|
|
||||||
# ── Plan-20 client hooks — MERGED into any existing config, never clobbered.
|
# ── Plan-20 client hooks — MERGED into any existing config, never clobbered.
|
||||||
@@ -756,6 +910,7 @@ def cmd_init(args):
|
|||||||
claude_dst = os.path.join(target, ".claude", "settings.json")
|
claude_dst = os.path.join(target, ".claude", "settings.json")
|
||||||
if "claude" in clients:
|
if "claude" in clients:
|
||||||
dst = claude_dst
|
dst = claude_dst
|
||||||
|
mark_owned_if_absent(dst)
|
||||||
remove_json_hooks(dst, "claude_hook.py", backups)
|
remove_json_hooks(dst, "claude_hook.py", backups)
|
||||||
r = merge_json_hooks(dst, os.path.join(ad, "claude-code", "settings.template.json"),
|
r = merge_json_hooks(dst, os.path.join(ad, "claude-code", "settings.template.json"),
|
||||||
"casan-hook.py", backups)
|
"casan-hook.py", backups)
|
||||||
@@ -770,6 +925,7 @@ def cmd_init(args):
|
|||||||
codex_hooks_dst = os.path.join(target, ".codex", "hooks.json")
|
codex_hooks_dst = os.path.join(target, ".codex", "hooks.json")
|
||||||
if "codex" in clients:
|
if "codex" in clients:
|
||||||
dsth = codex_hooks_dst
|
dsth = codex_hooks_dst
|
||||||
|
mark_owned_if_absent(dsth)
|
||||||
remove_json_hooks(dsth, "codex_hook.py", backups)
|
remove_json_hooks(dsth, "codex_hook.py", backups)
|
||||||
r = merge_json_hooks(dsth, os.path.join(ad, "codex", "hooks.template.json"),
|
r = merge_json_hooks(dsth, os.path.join(ad, "codex", "hooks.template.json"),
|
||||||
"casan-hook.py", backups)
|
"casan-hook.py", backups)
|
||||||
@@ -787,6 +943,7 @@ def cmd_init(args):
|
|||||||
os.path.join(target, ".codex", "config.toml"), backups)
|
os.path.join(target, ".codex", "config.toml"), backups)
|
||||||
|
|
||||||
vscode_file = os.path.join(target, ".vscode", "extensions.json")
|
vscode_file = os.path.join(target, ".vscode", "extensions.json")
|
||||||
|
mark_owned_if_absent(vscode_file)
|
||||||
vscode_merge = merge_vscode_recommendations(vscode_file, clients, backups)
|
vscode_merge = merge_vscode_recommendations(vscode_file, clients, backups)
|
||||||
if vscode_merge not in ("unchanged", "absent"):
|
if vscode_merge not in ("unchanged", "absent"):
|
||||||
created_add(vscode_file)
|
created_add(vscode_file)
|
||||||
@@ -812,24 +969,31 @@ def cmd_init(args):
|
|||||||
ci_src = os.path.join(dk, "templates", "gitea-workflow", "ci.yml")
|
ci_src = os.path.join(dk, "templates", "gitea-workflow", "ci.yml")
|
||||||
ci_dst = os.path.join(target, ".gitea", "workflows", "casan-ci.yml")
|
ci_dst = os.path.join(target, ".gitea", "workflows", "casan-ci.yml")
|
||||||
if _copy_if_absent(ci_src, ci_dst, None, target, created):
|
if _copy_if_absent(ci_src, ci_dst, None, target, created):
|
||||||
|
owned_files.add(os.path.relpath(
|
||||||
|
ci_dst, target).replace(os.sep, "/"))
|
||||||
level_extras.append(".gitea/workflows/casan-ci.yml")
|
level_extras.append(".gitea/workflows/casan-ci.yml")
|
||||||
dom_src = os.path.join(dk, "templates", "domain-pack")
|
dom_src = os.path.join(dk, "templates", "domain-pack")
|
||||||
dom_dst = os.path.join(target, "apps", project, "domain")
|
dom_dst = os.path.join(target, "apps", project, "domain")
|
||||||
|
created_before_domain = len(created)
|
||||||
n = _copy_tree_missing(dom_src, dom_dst, target, created)
|
n = _copy_tree_missing(dom_src, dom_dst, target, created)
|
||||||
if n:
|
if n:
|
||||||
|
owned_files.update(
|
||||||
|
path.replace(os.sep, "/")
|
||||||
|
for path in created[created_before_domain:])
|
||||||
level_extras.append("apps/%s/domain (%d files)" % (project, n))
|
level_extras.append("apps/%s/domain (%d files)" % (project, n))
|
||||||
|
|
||||||
# ── manifest (so uninstall/verify know what init created) ──
|
# ── manifest (so uninstall/verify know what init created) ──
|
||||||
manifest = {
|
manifest = {
|
||||||
"created": created,
|
"created": created,
|
||||||
"backups": [os.path.relpath(b, target) for b in backups],
|
"backups": [os.path.relpath(b, target) for b in backups],
|
||||||
|
"owned_files": sorted(owned_files),
|
||||||
"project_id": project,
|
"project_id": project,
|
||||||
"at": now_iso(),
|
"at": now_iso(),
|
||||||
}
|
}
|
||||||
p = os.path.join(cfg_dir, "init-manifest.json")
|
p = os.path.join(cfg_dir, "init-manifest.json")
|
||||||
_write(p, json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", backups)
|
_write(p, json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", backups)
|
||||||
|
|
||||||
print(json.dumps({
|
result = {
|
||||||
"status": "initialized",
|
"status": "initialized",
|
||||||
"project_id": project,
|
"project_id": project,
|
||||||
"target": target,
|
"target": target,
|
||||||
@@ -846,7 +1010,8 @@ def cmd_init(args):
|
|||||||
"level_extras": level_extras,
|
"level_extras": level_extras,
|
||||||
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
|
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
|
||||||
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
|
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
|
||||||
}, ensure_ascii=False, indent=2))
|
}
|
||||||
|
_emit_json_or_human(args, result, _render_init)
|
||||||
if legacy_migration.get("manual_review"):
|
if legacy_migration.get("manual_review"):
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"casan init: WARNING — legacy CASAN prose outside managed markers "
|
"casan init: WARNING — legacy CASAN prose outside managed markers "
|
||||||
@@ -885,7 +1050,7 @@ def cmd_verify(args):
|
|||||||
"harness_version_now": harness_version(harness),
|
"harness_version_now": harness_version(harness),
|
||||||
"harness_root": harness,
|
"harness_root": harness,
|
||||||
}
|
}
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
_emit_json_or_human(args, result, _render_verify)
|
||||||
if not ok:
|
if not ok:
|
||||||
sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved harness does not match the "
|
sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved harness does not match the "
|
||||||
"project pin. The global harness changed or was tampered.\n")
|
"project pin. The global harness changed or was tampered.\n")
|
||||||
@@ -919,7 +1084,7 @@ def cmd_level(args):
|
|||||||
"4 enterprise": "future — not shipped",
|
"4 enterprise": "future — not shipped",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
_emit_json_or_human(args, out, _render_level)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -1091,10 +1256,180 @@ def cmd_doctor(args):
|
|||||||
"with Plan-20 hooks and needs manual review: %s." %
|
"with Plan-20 hooks and needs manual review: %s." %
|
||||||
", ".join(legacy_conflicts))
|
", ".join(legacy_conflicts))
|
||||||
checks["status"] = "ready" if ready else "not_ready"
|
checks["status"] = "ready" if ready else "not_ready"
|
||||||
print(json.dumps(checks, ensure_ascii=False, indent=2))
|
_emit_json_or_human(args, checks, _render_doctor)
|
||||||
return 0 if ready else 2
|
return 0 if ready else 2
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_integration_json(path, metadata_key, metadata_prefix, remove_if_empty):
|
||||||
|
"""Remove CASAN template metadata and delete a CASAN-created empty file."""
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return False
|
||||||
|
doc = _load_json_or(path, None)
|
||||||
|
if not isinstance(doc, dict):
|
||||||
|
return False
|
||||||
|
metadata = doc.get(metadata_key)
|
||||||
|
if isinstance(metadata, str) and metadata.startswith(metadata_prefix):
|
||||||
|
doc.pop(metadata_key, None)
|
||||||
|
hooks = doc.get("hooks")
|
||||||
|
if isinstance(hooks, dict) and not hooks:
|
||||||
|
doc.pop("hooks", None)
|
||||||
|
recommendations = doc.get("recommendations")
|
||||||
|
if isinstance(recommendations, list) and not recommendations:
|
||||||
|
doc.pop("recommendations", None)
|
||||||
|
with owner_writable(path):
|
||||||
|
if not doc and remove_if_empty:
|
||||||
|
os.unlink(path)
|
||||||
|
else:
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_owned_file(path, removed, target):
|
||||||
|
if not os.path.isfile(path) and not os.path.islink(path):
|
||||||
|
return
|
||||||
|
with owner_writable(path):
|
||||||
|
os.unlink(path)
|
||||||
|
removed.append(os.path.relpath(path, target).replace(os.sep, "/"))
|
||||||
|
|
||||||
|
|
||||||
|
def _prune_empty_directory(path):
|
||||||
|
try:
|
||||||
|
os.rmdir(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _uninstall_vscode_extension(enabled):
|
||||||
|
if not enabled:
|
||||||
|
return {
|
||||||
|
"status": "retained_shared",
|
||||||
|
"reason": "use --remove-vscode-extension to remove it from this machine",
|
||||||
|
}
|
||||||
|
code = shutil.which("code")
|
||||||
|
if not code:
|
||||||
|
return {"status": "not_removed", "reason": "code_cli_not_found"}
|
||||||
|
result = subprocess.run(
|
||||||
|
[code, "--uninstall-extension", "fpt-casan.casan-governed-chat"],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
|
||||||
|
return {
|
||||||
|
"status": "uninstalled" if result.returncode == 0 else "failed",
|
||||||
|
"command": code,
|
||||||
|
"detail": (result.stdout or result.stderr).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_uninstall(args):
|
||||||
|
"""Remove one project's CASAN adoption without clobbering user config."""
|
||||||
|
target = os.path.abspath(args.target or os.getcwd())
|
||||||
|
config_path = os.path.join(target, ".casan", "config.json")
|
||||||
|
manifest_path = os.path.join(target, ".casan", "init-manifest.json")
|
||||||
|
config = _load_json_or(config_path, {})
|
||||||
|
manifest = _load_json_or(manifest_path, {})
|
||||||
|
specify_gitignore = os.path.join(target, ".specify", ".gitignore")
|
||||||
|
try:
|
||||||
|
with open(specify_gitignore, "r", encoding="utf-8") as fh:
|
||||||
|
has_casan_evidence = fh.read().startswith(
|
||||||
|
"# CASAN runtime state — do not commit")
|
||||||
|
except OSError:
|
||||||
|
has_casan_evidence = False
|
||||||
|
if (not config and not manifest and
|
||||||
|
not (args.purge and has_casan_evidence)):
|
||||||
|
sys.stderr.write(
|
||||||
|
"casan uninstall: project is not initialized; nothing to remove.\n")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
created = {
|
||||||
|
str(path).replace("\\", "/")
|
||||||
|
for path in manifest.get("created", [])
|
||||||
|
if isinstance(path, str)
|
||||||
|
}
|
||||||
|
owned_files = {
|
||||||
|
str(path).replace("\\", "/")
|
||||||
|
for path in manifest.get("owned_files", [])
|
||||||
|
if isinstance(path, str)
|
||||||
|
}
|
||||||
|
hook_changes = {}
|
||||||
|
integration_files = (
|
||||||
|
(".claude/settings.json", "claude_hook.py", "casan-hook.py"),
|
||||||
|
(".codex/hooks.json", "codex_hook.py", "casan-hook.py"),
|
||||||
|
)
|
||||||
|
for relative, legacy_marker, current_marker in integration_files:
|
||||||
|
path = os.path.join(target, *relative.split("/"))
|
||||||
|
legacy = remove_json_hooks(path, legacy_marker, None)
|
||||||
|
current = remove_json_hooks(path, current_marker, None)
|
||||||
|
status = "removed" if "removed" in (legacy, current) else current
|
||||||
|
hook_changes[relative] = status
|
||||||
|
|
||||||
|
codex_config = os.path.join(target, ".codex", "config.toml")
|
||||||
|
hook_changes[".codex/config.toml"] = clean_legacy_codex_config(
|
||||||
|
codex_config, None)
|
||||||
|
|
||||||
|
vscode_path = os.path.join(target, ".vscode", "extensions.json")
|
||||||
|
hook_changes[".vscode/extensions.json"] = merge_vscode_recommendations(
|
||||||
|
vscode_path, [], None)
|
||||||
|
|
||||||
|
_clean_integration_json(
|
||||||
|
os.path.join(target, ".claude", "settings.json"),
|
||||||
|
"//", "CASAN Plan-20", ".claude/settings.json" in owned_files)
|
||||||
|
_clean_integration_json(
|
||||||
|
os.path.join(target, ".codex", "hooks.json"),
|
||||||
|
"description", "CASAN Plan-20", ".codex/hooks.json" in owned_files)
|
||||||
|
_clean_integration_json(
|
||||||
|
vscode_path, "_casan_unused", "",
|
||||||
|
".vscode/extensions.json" in owned_files)
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
for relative in (
|
||||||
|
".casan/config.json",
|
||||||
|
".casan/version.lock",
|
||||||
|
".casan/agentic.env",
|
||||||
|
".casan/casan-hook.py",
|
||||||
|
".casan/init-manifest.json"):
|
||||||
|
_remove_owned_file(
|
||||||
|
os.path.join(target, *relative.split("/")), removed, target)
|
||||||
|
|
||||||
|
if args.purge:
|
||||||
|
for relative in (".specify/logs", ".specify/state"):
|
||||||
|
path = os.path.join(target, *relative.split("/"))
|
||||||
|
if os.path.isdir(path):
|
||||||
|
with owner_writable(path):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
removed.append(relative)
|
||||||
|
if has_casan_evidence:
|
||||||
|
_remove_owned_file(specify_gitignore, removed, target)
|
||||||
|
|
||||||
|
for relative in (
|
||||||
|
".casan", ".claude", ".codex", ".vscode", ".specify"):
|
||||||
|
_prune_empty_directory(os.path.join(target, relative))
|
||||||
|
|
||||||
|
retained = []
|
||||||
|
if not args.purge and os.path.isdir(os.path.join(target, ".specify")):
|
||||||
|
retained.append(".specify/ runtime evidence (use --purge to remove)")
|
||||||
|
for item in manifest.get("created", []):
|
||||||
|
normalized = str(item).replace("\\", "/")
|
||||||
|
if (normalized.startswith(".gitea/") or
|
||||||
|
normalized.startswith("apps/")):
|
||||||
|
retained.append(normalized + " (may contain project changes)")
|
||||||
|
backups = manifest.get("backups", [])
|
||||||
|
if backups:
|
||||||
|
retained.append("%d .casan-bak backup(s)" % len(backups))
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "uninstalled",
|
||||||
|
"project_id": config.get("project_id") or manifest.get("project_id"),
|
||||||
|
"target": target,
|
||||||
|
"hook_changes": hook_changes,
|
||||||
|
"removed": removed,
|
||||||
|
"purged": bool(args.purge),
|
||||||
|
"vscode_extension": _uninstall_vscode_extension(
|
||||||
|
args.remove_vscode_extension),
|
||||||
|
"retained": retained,
|
||||||
|
}
|
||||||
|
_emit_json_or_human(args, result, _render_uninstall)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
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="CASAN hybrid adoption")
|
||||||
sub = parser.add_subparsers(dest="cmd")
|
sub = parser.add_subparsers(dest="cmd")
|
||||||
@@ -1122,21 +1457,42 @@ def main(argv=None):
|
|||||||
pi.add_argument("--force", action="store_true",
|
pi.add_argument("--force", action="store_true",
|
||||||
help="bypass the source-hub safety guard (adopt CASAN into a CASAN checkout)")
|
help="bypass the source-hub safety guard (adopt CASAN into a CASAN checkout)")
|
||||||
pi.add_argument("--harness", help="override harness root")
|
pi.add_argument("--harness", help="override harness root")
|
||||||
|
pi.add_argument("--json", action="store_true",
|
||||||
|
help="emit the complete machine-readable result")
|
||||||
|
|
||||||
pv = sub.add_parser("verify", help="verify the resolved harness matches the project pin")
|
pv = sub.add_parser("verify", help="verify the resolved harness matches the project pin")
|
||||||
pv.add_argument("--target", help="project root (default: cwd)")
|
pv.add_argument("--target", help="project root (default: cwd)")
|
||||||
pv.add_argument("--harness", help="override harness root")
|
pv.add_argument("--harness", help="override harness root")
|
||||||
|
pv.add_argument("--json", action="store_true",
|
||||||
|
help="emit the complete machine-readable result")
|
||||||
|
|
||||||
pl = sub.add_parser("level", help="show installed + project packaging level")
|
pl = sub.add_parser("level", help="show installed + project packaging level")
|
||||||
pl.add_argument("--show", action="store_true", help="(default) show levels")
|
pl.add_argument("--show", action="store_true", help="(default) show levels")
|
||||||
pl.add_argument("--target", help="project root (default: cwd)")
|
pl.add_argument("--target", help="project root (default: cwd)")
|
||||||
pl.add_argument("--harness", help="override harness root")
|
pl.add_argument("--harness", help="override harness root")
|
||||||
|
pl.add_argument("--json", action="store_true",
|
||||||
|
help="emit the complete machine-readable result")
|
||||||
|
|
||||||
pd = sub.add_parser("doctor", help="verify selected client integrations end-to-end")
|
pd = sub.add_parser("doctor", help="verify selected client integrations end-to-end")
|
||||||
pd.add_argument("--target", help="project root (default: cwd)")
|
pd.add_argument("--target", help="project root (default: cwd)")
|
||||||
pd.add_argument("--client", action="append",
|
pd.add_argument("--client", action="append",
|
||||||
help="client(s) to check; default: clients enabled in config")
|
help="client(s) to check; default: clients enabled in config")
|
||||||
pd.add_argument("--harness", help="override harness root")
|
pd.add_argument("--harness", help="override harness root")
|
||||||
|
pd.add_argument("--json", action="store_true",
|
||||||
|
help="emit the complete machine-readable result")
|
||||||
|
|
||||||
|
pu = sub.add_parser(
|
||||||
|
"uninstall",
|
||||||
|
help="remove CASAN from a project while preserving user configuration")
|
||||||
|
pu.add_argument("--target", help="project root (default: cwd)")
|
||||||
|
pu.add_argument(
|
||||||
|
"--purge", action="store_true",
|
||||||
|
help="also remove .specify runtime logs and state")
|
||||||
|
pu.add_argument(
|
||||||
|
"--remove-vscode-extension", action="store_true",
|
||||||
|
help="also uninstall the shared @casan VS Code extension from this machine")
|
||||||
|
pu.add_argument("--json", action="store_true",
|
||||||
|
help="emit the complete machine-readable result")
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
if args.cmd == "init":
|
if args.cmd == "init":
|
||||||
@@ -1153,6 +1509,8 @@ def main(argv=None):
|
|||||||
return cmd_level(args)
|
return cmd_level(args)
|
||||||
if args.cmd == "doctor":
|
if args.cmd == "doctor":
|
||||||
return cmd_doctor(args)
|
return cmd_doctor(args)
|
||||||
|
if args.cmd == "uninstall":
|
||||||
|
return cmd_uninstall(args)
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 64
|
return 64
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,17 @@ fi
|
|||||||
echo "==> installing CASAN core into $TARGET (project=$PROJECT domain=$DOMAIN)"
|
echo "==> installing CASAN core into $TARGET (project=$PROJECT domain=$DOMAIN)"
|
||||||
mkdir -p "$TARGET/packages" "$TARGET/bin" "$TARGET/apps/$PROJECT/domain"
|
mkdir -p "$TARGET/packages" "$TARGET/bin" "$TARGET/apps/$PROJECT/domain"
|
||||||
|
|
||||||
# 1) core harness + CLI
|
# 1) production core harness + CLI. Migrate the old Level-5 registry before
|
||||||
RSYNC_EXCLUDES=(--exclude='__pycache__' --exclude='*.pyc' --exclude='.DS_Store' --exclude='*.log')
|
# cleaning source-only/test content from an existing vendored installation.
|
||||||
[[ -f "$TARGET/packages/casan-harness/level5/project-registry.json" ]] && RSYNC_EXCLUDES+=(--exclude='level5/project-registry.json')
|
OLD_REG="$TARGET/packages/casan-harness/level5/project-registry.json"
|
||||||
rsync -a "${RSYNC_EXCLUDES[@]}" "$SRC/packages/casan-harness/" "$TARGET/packages/casan-harness/"
|
NEW_REG="$TARGET/packages/casan-harness/config/project-registry.json"
|
||||||
|
if [[ -f "$OLD_REG" && ! -f "$NEW_REG" ]]; then
|
||||||
|
mkdir -p "$(dirname "$NEW_REG")"
|
||||||
|
cp "$OLD_REG" "$NEW_REG"
|
||||||
|
fi
|
||||||
|
python3 "$SRC/scripts/copy-runtime.py" \
|
||||||
|
--source-root "$SRC" --destination-root "$TARGET" --component harness --clean \
|
||||||
|
--preserve packages/casan-harness/config/project-registry.json
|
||||||
cp "$SRC/bin/casan" "$TARGET/bin/casan"; chmod +x "$TARGET/bin/casan"
|
cp "$SRC/bin/casan" "$TARGET/bin/casan"; chmod +x "$TARGET/bin/casan"
|
||||||
[[ -f "$SRC/VERSION" ]] && cp "$SRC/VERSION" "$TARGET/VERSION"
|
[[ -f "$SRC/VERSION" ]] && cp "$SRC/VERSION" "$TARGET/VERSION"
|
||||||
|
|
||||||
@@ -107,7 +114,7 @@ path.write_text(path.read_text(encoding="utf-8").replace("__PROJECT_ID__", proje
|
|||||||
PY
|
PY
|
||||||
|
|
||||||
# 5) register in project-registry.json (append if absent)
|
# 5) register in project-registry.json (append if absent)
|
||||||
REG="$TARGET/packages/casan-harness/level5/project-registry.json"
|
REG="$NEW_REG"
|
||||||
python3 - "$REG" "$PROJECT" "$DOMAIN" <<'PY'
|
python3 - "$REG" "$PROJECT" "$DOMAIN" <<'PY'
|
||||||
import json, os, sys
|
import json, os, sys
|
||||||
reg, pid, dom = sys.argv[1], sys.argv[2], sys.argv[3]
|
reg, pid, dom = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
|||||||
@@ -25,6 +25,35 @@ class ScaffoldError(RuntimeError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_exclusions(component: str) -> set[str]:
|
||||||
|
path = SOURCE_ROOT / "packaging" / "runtime-layout.json"
|
||||||
|
try:
|
||||||
|
layout = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
values = layout["components"][component]["exclude"]
|
||||||
|
except (OSError, ValueError, KeyError, TypeError) as error:
|
||||||
|
raise ScaffoldError(f"production runtime layout is unavailable: {error}") from error
|
||||||
|
return {
|
||||||
|
str(value).replace("\\", "/").strip("/")
|
||||||
|
for value in values
|
||||||
|
if isinstance(value, str) and value.strip("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def excluded(relative: Path, exclusions: set[str]) -> bool:
|
||||||
|
value = relative.as_posix()
|
||||||
|
return (
|
||||||
|
any(part in {
|
||||||
|
"__pycache__", "node_modules", "dist", "build", "coverage",
|
||||||
|
} for part in relative.parts)
|
||||||
|
or relative.name == ".DS_Store"
|
||||||
|
or relative.suffix in {".pyc", ".pyo", ".log", ".tmp"}
|
||||||
|
or any(
|
||||||
|
value == item or value.startswith(item.rstrip("/") + "/")
|
||||||
|
for item in exclusions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def atomic_write(path: Path, content: bytes, mode: int = 0o644) -> str:
|
def atomic_write(path: Path, content: bytes, mode: int = 0o644) -> str:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
@@ -89,14 +118,17 @@ def copy_domain_pack(target: Path, slug: str, name: str) -> tuple[int, int]:
|
|||||||
def install_harness(target: Path) -> tuple[int, int]:
|
def install_harness(target: Path) -> tuple[int, int]:
|
||||||
created = unchanged = 0
|
created = unchanged = 0
|
||||||
harness_source = SOURCE_ROOT / "packages" / "casan-harness"
|
harness_source = SOURCE_ROOT / "packages" / "casan-harness"
|
||||||
|
exclusions = runtime_exclusions("harness")
|
||||||
for source in sorted(harness_source.rglob("*")):
|
for source in sorted(harness_source.rglob("*")):
|
||||||
if not source.is_file() or "__pycache__" in source.parts or source.suffix == ".pyc":
|
if not source.is_file():
|
||||||
continue
|
continue
|
||||||
relative = source.relative_to(harness_source)
|
relative = source.relative_to(harness_source)
|
||||||
|
if excluded(relative, exclusions):
|
||||||
|
continue
|
||||||
destination = target / "packages" / "casan-harness" / relative
|
destination = target / "packages" / "casan-harness" / relative
|
||||||
# The target registry is adoption state, not immutable harness code. Preserve it
|
# The target registry is adoption state, not immutable harness code. Preserve it
|
||||||
# after the first install so repeated scaffolds and upgrades remain idempotent.
|
# after the first install so repeated scaffolds and upgrades remain idempotent.
|
||||||
if relative.as_posix() == "level5/project-registry.json" and destination.exists():
|
if relative.as_posix() == "config/project-registry.json" and destination.exists():
|
||||||
unchanged += 1
|
unchanged += 1
|
||||||
continue
|
continue
|
||||||
mode = stat.S_IMODE(source.stat().st_mode)
|
mode = stat.S_IMODE(source.stat().st_mode)
|
||||||
@@ -114,7 +146,7 @@ def install_harness(target: Path) -> tuple[int, int]:
|
|||||||
|
|
||||||
|
|
||||||
def register_project(target: Path, slug: str, name: str) -> None:
|
def register_project(target: Path, slug: str, name: str) -> None:
|
||||||
registry_path = target / "packages" / "casan-harness" / "level5" / "project-registry.json"
|
registry_path = target / "packages" / "casan-harness" / "config" / "project-registry.json"
|
||||||
data = json.loads(registry_path.read_text(encoding="utf-8"))
|
data = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||||
harness_version = next((item.get("harness_version") for item in data.get("projects", []) if item.get("harness_version")), "1.0.0")
|
harness_version = next((item.get("harness_version") for item in data.get("projects", []) if item.get("harness_version")), "1.0.0")
|
||||||
# A shipped harness may carry source-hub examples. Never register dangling
|
# A shipped harness may carry source-hub examples. Never register dangling
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# CASAN harness gate — Gitea Actions workflow (adoption template).
|
# CASAN production gate — Gitea Actions workflow (adoption template).
|
||||||
# Copy to .gitea/workflows/casan-ci.yml in your project. Assumes the CASAN core harness
|
# The runner installs the approved CASAN release globally and the project provides
|
||||||
# lives at packages/casan-harness/ (via casan-devkit install.sh) and domain data at
|
# CASAN_PROJECT_MANIFEST. Internal CASAN product tests are intentionally not shipped.
|
||||||
# apps/<project>/domain/. Runs the full governance gate on every push/PR.
|
|
||||||
name: CASAN Gate
|
name: CASAN Gate
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -15,11 +14,7 @@ jobs:
|
|||||||
runs-on: ci-runner
|
runs-on: ci-runner
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
env:
|
env:
|
||||||
CASAN_CI_RUN_FRONTEND: "0" # set 1 if your project has a frontend workspace
|
CASAN_PROJECT_MANIFEST: apps/<project>/domain/project.manifest.json
|
||||||
CASAN_CI_RUN_BACKEND: "0" # set 1 if your project has backend tests
|
|
||||||
CASAN_CI_RUN_INFRA_LAB: "0"
|
|
||||||
CASAN_CI_STEP_TIMEOUT_SEC: "1200" # headroom; some suites are model-backed
|
|
||||||
# CASAN_DOMAIN_ROOT: apps/<project>/domain # uncomment + set for your project
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -30,10 +25,8 @@ jobs:
|
|||||||
command -v python3 >/dev/null || { apt-get update && apt-get install -y python3; }
|
command -v python3 >/dev/null || { apt-get update && apt-get install -y python3; }
|
||||||
python3 --version
|
python3 --version
|
||||||
|
|
||||||
- name: Run CASAN harness gate
|
- name: Verify installed CASAN runtime
|
||||||
run: bash packages/casan-harness/scripts/bash/ci-harness-gate.sh
|
run: casan verify-harness
|
||||||
|
|
||||||
- name: Verify audit chain + policy bundle
|
- name: Run project governance gate
|
||||||
run: |
|
run: casan gate
|
||||||
bash packages/casan-harness/scripts/bash/verify-audit-chain.sh
|
|
||||||
bash packages/casan-harness/scripts/bash/sign-policy-bundle.sh verify
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ assert 'CASAN_PROJECT_ID: "sample-project"' in workflow
|
|||||||
assert "__PROJECT_ID__" not in workflow
|
assert "__PROJECT_ID__" not in workflow
|
||||||
assert (root / "bin/casan-chat").exists()
|
assert (root / "bin/casan-chat").exists()
|
||||||
assert (root / "bin/casan-chat.ps1").exists()
|
assert (root / "bin/casan-chat.ps1").exists()
|
||||||
|
assert not (root / "packages/casan-harness/tests").exists()
|
||||||
|
assert not (root / "packages/casan-harness/level5").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 "sample-project" in text
|
||||||
|
|||||||
@@ -27,6 +27,57 @@ echo "===== ① global install ====="
|
|||||||
if sh "$REPO_ROOT/install.sh" >/dev/null 2>&1; then pass "install.sh completes"; else fail "install.sh failed"; fi
|
if sh "$REPO_ROOT/install.sh" >/dev/null 2>&1; then pass "install.sh completes"; else fail "install.sh failed"; fi
|
||||||
[[ -x "$CASAN" ]] && pass "global launcher created" || fail "launcher missing"
|
[[ -x "$CASAN" ]] && pass "global launcher created" || fail "launcher missing"
|
||||||
[[ -f "$CASAN_HOME/current/.harness-hash" ]] && pass "integrity hash recorded at install" || fail "no .harness-hash"
|
[[ -f "$CASAN_HOME/current/.harness-hash" ]] && pass "integrity hash recorded at install" || fail "no .harness-hash"
|
||||||
|
python3 - "$CASAN_HOME/current" <<'PY' \
|
||||||
|
&& pass "global install has the exact production directory layout" \
|
||||||
|
|| fail "global install contains missing or source-only paths"
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
root = Path(sys.argv[1])
|
||||||
|
assert {path.name for path in root.iterdir()} == {
|
||||||
|
".casan-level", ".harness-hash", "VERSION", "bin", "packages", "packaging",
|
||||||
|
}
|
||||||
|
assert {path.name for path in (root / "packages").iterdir()} == {
|
||||||
|
"casan-harness", "casan-devkit",
|
||||||
|
}
|
||||||
|
assert {path.name for path in (root / "packages/casan-harness").iterdir()} == {
|
||||||
|
"adapters", "agentops", "config", "governance", "memory", "prompts",
|
||||||
|
"schemas", "scripts", "security", "templates",
|
||||||
|
}
|
||||||
|
assert {path.name for path in (root / "packages/casan-devkit").iterdir()} == {
|
||||||
|
"casan-init.py", "package-vscode-extension.py", "project-scaffold.py",
|
||||||
|
"quality-profiles", "schemas", "templates",
|
||||||
|
}
|
||||||
|
assert {path.name for path in (root / "packaging").iterdir()} == {
|
||||||
|
"runtime-layout.json",
|
||||||
|
}
|
||||||
|
for forbidden in (
|
||||||
|
"packages/casan-harness/tests",
|
||||||
|
"packages/casan-harness/level5",
|
||||||
|
"packages/casan-harness/scripts/bash/ci-harness-gate.sh",
|
||||||
|
"packages/casan-harness/scripts/bash/security-gate.sh",
|
||||||
|
"packages/casan-harness/scripts/bash/test-integrity.py",
|
||||||
|
"packages/casan-harness/scripts/bash/dashboard-server.py",
|
||||||
|
"packages/casan-devkit/tests",
|
||||||
|
"packages/casan-devkit/install.sh",
|
||||||
|
"install.sh",
|
||||||
|
"install.ps1",
|
||||||
|
"docs",
|
||||||
|
):
|
||||||
|
assert not (root / forbidden).exists(), forbidden
|
||||||
|
for required in (
|
||||||
|
"packages/casan-harness/config/project-registry.json",
|
||||||
|
"packages/casan-harness/config/tool-registry.yaml",
|
||||||
|
"packages/casan-harness/scripts/bash/project-gate.sh",
|
||||||
|
"packages/casan-harness/scripts/python/agentic_bridge.py",
|
||||||
|
):
|
||||||
|
assert (root / required).is_file(), required
|
||||||
|
registry = json.loads(
|
||||||
|
(root / "packages/casan-harness/config/project-registry.json").read_text(
|
||||||
|
encoding="utf-8"))
|
||||||
|
assert registry["projects"] == []
|
||||||
|
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 "client selector accepts menu numbers, aliases, repeats, and all" \
|
||||||
@@ -39,6 +90,33 @@ 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"]
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
RELEASE_DIST="$WORK/release-dist"
|
||||||
|
CASAN_DIST_DIR="$RELEASE_DIST" bash "$REPO_ROOT/scripts/package-release.sh" core >/dev/null
|
||||||
|
python3 - "$RELEASE_DIST/casan-core-v$(cat "$REPO_ROOT/VERSION").tar.gz" <<'PY' \
|
||||||
|
&& pass "core release artifact contains production runtime only" \
|
||||||
|
|| fail "core release artifact leaked source-only content"
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
with tarfile.open(sys.argv[1], "r:gz") as archive:
|
||||||
|
names = [name.split("/", 1)[1] for name in archive.getnames() if "/" in name]
|
||||||
|
for forbidden in (
|
||||||
|
"packages/casan-harness/tests",
|
||||||
|
"packages/casan-harness/level5",
|
||||||
|
"packages/casan-harness/scripts/bash/ci-harness-gate.sh",
|
||||||
|
"packages/casan-harness/scripts/bash/test-integrity.py",
|
||||||
|
):
|
||||||
|
assert not any(name == forbidden or name.startswith(forbidden + "/") for name in names), forbidden
|
||||||
|
for required in (
|
||||||
|
"packages/casan-harness/config/tool-registry.yaml",
|
||||||
|
"packages/casan-harness/scripts/bash/casan-harness.sh",
|
||||||
|
"packaging/runtime-layout.json",
|
||||||
|
"scripts/copy-runtime.py",
|
||||||
|
"install.sh",
|
||||||
|
):
|
||||||
|
assert required in names, required
|
||||||
|
PY
|
||||||
|
|
||||||
echo "===== ② casan init (config only, no harness copy) ====="
|
echo "===== ② casan init (config only, no harness copy) ====="
|
||||||
PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package.json"
|
PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package.json"
|
||||||
( cd "$PROJ" && "$CASAN" init --project my-app --mode enforce >/dev/null 2>&1 ) \
|
( cd "$PROJ" && "$CASAN" init --project my-app --mode enforce >/dev/null 2>&1 ) \
|
||||||
@@ -160,7 +238,7 @@ L2="$WORK/l2"; mkdir -p "$L2"; ( cd "$L2" && "$DKC" init --level 2 --project l2
|
|||||||
( [ -f "$L2/.gitea/workflows/casan-ci.yml" ] && [ -d "$L2/apps/l2/domain" ] ) && pass "init --level 2 adds CI + domain-pack" || fail "L2 missing devkit extras"
|
( [ -f "$L2/.gitea/workflows/casan-ci.yml" ] && [ -d "$L2/apps/l2/domain" ] ) && pass "init --level 2 adds CI + domain-pack" || fail "L2 missing devkit extras"
|
||||||
L4RC=0; L4="$WORK/l4"; mkdir -p "$L4"; ( cd "$L4" && "$DKC" init --level 4 --project l4 >/dev/null 2>&1 ) || L4RC=$?
|
L4RC=0; L4="$WORK/l4"; mkdir -p "$L4"; ( cd "$L4" && "$DKC" init --level 4 --project l4 >/dev/null 2>&1 ) || L4RC=$?
|
||||||
[ "$L4RC" -eq 3 ] && pass "init --level 4 (enterprise) refused (rc=3)" || fail "L4 init not refused (rc=$L4RC)"
|
[ "$L4RC" -eq 3 ] && pass "init --level 4 (enterprise) refused (rc=3)" || fail "L4 init not refused (rc=$L4RC)"
|
||||||
LVL=$( ( cd "$L2" && "$DKC" level show ) | python3 -c 'import json,sys;print(json.load(sys.stdin)["project_target_level"])' 2>/dev/null)
|
LVL=$( ( cd "$L2" && "$DKC" level show --json ) | python3 -c 'import json,sys;print(json.load(sys.stdin)["project_target_level"])' 2>/dev/null)
|
||||||
[ "$LVL" = "2" ] && pass "casan level show reports project target level" || fail "level show wrong ($LVL)"
|
[ "$LVL" = "2" ] && pass "casan level show reports project target level" || fail "level show wrong ($LVL)"
|
||||||
|
|
||||||
echo "===== ⑨ SAFETY: init refuses to adopt a CASAN source hub into itself ====="
|
echo "===== ⑨ SAFETY: init refuses to adopt a CASAN source hub into itself ====="
|
||||||
@@ -269,6 +347,48 @@ CASAN_HOME="$DK_HOME" node "$REPO_ROOT/packages/casan-devkit/tests/vscode-extens
|
|||||||
( cd "$VSP" && PATH="$FAKE_BIN:$PATH" "$DKC" doctor --client vscode-copilot >/dev/null 2>&1 ) \
|
( cd "$VSP" && PATH="$FAKE_BIN:$PATH" "$DKC" doctor --client vscode-copilot >/dev/null 2>&1 ) \
|
||||||
&& pass "doctor proves the VS Code/Copilot adapter and installed extension" || fail "VS Code doctor failed"
|
&& pass "doctor proves the VS Code/Copilot adapter and installed extension" || fail "VS Code doctor failed"
|
||||||
|
|
||||||
|
echo "===== ⑪ professional output + safe project uninstall ====="
|
||||||
|
UN="$WORK/uninstall-project"
|
||||||
|
mkdir -p "$UN/.claude"
|
||||||
|
cat > "$UN/.claude/settings.json" <<'EOF'
|
||||||
|
{"model":"claude-opus-4-8","hooks":{"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"my-user-hook.sh"}]}]}}
|
||||||
|
EOF
|
||||||
|
INIT_OUT=$(cd "$UN" && "$DKC" init --project uninstall-project --client claude --non-interactive)
|
||||||
|
echo "$INIT_OUT" | grep -q "CASAN initialized" \
|
||||||
|
&& pass "init defaults to concise human-readable output" \
|
||||||
|
|| fail "init human output missing ($INIT_OUT)"
|
||||||
|
if echo "$INIT_OUT" | head -1 | grep -q '^[[:space:]]*{'; then
|
||||||
|
fail "init still defaults to raw JSON"
|
||||||
|
else
|
||||||
|
pass "init no longer dumps raw JSON by default"
|
||||||
|
fi
|
||||||
|
( cd "$UN" && "$DKC" doctor --json ) | python3 -c \
|
||||||
|
'import json,sys; d=json.load(sys.stdin); assert d["status"] == "ready"' \
|
||||||
|
&& pass "doctor --json preserves the machine-readable contract" \
|
||||||
|
|| fail "doctor --json is not valid/ready"
|
||||||
|
UN_OUT=$(cd "$UN" && "$DKC" uninstall)
|
||||||
|
echo "$UN_OUT" | grep -q "CASAN removed from project" \
|
||||||
|
&& pass "uninstall emits a concise completion summary" \
|
||||||
|
|| fail "uninstall summary missing ($UN_OUT)"
|
||||||
|
[ ! -f "$UN/.casan/config.json" ] \
|
||||||
|
&& pass "uninstall removes CASAN-owned project config" \
|
||||||
|
|| fail "uninstall left CASAN config enabled"
|
||||||
|
py_check "$UN/.claude/settings.json" "my-user-hook" \
|
||||||
|
&& pass "uninstall preserves user-authored hooks" \
|
||||||
|
|| fail "uninstall removed a user hook"
|
||||||
|
if py_check "$UN/.claude/settings.json" "casan-hook.py"; then
|
||||||
|
fail "uninstall left the CASAN Claude hook enabled"
|
||||||
|
else
|
||||||
|
pass "uninstall removes only the CASAN hook"
|
||||||
|
fi
|
||||||
|
[ -d "$UN/.specify" ] \
|
||||||
|
&& pass "uninstall retains runtime evidence unless --purge is explicit" \
|
||||||
|
|| fail "uninstall removed evidence without --purge"
|
||||||
|
( cd "$UN" && "$DKC" uninstall --purge >/dev/null ) \
|
||||||
|
&& [ ! -d "$UN/.specify" ] \
|
||||||
|
&& pass "a follow-up uninstall --purge removes retained evidence" \
|
||||||
|
|| fail "uninstall --purge did not remove retained evidence"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "===== HYBRID INSTALL SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
echo "===== HYBRID INSTALL SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||||
[[ "$FAIL" -eq 0 ]] || exit 1
|
[[ "$FAIL" -eq 0 ]] || exit 1
|
||||||
|
|||||||
@@ -75,10 +75,13 @@ class ProjectScaffoldTests(unittest.TestCase):
|
|||||||
args = options(directory, with_harness=True)
|
args = options(directory, with_harness=True)
|
||||||
first = SCAFFOLD.scaffold(args)
|
first = SCAFFOLD.scaffold(args)
|
||||||
second = SCAFFOLD.scaffold(args)
|
second = SCAFFOLD.scaffold(args)
|
||||||
registry = json.loads((Path(directory) / "packages/casan-harness/level5/project-registry.json").read_text(encoding="utf-8"))
|
registry = json.loads((Path(directory) / "packages/casan-harness/config/project-registry.json").read_text(encoding="utf-8"))
|
||||||
self.assertEqual([item["project_id"] for item in registry["projects"]], ["inventory-app"])
|
self.assertEqual([item["project_id"] for item in registry["projects"]], ["inventory-app"])
|
||||||
self.assertGreater(first["created"], 100)
|
self.assertGreater(first["created"], 100)
|
||||||
self.assertEqual(second["created"], 0)
|
self.assertEqual(second["created"], 0)
|
||||||
|
self.assertFalse((Path(directory) / "packages/casan-harness/tests").exists())
|
||||||
|
self.assertFalse((Path(directory) / "packages/casan-harness/level5").exists())
|
||||||
|
self.assertFalse((Path(directory) / "packages/casan-harness/scripts/bash/ci-harness-gate.sh").exists())
|
||||||
|
|
||||||
def test_invalid_identifiers_and_broad_target_fail_closed(self):
|
def test_invalid_identifiers_and_broad_target_fail_closed(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Layout:
|
|||||||
- `scripts/` — bash + powershell gate logic (H1→H7), path resolver `casan-paths.sh`
|
- `scripts/` — bash + powershell gate logic (H1→H7), path resolver `casan-paths.sh`
|
||||||
- `security/` — filter/policy rules (prompt-filter, pii-rules, output-policy, ...)
|
- `security/` — filter/policy rules (prompt-filter, pii-rules, output-policy, ...)
|
||||||
- `governance/`, `agentops/` — H5/H6 code
|
- `governance/`, `agentops/` — H5/H6 code
|
||||||
- `level5/` — L5 config (drift/kpi/model-fallback/tool-registry YAMLs)
|
- `config/` — runtime policy, registry, model fallback, and tool metadata
|
||||||
- `templates/`, `config/` — spec/plan templates, loop-policy
|
- `templates/` — spec/plan templates
|
||||||
- `tests/` — reproducible harness test suites + integrity manifest
|
- `tests/` — source-hub verification only; excluded from production installs
|
||||||
|
|
||||||
Runtime state (logs, audit chain, tenant state) is NOT part of this package — it stays with
|
Runtime state (logs, audit chain, tenant state) is NOT part of this package — it stays with
|
||||||
the app under `CASAN_STATE_ROOT`. Domain data (golden-runs, corpus, input) lives in
|
the app under `CASAN_STATE_ROOT`. Domain data (golden-runs, corpus, input) lives in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
version: 1.0
|
version: 1.0
|
||||||
description: CASAN Level 5 drift detection policy
|
description: CASAN production drift detection policy
|
||||||
|
|
||||||
golden_dir: apps/okr/domain/golden-runs
|
golden_dir: apps/okr/domain/golden-runs
|
||||||
evidence_dir: docs/output/casan/level5-evidence
|
evidence_dir: docs/output/casan/level5-evidence
|
||||||
+7
-7
@@ -5,11 +5,11 @@
|
|||||||
"H4-security",
|
"H4-security",
|
||||||
"H5-governance",
|
"H5-governance",
|
||||||
"H6-agentops",
|
"H6-agentops",
|
||||||
"L5-drift-detection",
|
"drift-detection",
|
||||||
"L5-model-fallback",
|
"model-fallback",
|
||||||
"L5-tool-registry",
|
"tool-registry",
|
||||||
"L5-rollback",
|
"rollback",
|
||||||
"L5-business-feedback"
|
"business-feedback"
|
||||||
],
|
],
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"speckit": ">=1.0.0",
|
"speckit": ">=1.0.0",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"wrapper": "packages/casan-harness/scripts/bash/casan-harness.sh",
|
"wrapper": "packages/casan-harness/scripts/bash/casan-harness.sh",
|
||||||
"test": "packages/casan-harness/tests/run-casan4-harness-tests.sh",
|
"project_gate": "packages/casan-harness/scripts/bash/project-gate.sh",
|
||||||
"dashboard": "docs/output/casan/agentops-dashboard.html"
|
"agentic_bridge": "packages/casan-harness/scripts/python/agentic_bridge.py"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
version: 1.0
|
version: 1.0
|
||||||
description: CASAN Level 5 business KPI feedback schema
|
description: CASAN business KPI feedback schema
|
||||||
|
|
||||||
kpis:
|
kpis:
|
||||||
- id: cycle_time_minutes
|
- id: cycle_time_minutes
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
version: 1.0
|
version: 1.0
|
||||||
description: CASAN Level 5 model/tool fallback policy
|
description: CASAN model/tool fallback policy
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
- step_pattern: "review|test|security"
|
- step_pattern: "review|test|security"
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
version: 1.0
|
version: 1.0
|
||||||
description: CASAN Level 5 tool registry with ownership, schema, risk, idempotency, and rollback metadata
|
description: CASAN production tool registry with ownership, schema, risk, idempotency, and rollback metadata
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
- id: agent_step
|
- id: agent_step
|
||||||
@@ -41,8 +41,8 @@ _casan_find_app_root() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Harness (CODE) root: the tree that holds scripts/, security/, templates/, tests/,
|
# Harness (CODE) root: the production tree that holds scripts/, security/,
|
||||||
# config/, level5/*.yaml. Derived from this file's location so it follows the move.
|
# templates/, config/, adapters/, and schemas/. Derived from this file's location.
|
||||||
if [[ -z "${CASAN_HARNESS_ROOT:-}" ]]; then
|
if [[ -z "${CASAN_HARNESS_ROOT:-}" ]]; then
|
||||||
CASAN_HARNESS_ROOT="$(cd "$_casan_paths_self/../.." && pwd)"
|
CASAN_HARNESS_ROOT="$(cd "$_casan_paths_self/../.." && pwd)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ BIN = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash")
|
|||||||
MODEL_ROUTER = os.environ.get("CASAN_GOAL_MODEL_ROUTER") or os.path.join(BIN, "model-router.sh")
|
MODEL_ROUTER = os.environ.get("CASAN_GOAL_MODEL_ROUTER") or os.path.join(BIN, "model-router.sh")
|
||||||
SECURITY = os.path.join(BIN, "security-check.sh")
|
SECURITY = os.path.join(BIN, "security-check.sh")
|
||||||
STATE_ROOT = os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
|
STATE_ROOT = os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
|
||||||
PROJECT_REGISTRY = os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json")
|
PROJECT_REGISTRY = os.path.join(ROOT, "packages", "casan-harness", "config", "project-registry.json")
|
||||||
APPROVAL_INBOX = os.path.join(BIN, "approval-inbox.py")
|
APPROVAL_INBOX = os.path.join(BIN, "approval-inbox.py")
|
||||||
_MANIFEST_SPEC = importlib.util.spec_from_file_location(
|
_MANIFEST_SPEC = importlib.util.spec_from_file_location(
|
||||||
"casan_goal_project_manifest", os.path.join(BIN, "project_manifest.py")
|
"casan_goal_project_manifest", os.path.join(BIN, "project_manifest.py")
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def _manifest_for_files(files: list[str]) -> dict:
|
|||||||
if selected or project:
|
if selected or project:
|
||||||
return PROJECT_MANIFEST.load(ROOT, selected, project)
|
return PROJECT_MANIFEST.load(ROOT, selected, project)
|
||||||
|
|
||||||
registry = load(os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json"))
|
registry = load(os.path.join(ROOT, "packages", "casan-harness", "config", "project-registry.json"))
|
||||||
candidates = []
|
candidates = []
|
||||||
for entry in registry.get("projects", []):
|
for entry in registry.get("projects", []):
|
||||||
manifest_path = entry.get("manifest")
|
manifest_path = entry.get("manifest")
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def load(root: str, manifest_path: str | None = None, project_id: str | None = N
|
|||||||
if selected:
|
if selected:
|
||||||
path = within(root, selected, "manifest")
|
path = within(root, selected, "manifest")
|
||||||
elif requested:
|
elif requested:
|
||||||
registry = _json(os.path.join(root, "packages/casan-harness/level5/project-registry.json"), "project registry")
|
registry = _json(os.path.join(root, "packages/casan-harness/config/project-registry.json"), "project registry")
|
||||||
entry = next((item for item in registry.get("projects", []) if item.get("project_id") == requested), None)
|
entry = next((item for item in registry.get("projects", []) if item.get("project_id") == requested), None)
|
||||||
if not entry:
|
if not entry:
|
||||||
_fail(f"project is not registered: {requested}")
|
_fail(f"project is not registered: {requested}")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ fi
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$SCRIPT_DIR/casan-paths.sh"
|
source "$SCRIPT_DIR/casan-paths.sh"
|
||||||
PROJECT_ROOT="$CASAN_APP_ROOT"
|
PROJECT_ROOT="$CASAN_APP_ROOT"
|
||||||
REGISTRY="$CASAN_HARNESS_ROOT/level5/tool-registry.yaml"
|
REGISTRY="$CASAN_HARNESS_ROOT/config/tool-registry.yaml"
|
||||||
LOG_DIR="$CASAN_STATE_ROOT/logs/level5"
|
LOG_DIR="$CASAN_STATE_ROOT/logs/level5"
|
||||||
AUDIT_DIR="$CASAN_STATE_ROOT/logs/audit"
|
AUDIT_DIR="$CASAN_STATE_ROOT/logs/audit"
|
||||||
mkdir -p "$LOG_DIR" "$AUDIT_DIR"
|
mkdir -p "$LOG_DIR" "$AUDIT_DIR"
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$SCRIPT_DIR/casan-paths.sh"
|
source "$SCRIPT_DIR/casan-paths.sh"
|
||||||
PROJECT_ROOT="$CASAN_APP_ROOT"
|
PROJECT_ROOT="$CASAN_APP_ROOT"
|
||||||
REGISTRY="$CASAN_HARNESS_ROOT/level5/project-registry.json"
|
REGISTRY="$CASAN_HARNESS_ROOT/config/project-registry.json"
|
||||||
PACKAGE="$CASAN_HARNESS_ROOT/level5/harness-package.json"
|
PACKAGE="$CASAN_HARNESS_ROOT/config/harness-package.json"
|
||||||
|
|
||||||
python - "$REGISTRY" "$PACKAGE" <<'PY'
|
python - "$REGISTRY" "$PACKAGE" <<'PY'
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env pwsh
|
#!/usr/bin/env pwsh
|
||||||
# CASAN L5 Tool Registry Gate - PowerShell port of tool-registry-gate.sh
|
# CASAN Tool Registry Gate - PowerShell port of tool-registry-gate.sh
|
||||||
# Usage:
|
# Usage:
|
||||||
# tool-registry-gate.ps1 <tool-id> [idempotency-key]
|
# tool-registry-gate.ps1 <tool-id> [idempotency-key]
|
||||||
#
|
#
|
||||||
@@ -13,8 +13,9 @@ param(
|
|||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||||
$projectRoot = (Resolve-Path (Join-Path $scriptDir "../../..")).Path
|
$harnessRoot = if ($env:CASAN_HARNESS_ROOT) { $env:CASAN_HARNESS_ROOT } else { (Resolve-Path (Join-Path $scriptDir "../..")).Path }
|
||||||
$registry = Join-Path $projectRoot ".specify/level5/tool-registry.yaml"
|
$projectRoot = if ($env:CASAN_APP_ROOT) { $env:CASAN_APP_ROOT } else { (Resolve-Path (Join-Path $scriptDir "../../../..")).Path }
|
||||||
|
$registry = Join-Path $harnessRoot "config/tool-registry.yaml"
|
||||||
$logDir = Join-Path $projectRoot ".specify/logs/level5"
|
$logDir = Join-Path $projectRoot ".specify/logs/level5"
|
||||||
$txLog = Join-Path $logDir "tool-registry.jsonl"
|
$txLog = Join-Path $logDir "tool-registry.jsonl"
|
||||||
$toolCallLog = Join-Path $projectRoot ".specify/logs/audit/tool-calls.jsonl"
|
$toolCallLog = Join-Path $projectRoot ".specify/logs/audit/tool-calls.jsonl"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ guarantee from Plan-16 even when the harness lives outside the repo.
|
|||||||
Only GATE-relevant trees are hashed (the code that makes security/governance
|
Only GATE-relevant trees are hashed (the code that makes security/governance
|
||||||
decisions), never runtime state/logs:
|
decisions), never runtime state/logs:
|
||||||
|
|
||||||
scripts/bash scripts/python security level5 adapters schemas
|
scripts/bash scripts/python security config adapters schemas
|
||||||
|
|
||||||
stdlib-only, deterministic (sorted paths), text-mode agnostic (hashes raw bytes).
|
stdlib-only, deterministic (sorted paths), text-mode agnostic (hashes raw bytes).
|
||||||
|
|
||||||
@@ -30,10 +30,9 @@ import sys
|
|||||||
|
|
||||||
ALGO = "sha256"
|
ALGO = "sha256"
|
||||||
HASHED_SUBTREES = (
|
HASHED_SUBTREES = (
|
||||||
"scripts/bash", "scripts/python", "security", "level5", "adapters", "schemas")
|
"scripts/bash", "scripts/python", "security", "config", "adapters", "schemas")
|
||||||
SKIP_DIR_NAMES = {"__pycache__", ".git", "node_modules"}
|
SKIP_DIR_NAMES = {"__pycache__", ".git", "node_modules"}
|
||||||
SKIP_SUFFIXES = (".pyc", ".pyo", ".log", ".tmp", ".DS_Store")
|
SKIP_SUFFIXES = (".pyc", ".pyo", ".log", ".tmp", ".DS_Store")
|
||||||
# Within level5, only policy/config, not regenerated runtime artifacts.
|
|
||||||
SKIP_BASENAMES = {"policy-manifest.json", "policy-manifest.sig", "project-registry.json"}
|
SKIP_BASENAMES = {"policy-manifest.json", "policy-manifest.sig", "project-registry.json"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ HALLU_COUNT="$(tail -n 1 "$CASAN_STATE_ROOT/logs/cost/metrics.jsonl" | sed -n 's
|
|||||||
[[ "${HALLU_COUNT:-0}" -ge 3 ]] && pass "H6 detects hallucination signals (count=$HALLU_COUNT)" || fail "H6 did not detect hallucination signals (count=${HALLU_COUNT:-0})"
|
[[ "${HALLU_COUNT:-0}" -ge 3 ]] && pass "H6 detects hallucination signals (count=$HALLU_COUNT)" || fail "H6 did not detect hallucination signals (count=${HALLU_COUNT:-0})"
|
||||||
|
|
||||||
# H6: imported provider telemetry becomes the authoritative cost source
|
# H6: imported provider telemetry becomes the authoritative cost source
|
||||||
"$SCRIPTS/import-provider-telemetry.sh" "$CASAN_HARNESS_ROOT/level5/provider-usage-sample.json" > "$EVIDENCE_DIR/05c-provider-import.stdout"
|
"$SCRIPTS/import-provider-telemetry.sh" "$CASAN_HARNESS_ROOT/tests/fixtures/provider-usage-sample.json" > "$EVIDENCE_DIR/05c-provider-import.stdout"
|
||||||
CASAN_AGENT_NAME=demo.agent CASAN_STEP_NAME=speckit.implement \
|
CASAN_AGENT_NAME=demo.agent CASAN_STEP_NAME=speckit.implement \
|
||||||
"$SCRIPTS/agent-metrics.sh" "$METRICS_IN" "$EVIDENCE_DIR/05c-provider-output.txt" > "$EVIDENCE_DIR/05c-provider-metrics.stdout"
|
"$SCRIPTS/agent-metrics.sh" "$METRICS_IN" "$EVIDENCE_DIR/05c-provider-output.txt" > "$EVIDENCE_DIR/05c-provider-metrics.stdout"
|
||||||
assert_contains "$CASAN_STATE_ROOT/logs/cost/metrics.jsonl" '"cost_source":"provider_telemetry"'
|
assert_contains "$CASAN_STATE_ROOT/logs/cost/metrics.jsonl" '"cost_source":"provider_telemetry"'
|
||||||
@@ -261,7 +261,7 @@ assert_contains "$LEVEL5_DIR/14-business-kpi.stdout" "status=pass"
|
|||||||
assert_contains "$LEVEL5_DIR/16-policy-verify.stdout" "POLICY_SIGNATURE_VALID"
|
assert_contains "$LEVEL5_DIR/16-policy-verify.stdout" "POLICY_SIGNATURE_VALID"
|
||||||
|
|
||||||
# L5: real provider usage telemetry import path
|
# L5: real provider usage telemetry import path
|
||||||
"$SCRIPTS/import-provider-telemetry.sh" "$CASAN_HARNESS_ROOT/level5/provider-usage-sample.json" > "$LEVEL5_DIR/17-provider-telemetry.stdout"
|
"$SCRIPTS/import-provider-telemetry.sh" "$CASAN_HARNESS_ROOT/tests/fixtures/provider-usage-sample.json" > "$LEVEL5_DIR/17-provider-telemetry.stdout"
|
||||||
assert_contains "$LEVEL5_DIR/17-provider-telemetry.stdout" "PROVIDER_TELEMETRY_IMPORTED"
|
assert_contains "$LEVEL5_DIR/17-provider-telemetry.stdout" "PROVIDER_TELEMETRY_IMPORTED"
|
||||||
|
|
||||||
# L5: shared harness package is registered by multiple projects
|
# L5: shared harness package is registered by multiple projects
|
||||||
|
|||||||
+12
-16
@@ -7,29 +7,25 @@
|
|||||||
"level": 1,
|
"level": 1,
|
||||||
"package_names": ["casan-core", "casan-harness"],
|
"package_names": ["casan-core", "casan-harness"],
|
||||||
"status": "implemented",
|
"status": "implemented",
|
||||||
"summary": "H1-H7 harness core, security/action gates, evidence pack, audit, cost/telemetry, hardening tests, policy/config defaults, lightweight CLI.",
|
"summary": "Minimal H1-H7 production runtime: security/action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, and lightweight CLI. Source-only tests and Platform helpers are excluded.",
|
||||||
"target_users": ["governance-harness-only projects", "BJT initial adoption", "CI gate usage"],
|
"target_users": ["governance-harness-only projects", "BJT initial adoption", "CI gate usage"],
|
||||||
"includes": [
|
"includes": [
|
||||||
"packages/casan-harness/scripts",
|
"packages/casan-harness",
|
||||||
"packages/casan-harness/security",
|
|
||||||
"packages/casan-harness/governance",
|
|
||||||
"packages/casan-harness/agentops",
|
|
||||||
"packages/casan-harness/config",
|
|
||||||
"packages/casan-harness/level5",
|
|
||||||
"packages/casan-harness/memory",
|
|
||||||
"packages/casan-harness/templates",
|
|
||||||
"packages/casan-harness/tests",
|
|
||||||
"packages/casan-harness/adapters",
|
|
||||||
"packages/casan-harness/schemas",
|
|
||||||
"packages/casan-harness/init-options.json",
|
|
||||||
"packages/casan-harness/README.md",
|
|
||||||
"bin/casan",
|
"bin/casan",
|
||||||
"install.sh",
|
"install.sh",
|
||||||
"install.ps1",
|
"install.ps1",
|
||||||
"packaging/levels.json",
|
"packaging/levels.json",
|
||||||
|
"packaging/runtime-layout.json",
|
||||||
|
"scripts/copy-runtime.py",
|
||||||
"VERSION"
|
"VERSION"
|
||||||
],
|
],
|
||||||
"excludes_globs": ["**/__pycache__/**", "**/*.pyc", "**/.DS_Store"],
|
"excludes_globs": [
|
||||||
|
"**/__pycache__/**",
|
||||||
|
"**/*.pyc",
|
||||||
|
"**/.DS_Store",
|
||||||
|
"packages/casan-harness/tests/**",
|
||||||
|
"packages/casan-harness/level5/**"
|
||||||
|
],
|
||||||
"artifact": "casan-core-v{VERSION}.tar.gz",
|
"artifact": "casan-core-v{VERSION}.tar.gz",
|
||||||
"docker_image": "casan-harness:{VERSION}"
|
"docker_image": "casan-harness:{VERSION}"
|
||||||
},
|
},
|
||||||
@@ -37,7 +33,7 @@
|
|||||||
"level": 2,
|
"level": 2,
|
||||||
"package_names": ["casan-devkit", "casan-project-kit"],
|
"package_names": ["casan-devkit", "casan-project-kit"],
|
||||||
"status": "implemented",
|
"status": "implemented",
|
||||||
"summary": "Level 1 + project templates, domain-pack scaffold, Gitea workflow templates, harness Dockerfile, install script, adoption/CI/domain-pack guides.",
|
"summary": "Level 1 + production adoption commands, project templates, domain-pack scaffold, Gitea workflow templates, and adoption/CI/domain-pack guides.",
|
||||||
"target_users": ["new projects adopting CASAN", "internal teams needing a repeatable setup"],
|
"target_users": ["new projects adopting CASAN", "internal teams needing a repeatable setup"],
|
||||||
"extends": "core",
|
"extends": "core",
|
||||||
"includes": [
|
"includes": [
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"description": "Production runtime allowlist for global CASAN installs and release bundles. Source-only tests, development tooling, and Platform helpers must not cross this boundary.",
|
||||||
|
"components": {
|
||||||
|
"harness": {
|
||||||
|
"source": "packages/casan-harness",
|
||||||
|
"exclude": [
|
||||||
|
"README.md",
|
||||||
|
"init-options.json",
|
||||||
|
"level5",
|
||||||
|
"tests",
|
||||||
|
"scripts/bash/chat-cloud-smoke.sh",
|
||||||
|
"scripts/bash/ci-harness-gate.sh",
|
||||||
|
"scripts/bash/dashboard-serve.sh",
|
||||||
|
"scripts/bash/dashboard-server.py",
|
||||||
|
"scripts/bash/guideH4-H5-H6.md",
|
||||||
|
"scripts/bash/infra-lab.sh",
|
||||||
|
"scripts/bash/local-full.sh",
|
||||||
|
"scripts/bash/security-gate.sh",
|
||||||
|
"scripts/bash/test-integrity.py"
|
||||||
|
],
|
||||||
|
"transforms": {
|
||||||
|
"config/project-registry.json": "empty_project_registry"
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"adapters/claude-code/claude_hook.py",
|
||||||
|
"adapters/codex/codex_hook.py",
|
||||||
|
"adapters/vscode/vscode_hook.py",
|
||||||
|
"config/project-registry.json",
|
||||||
|
"config/tool-registry.yaml",
|
||||||
|
"scripts/bash/casan-harness.sh",
|
||||||
|
"scripts/bash/project-gate.sh",
|
||||||
|
"scripts/bash/security-check.sh",
|
||||||
|
"scripts/bash/tool-registry-gate.sh",
|
||||||
|
"scripts/python/agentic_bridge.py",
|
||||||
|
"scripts/python/harness_hash.py"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devkit": {
|
||||||
|
"source": "packages/casan-devkit",
|
||||||
|
"exclude": [
|
||||||
|
"Dockerfile.harness",
|
||||||
|
"README.md",
|
||||||
|
"install.sh",
|
||||||
|
"tests",
|
||||||
|
"windows"
|
||||||
|
],
|
||||||
|
"required": [
|
||||||
|
"casan-init.py",
|
||||||
|
"package-vscode-extension.py",
|
||||||
|
"project-scaffold.py",
|
||||||
|
"templates/domain-pack/domain-pack.yaml",
|
||||||
|
"templates/project/casan-hook.py"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ function locateManifest(root, { manifestPath, projectId } = {}) {
|
|||||||
|
|
||||||
const requestedProject = projectId || process.env.CASAN_PROJECT_ID;
|
const requestedProject = projectId || process.env.CASAN_PROJECT_ID;
|
||||||
if (requestedProject) {
|
if (requestedProject) {
|
||||||
const registryPath = join(root, 'packages/casan-harness/level5/project-registry.json');
|
const registryPath = join(root, 'packages/casan-harness/config/project-registry.json');
|
||||||
const registry = readJson(registryPath, 'project registry');
|
const registry = readJson(registryPath, 'project registry');
|
||||||
const entry = registry.projects?.find((item) => item.project_id === requestedProject || item.manifest === requestedProject);
|
const entry = registry.projects?.find((item) => item.project_id === requestedProject || item.manifest === requestedProject);
|
||||||
if (!entry) fail(`project is not registered: ${requestedProject}`);
|
if (!entry) fail(`project is not registered: ${requestedProject}`);
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy one CASAN production component through the canonical runtime allowlist."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
IGNORED_NAMES = {
|
||||||
|
"__pycache__", ".DS_Store", "node_modules", "dist", "build", "coverage",
|
||||||
|
}
|
||||||
|
IGNORED_SUFFIXES = {".pyc", ".pyo", ".log", ".tmp"}
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_layout(source_root: Path) -> dict:
|
||||||
|
path = source_root / "packaging" / "runtime-layout.json"
|
||||||
|
try:
|
||||||
|
layout = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
raise LayoutError(f"cannot load {path}: {error}") from error
|
||||||
|
if layout.get("schema_version") != 1:
|
||||||
|
raise LayoutError("runtime layout schema_version must be 1")
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
def is_excluded(relative: Path, exclusions: set[str]) -> bool:
|
||||||
|
normalized = relative.as_posix()
|
||||||
|
if any(part in IGNORED_NAMES for part in relative.parts):
|
||||||
|
return True
|
||||||
|
if relative.suffix in IGNORED_SUFFIXES:
|
||||||
|
return True
|
||||||
|
return any(
|
||||||
|
normalized == excluded or normalized.startswith(excluded.rstrip("/") + "/")
|
||||||
|
for excluded in exclusions
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transformed_content(path: Path, transform: str) -> bytes:
|
||||||
|
if transform != "empty_project_registry":
|
||||||
|
raise LayoutError(f"unknown runtime transform: {transform}")
|
||||||
|
try:
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
raise LayoutError(f"cannot transform project registry: {error}") from error
|
||||||
|
payload["projects"] = []
|
||||||
|
return (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_component(
|
||||||
|
source_root: Path,
|
||||||
|
destination_root: Path,
|
||||||
|
name: str,
|
||||||
|
preserved: set[str] | None = None,
|
||||||
|
clean: bool = False) -> dict:
|
||||||
|
layout = load_layout(source_root)
|
||||||
|
component = layout.get("components", {}).get(name)
|
||||||
|
if not isinstance(component, dict):
|
||||||
|
raise LayoutError(f"unknown runtime component: {name}")
|
||||||
|
|
||||||
|
relative_source = Path(str(component.get("source", "")))
|
||||||
|
source = (source_root / relative_source).resolve()
|
||||||
|
destination = destination_root / relative_source
|
||||||
|
if not source.is_dir():
|
||||||
|
raise LayoutError(f"component source is missing: {source}")
|
||||||
|
if source_root not in source.parents:
|
||||||
|
raise LayoutError(f"component source escapes source root: {source}")
|
||||||
|
|
||||||
|
exclusions = {
|
||||||
|
str(path).replace("\\", "/").strip("/")
|
||||||
|
for path in component.get("exclude", [])
|
||||||
|
if isinstance(path, str) and path.strip("/")
|
||||||
|
}
|
||||||
|
unsafe = [
|
||||||
|
path for path in exclusions
|
||||||
|
if Path(path).is_absolute() or ".." in Path(path).parts
|
||||||
|
]
|
||||||
|
if unsafe:
|
||||||
|
raise LayoutError(
|
||||||
|
f"runtime component {name} has unsafe exclusions: {', '.join(unsafe)}")
|
||||||
|
transforms = component.get("transforms", {})
|
||||||
|
if not isinstance(transforms, dict):
|
||||||
|
raise LayoutError(f"runtime component {name} transforms must be an object")
|
||||||
|
removed = 0
|
||||||
|
if clean and destination.exists():
|
||||||
|
for excluded in sorted(exclusions, reverse=True):
|
||||||
|
candidate = destination / excluded
|
||||||
|
if candidate.is_dir() and not candidate.is_symlink():
|
||||||
|
shutil.rmtree(candidate)
|
||||||
|
removed += 1
|
||||||
|
elif candidate.exists() or candidate.is_symlink():
|
||||||
|
candidate.unlink()
|
||||||
|
removed += 1
|
||||||
|
for candidate in sorted(destination.rglob("*"), reverse=True):
|
||||||
|
if (candidate.name in IGNORED_NAMES or
|
||||||
|
(candidate.is_file() and candidate.suffix in IGNORED_SUFFIXES)):
|
||||||
|
if candidate.is_dir() and not candidate.is_symlink():
|
||||||
|
shutil.rmtree(candidate)
|
||||||
|
else:
|
||||||
|
candidate.unlink()
|
||||||
|
removed += 1
|
||||||
|
copied = 0
|
||||||
|
skipped = 0
|
||||||
|
preserved = preserved or set()
|
||||||
|
for path in sorted(source.rglob("*")):
|
||||||
|
relative = path.relative_to(source)
|
||||||
|
if is_excluded(relative, exclusions):
|
||||||
|
skipped += path.is_file()
|
||||||
|
continue
|
||||||
|
target = destination / relative
|
||||||
|
if path.is_dir():
|
||||||
|
continue
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
target_relative = (relative_source / relative).as_posix()
|
||||||
|
if target_relative in preserved and target.exists():
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
transform = transforms.get(relative.as_posix())
|
||||||
|
if transform:
|
||||||
|
target.write_bytes(transformed_content(path, str(transform)))
|
||||||
|
shutil.copymode(path, target)
|
||||||
|
else:
|
||||||
|
shutil.copy2(path, target)
|
||||||
|
copied += 1
|
||||||
|
|
||||||
|
if clean and destination.exists():
|
||||||
|
for candidate in sorted(destination.rglob("*"), reverse=True):
|
||||||
|
if candidate.is_dir() and not candidate.is_symlink():
|
||||||
|
try:
|
||||||
|
candidate.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
missing = [
|
||||||
|
required for required in component.get("required", [])
|
||||||
|
if not (destination / required).is_file()
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise LayoutError(
|
||||||
|
f"runtime component {name} is incomplete; missing: {', '.join(missing)}")
|
||||||
|
return {
|
||||||
|
"component": name,
|
||||||
|
"source": str(source),
|
||||||
|
"destination": str(destination),
|
||||||
|
"copied_files": copied,
|
||||||
|
"excluded_files": skipped,
|
||||||
|
"removed_paths": removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parser() -> argparse.ArgumentParser:
|
||||||
|
value = argparse.ArgumentParser(description=__doc__)
|
||||||
|
value.add_argument("--source-root", required=True)
|
||||||
|
value.add_argument("--destination-root", required=True)
|
||||||
|
value.add_argument("--component", action="append", required=True)
|
||||||
|
value.add_argument(
|
||||||
|
"--preserve", action="append", default=[],
|
||||||
|
help="destination-root-relative file to retain when it already exists")
|
||||||
|
value.add_argument(
|
||||||
|
"--clean", action="store_true",
|
||||||
|
help="remove excluded source-only paths left by an older installation")
|
||||||
|
value.add_argument("--json", action="store_true")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = parser().parse_args(argv)
|
||||||
|
source_root = Path(args.source_root).expanduser().resolve()
|
||||||
|
destination_root = Path(args.destination_root).expanduser().resolve()
|
||||||
|
destination_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
preserved = {
|
||||||
|
str(path).replace("\\", "/").strip("/")
|
||||||
|
for path in args.preserve
|
||||||
|
if str(path).strip("/")
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
results = [
|
||||||
|
copy_component(
|
||||||
|
source_root, destination_root, component, preserved, args.clean)
|
||||||
|
for component in args.component
|
||||||
|
]
|
||||||
|
except LayoutError as error:
|
||||||
|
sys.stderr.write(f"copy-runtime: {error}\n")
|
||||||
|
return 1
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"status": "ok", "components": results}, indent=2))
|
||||||
|
else:
|
||||||
|
for result in results:
|
||||||
|
print(
|
||||||
|
"copy-runtime: {component} copied={copied_files} "
|
||||||
|
"excluded={excluded_files} removed={removed_paths}".format(**result))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -16,7 +16,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
MANIFEST="packaging/levels.json"
|
MANIFEST="packaging/levels.json"
|
||||||
VERSION="$(cat VERSION 2>/dev/null || echo 0.0.0)"
|
VERSION="$(cat VERSION 2>/dev/null || echo 0.0.0)"
|
||||||
DIST="$ROOT/dist"
|
DIST="${CASAN_DIST_DIR:-$ROOT/dist}"
|
||||||
|
|
||||||
BUNDLE="${1:-}"
|
BUNDLE="${1:-}"
|
||||||
if [[ -z "$BUNDLE" ]]; then
|
if [[ -z "$BUNDLE" ]]; then
|
||||||
@@ -87,6 +87,18 @@ copy_path() { # <relpath>
|
|||||||
fi
|
fi
|
||||||
local dest="$PKGDIR/$1"
|
local dest="$PKGDIR/$1"
|
||||||
mkdir -p "$(dirname "$dest")"
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
case "$1" in
|
||||||
|
packages/casan-harness)
|
||||||
|
python3 "$ROOT/scripts/copy-runtime.py" \
|
||||||
|
--source-root "$ROOT" --destination-root "$PKGDIR" --component harness
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
packages/casan-devkit)
|
||||||
|
python3 "$ROOT/scripts/copy-runtime.py" \
|
||||||
|
--source-root "$ROOT" --destination-root "$PKGDIR" --component devkit
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
# copy, dropping caches/artifacts
|
# copy, dropping caches/artifacts
|
||||||
if [[ -d "$src" ]]; then
|
if [[ -d "$src" ]]; then
|
||||||
rsync -a --exclude='__pycache__' --exclude='*.pyc' --exclude='.DS_Store' "$src/" "$dest/" 2>/dev/null \
|
rsync -a --exclude='__pycache__' --exclude='*.pyc' --exclude='.DS_Store' "$src/" "$dest/" 2>/dev/null \
|
||||||
|
|||||||
@@ -37,6 +37,19 @@ for path in "${required[@]}"; do
|
|||||||
}
|
}
|
||||||
done
|
done
|
||||||
|
|
||||||
|
for path in \
|
||||||
|
packages/casan-harness/tests \
|
||||||
|
packages/casan-harness/level5 \
|
||||||
|
packages/casan-harness/scripts/bash/ci-harness-gate.sh \
|
||||||
|
packages/casan-harness/scripts/bash/security-gate.sh \
|
||||||
|
packages/casan-harness/scripts/bash/test-integrity.py \
|
||||||
|
packages/casan-devkit/tests; do
|
||||||
|
[[ ! -e "$ROOT/$path" ]] || {
|
||||||
|
echo "ARTIFACT_SMOKE_FAIL reason=source_only_path_present path=$path" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
(cd "$ROOT" && shasum -a 256 -c SHA256SUMS >/dev/null)
|
(cd "$ROOT" && shasum -a 256 -c SHA256SUMS >/dev/null)
|
||||||
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
|
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||||
CLI_VERSION="$(cd "$ROOT" && ./bin/casan version)"
|
CLI_VERSION="$(cd "$ROOT" && ./bin/casan version)"
|
||||||
|
|||||||
Reference in New Issue
Block a user