Compare commits

...
13 Commits
Author SHA1 Message Date
thanhnv 06c404084d feat(casan): add native plugin facade
CASAN Supply Chain and Provenance / verify-scan-attest (push) Canceled after 0s
CASAN CI Gate / Frontend Tests (H3 gate) (push) Canceled after 0s
CASAN CI Gate / CASAN Security Gate + Vault KMS (H4/H5) (push) Canceled after 0s
CASAN CI Gate / Build & Deploy OKR → /opt/webapps/okr (push) Canceled after 0s
CASAN Harness CI / harness (push) Canceled after 0s
2026-08-18 16:26:27 +07:00
admin 581000bb56 Merge pull request 'feat(casan): establish assurance kernel and harden control plane' (#12) from feat/assurance-kernel-control-plane into main
CASAN Supply Chain and Provenance / verify-scan-attest (push) Canceled after 0s
CASAN CI Gate / Frontend Tests (H3 gate) (push) Canceled after 0s
CASAN CI Gate / CASAN Security Gate + Vault KMS (H4/H5) (push) Canceled after 0s
CASAN CI Gate / Build & Deploy OKR → /opt/webapps/okr (push) Canceled after 0s
CASAN Harness CI / harness (push) Canceled after 0s
Reviewed-on: http://161.33.139.73:3000/admin/casan5/pulls/12
2026-08-03 04:56:52 +00:00
thanhnv 5745519126 feat(casan): establish assurance kernel and harden control plane 2026-08-02 23:24:51 +07:00
thanhnv 8b477f3800 QA and context compress 2026-07-29 18:53:08 +07:00
thanhnv 7f01cf980a feat: make Core reports commercially production-ready 2026-07-29 13:12:11 +07:00
thanhnv 67d482ae7f fix core report inspection and vendored roots 2026-07-29 12:29:18 +07:00
thanhnv 1d6384d23c feat: simplify assurance reports with progressive disclosure 2026-07-29 11:55:49 +07:00
thanhnv a7b1c604d5 fix: preserve default chat project fallback 2026-07-28 23:18:26 +07:00
thanhnv cf568c66f4 chore: preserve Core viewer executable entrypoints 2026-07-28 23:10:16 +07:00
thanhnv 6983fb108b merge: ship Core Local Assurance Viewer 2026-07-28 23:09:57 +07:00
thanhnv 9c9efb1043 feat: ship production local assurance viewer in core 2026-07-28 23:09:22 +07:00
admin eef5a29ead Merge pull request 'feat: add production assurance dashboard flow' (#11) from codex/production-golden-path into main
Reviewed-on: http://161.33.139.73:3000/admin/casan5/pulls/11
2026-07-28 15:00:42 +00:00
admin d64e955b1c Merge pull request 'fix Codex hook trust detection' (#10) from codex/fix-codex-trust-doctor into main
Reviewed-on: http://161.33.139.73:3000/admin/casan5/pulls/10
2026-07-24 10:35:52 +00:00
152 changed files with 12780 additions and 552 deletions
+37
View File
@@ -0,0 +1,37 @@
{
"name": "casan",
"owner": {
"name": "CASAN Team"
},
"metadata": {
"description": "Native operator facade for the CASAN governed AI coding assurance runtime"
},
"plugins": [
{
"name": "casan",
"source": "./",
"description": "Adopt, diagnose, verify, and operate CASAN from Claude Code without bypassing project-local hook trust or runtime integrity controls.",
"version": "1.0.7",
"author": {
"name": "CASAN Team"
},
"license": "Proprietary",
"keywords": [
"agentic-coding",
"assurance",
"governance",
"security",
"audit"
],
"category": "workflow",
"tags": [
"agentic-coding",
"assurance",
"governance",
"security",
"audit"
],
"strict": true
}
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "casan",
"version": "1.0.7",
"description": "Native Claude Code operator facade for the CASAN governed AI coding assurance runtime.",
"author": {
"name": "CASAN Team"
},
"license": "Proprietary",
"keywords": [
"claude-code",
"agentic-coding",
"assurance",
"governance",
"security",
"audit"
],
"skills": [
"./skills/"
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "casan",
"version": "1.0.7",
"description": "Native Codex operator facade for the CASAN governed AI coding assurance runtime.",
"author": {
"name": "CASAN Team"
},
"license": "Proprietary",
"keywords": [
"agentic-coding",
"assurance",
"governance",
"security",
"audit"
],
"skills": "./skills/",
"interface": {
"displayName": "CASAN",
"shortDescription": "Governed AI coding assurance",
"longDescription": "Adopt, diagnose, verify, and operate the CASAN assurance runtime from Codex while preserving project-local hook trust and the versioned shared runtime.",
"developerName": "CASAN Team",
"category": "Developer Tools",
"capabilities": [
"Project adoption",
"Assurance verification",
"Governed execution"
],
"defaultPrompt": "Use $casan to adopt and verify CASAN in this project."
}
}
+12
View File
@@ -93,6 +93,18 @@ jobs:
- name: Run CASAN4 harness tests (35 tests)
run: bash packages/casan-harness/tests/run-casan4-harness-tests.sh
- name: Run Core Local Assurance Viewer contracts
run: python3 packages/casan-harness/tests/local-viewer-tests.py
- name: Prove visual reports from the clean Core release artifact
run: bash packages/casan-harness/tests/core-local-viewer-artifact-tests.sh
- name: Prove managed and vendored Core adoption
run: bash packages/casan-devkit/tests/hybrid-install-tests.sh
- name: Validate native Codex and Claude plugin facades
run: python3 packages/casan-devkit/tests/native-plugin-tests.py
- name: Run adversarial harness tests (44 tests)
run: bash packages/casan-harness/tests/adversarial-harness-tests.sh
+2
View File
@@ -56,6 +56,8 @@ jobs:
node-version: "20"
cache: npm
- run: npm ci
- name: Validate native Codex and Claude plugin facades
run: python3 packages/casan-devkit/tests/native-plugin-tests.py
- name: Run reusable governance suites
env:
CASAN_CI_RUN_PROJECT: "0"
+1
View File
@@ -77,6 +77,7 @@ o6.txt
o7.txt
t6.txt
t7.txt
tmp/design-qa/
# TS build cache
*.tsbuildinfo
+1 -1
View File
@@ -28,6 +28,7 @@ COPY packages/casan-control-panel/frontend/package.json ./packages/casan-control
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/casan-control-panel/backend/dist ./packages/casan-control-panel/backend/dist
COPY packages/casan-harness/scripts ./packages/casan-harness/scripts
COPY packages/casan-harness/kernel ./packages/casan-harness/kernel
COPY packages/casan-harness/config ./packages/casan-harness/config
COPY packages/casan-harness/security ./packages/casan-harness/security
COPY packages/casan-harness/config/project-registry.json ./packages/casan-harness/config/project-registry.json
@@ -46,7 +47,6 @@ WORKDIR /app/packages/casan-control-panel/backend
ENV NODE_ENV=production
ENV CASAN_PROFILE=prod
ENV CASAN_CP_STRICT=1
ENV CASAN_CP_TRUST_AUTH_PROXY=1
ENV CP_BIND=0.0.0.0
ENV CP_PORT=3010
+66 -12
View File
@@ -18,11 +18,17 @@ Mô hình production mặc định:
suite, test scripts, internal CI runners, `level5`, dashboard lab, source docs
hay release tooling.
Ngay sau init, CASAN ghi hai projection do CASAN sở hữu:
`.casan/discovery.json` và `.casan/readiness.json`. Readiness tách ba chiều:
**Core**, **Domain Pipeline** và **Provider Telemetry**. Core có thể sẵn sàng
cho prompt/report dù hai chiều tùy chọn còn `not_configured` hoặc
`optional_unavailable`.
## Trạng thái sản phẩm
| Thành phần | Trạng thái | Phạm vi |
|---|---|---|
| Core | Implemented | H1–H7 harness, hooks, policy gates, audit, evidence và CLI |
| Core | Implemented | H1–H7 harness, hooks, policy gates, audit, evidence, CLI và Local Assurance Viewer |
| DevKit | Implemented | Core + adoption tooling, domain-pack và CI template |
| Control Plane | Preview | Live H1–H7, H6, run history và evidence export; deploy riêng |
| Enterprise | Chưa phát hành | OIDC/KMS/WORM/HA/DR/SLA; CLI chủ động từ chối |
@@ -48,23 +54,30 @@ capability được đóng gói; runtime mô tả vị trí; maturity mô tả m
## Golden path: prompt → live assurance
Core không export HTML trên hot path. Sau mỗi prompt, hook tự ghi trace/H6 và
trả assurance receipt. Nếu project đã enroll Control Plane, receipt có deep link
đến đúng run; nếu offline, dùng `casan report latest`.
Core không export HTML và không giữ web server trên hot path. Sau mỗi prompt,
hook chỉ ghi trace/H6 và trả assurance receipt. Khi cần xem, `casan view` khởi
động/reuse **Local Assurance Viewer** read-only trên loopback và mở đúng run.
Viewer, H1–H7, H6, history và export đều nằm trong Core, hoạt động offline,
không cần Node/npm hoặc Platform.
```bash
# Platform preview: một lệnh, tự trỏ Control Plane vào project hiện tại
casan dashboard start
# Sau một prompt
casan report latest
casan view # mở run mới nhất
casan report export --format html # snapshot chỉ tạo khi được yêu cầu
casan readiness --refresh # Core / Domain / Provider, không chạy pipeline
casan view # mở run mới nhất trong Core viewer
casan report export --format html # run dossier, chỉ tạo khi được yêu cầu
casan report export --h6 --format html # H6 dossier on-demand
# Lifecycle viewer cục bộ
casan dashboard status
casan dashboard stop
```
`casan dashboard start` là convenience launcher cho local demo/evaluation.
Production triển khai Control Plane như service dùng chung và enroll project
bằng `casan init --dashboard-url https://casan.example`.
Không cần chạy `export` sau mỗi prompt. Evidence là source of truth; HTML/JSON
chỉ là projection on-demand. `casan dashboard start` trong Core mở viewer
single-project. Khi bundle Platform hiện diện, cùng lệnh đó quản lý Control
Plane. Production triển khai Platform như service dùng chung chỉ khi cần
multi-project, RBAC tập trung, approvals và fleet operations.
## Quick start
@@ -74,6 +87,33 @@ bằng `casan init --dashboard-url https://casan.example`.
- Windows: PowerShell 5.1+, Python 3 và Git for Windows/Git Bash.
- Client tương ứng nếu cần: Claude Code, Codex hoặc VS Code.
### Native plugin façade — tùy chọn
Repository root đồng thời là marketplace source cho Codex và Claude Code. Plugin
chỉ cung cấp skill `$casan` để agent biết cách adopt, diagnose và verify CASAN;
nó **không** tự bật hook, không tự cài runtime và không thay thế bước trust của
client.
Từ một checkout đã được tổ chức phê duyệt:
```bash
# Codex
codex plugin marketplace add /absolute/path/to/CASAN
codex plugin add casan@casan
```
Trong Claude Code:
```text
/plugin marketplace add /absolute/path/to/CASAN
/plugin install casan@casan
```
Sau khi cài plugin, mở session mới và gọi `$casan`. Runtime production vẫn được
cài một lần bằng `install.sh`/`install.ps1`, sau đó mỗi repository phải chạy
`casan init`. Không cài chồng native plugin và một bản skill copy thủ công vào
cùng client.
### 1. Cài DevKit một lần trên máy
Từ checkout hoặc release bundle đã được duyệt:
@@ -148,6 +188,7 @@ Project đã init không bị hỏi lại runtime: CASAN giữ nguyên mode hi
```bash
casan doctor
casan readiness --refresh
casan verify-harness
casan edition show
```
@@ -155,6 +196,11 @@ casan edition show
Với Codex, mở `/hooks`, review và trust đúng project hook sau lần init hoặc khi
bootstrap hash thay đổi.
`casan readiness` là product status dành cho người vận hành và dashboard;
`casan doctor` là diagnostic sâu cho integrity, hook, smoke test và trust.
Không dùng trạng thái thiếu Domain Pack hoặc thiếu token/cost provider để hạ
Core thành failed.
Codex hooks gọi bootstrap tương đối từ project root và không phụ thuộc vào
`git rev-parse`, nên ownership hoặc cấu hình Git không thể làm hỏng lifecycle
hook. Git vẫn được khuyến nghị mạnh cho source provenance, review diff và
@@ -377,6 +423,9 @@ installation hoặc project runtime.
│ ├── version.lock
│ ├── casan-hook.py
│ ├── agentic.env
│ ├── discovery.json
│ ├── readiness.json
│ ├── domain.json # chỉ khi chọn manifest bằng casan domain configure
│ ├── init-manifest.json
│ └── runtime/casan-core/ # chỉ khi --runtime vendored
├── .specify/
@@ -397,6 +446,9 @@ installation hoặc project runtime.
| `.casan/version.lock` | Pin version và SHA-256 của Core runtime đã resolve |
| `.casan/casan-hook.py` | Stdlib bootstrap, verify pin rồi dispatch adapter |
| `.casan/agentic.env` | Compatibility/reference flags; runtime đọc `config.json` |
| `.casan/discovery.json` | Inventory bounded các marker/source/requirements/Domain Pack candidate; không sửa source |
| `.casan/readiness.json` | Contract Core / Domain Pipeline / Provider Telemetry dùng chung cho CLI và viewer |
| `.casan/domain.json` | Reference CASAN-owned tới manifest do project sở hữu; chỉ tạo khi `casan domain configure` |
| `.casan/init-manifest.json` | Danh sách file CASAN quản lý, checksum và backup |
| `.casan/runtime/casan-core/` | Core production-only; chỉ có ở Vendored |
| `.specify/logs`, `.specify/state` | Trace, audit và state runtime; không commit |
@@ -586,6 +638,8 @@ Dùng explicit route `@casan <prompt>` và kiểm tra
| Path | Trách nhiệm |
|---|---|
| `.codex-plugin/`, `.claude-plugin/` | Native marketplace manifests; không tự bật enforcement |
| `skills/casan/` | Operator skill façade dùng chung cho Codex và Claude Code |
| `bin/casan` | CLI entrypoint |
| `install.sh`, `install.ps1` | Global installers |
| `packages/casan-harness/` | Runtime controls, adapters, policies, evidence và source tests |
+1 -1
View File
@@ -1 +1 @@
1.0.5
1.0.7
+27 -10
View File
@@ -60,6 +60,7 @@ if [[ -z "${HARNESS:-}" || ! -d "$HARNESS" ]]; then
fi
BASH_DIR="$HARNESS/scripts/bash"
CASAN_APP_ROOT="${CASAN_APP_ROOT:-${PROJECT_ROOT:-$(cd "$HARNESS/../.." && pwd)}}"
export CASAN_APP_ROOT
VERSION_FILE="$_self/../VERSION"
[[ -f "$VERSION_FILE" ]] || VERSION_FILE="$HARNESS/../../VERSION"
@@ -75,6 +76,8 @@ Commands:
init [--runtime managed|vendored] Adopt/reconfigure CASAN (interactive wizard by default)
uninstall [--purge] Remove CASAN from this project (preserves user config)
doctor [--client ...] Verify configured hooks, pin, adapters, and VS Code route
readiness [--refresh] [--json] Assess Core, Domain Pipeline, and Provider Telemetry
domain <status|discover|configure> Manage optional project-owned Domain Pack selection
edition <show|set> Show / change the product edition
level <show|set 1..4> Deprecated alias for edition
verify-harness Verify the resolved harness matches the project pin
@@ -87,10 +90,10 @@ Commands:
prompt verify Verify the adopted prompt-enforcement contract
prompt trace <trace-id> Verify that a prompt trace is H1-H7 certified
report latest [--json] Show the latest prompt assurance receipt
report export [trace] [--format] Export a trace snapshot on demand
view [trace-id] Open a trace in the enrolled Control Plane
pipeline [--manifest path] Run the manifest-driven SRS→test pipeline
dashboard <start|status|stop|open> Run the local Control Plane (Platform only)
report export [trace] [--format] Export a run/H6 dossier on demand
view [trace-id] Open the Core Local Assurance Viewer
pipeline [--manifest path] Run an optional project-owned SRS→test pipeline
dashboard <start|status|stop|open> Run local assurance (Core) or Control Plane
version Print version
help This help
@@ -115,6 +118,10 @@ case "$cmd" in
doctor)
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: doctor requires the casan-devkit package" >&2; exit 1; }
exec python3 "$DEVKIT_ROOT/casan-init.py" doctor "$@" ;;
readiness)
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" readiness "$@" ;;
domain)
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" domain "$@" ;;
level)
[[ -f "$DEVKIT_ROOT/casan-init.py" ]] || { echo "casan: level requires the casan-devkit package" >&2; exit 1; }
sub="${1:-show}"; shift || true
@@ -179,18 +186,28 @@ case "$cmd" in
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" view "$@" ;;
pipeline)
RUNNER="$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs"
[[ -f "$RUNNER" ]] || { echo "casan: pipeline runner is not installed" >&2; exit 1; }
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
cat <<'EOF'
Usage: casan pipeline [project runner arguments]
Runs a project-owned manifest-driven SRS→test pipeline when
scripts/run-casan-pipeline.mjs exists. The pipeline is an optional Domain Pack
capability; CASAN Core prompt assurance and visual reports do not depend on it.
EOF
exit 0
fi
if [[ ! -f "$RUNNER" ]]; then
echo "CASAN_PIPELINE_NOT_CONFIGURED — no project-owned Domain Pipeline runner was found." >&2
echo "Core remains available: use 'casan view' and 'casan readiness'." >&2
exit 2
fi
exec node "$RUNNER" "$@" ;;
dashboard)
CONTROL_PLANE_SCRIPT="$HARNESS/../casan-control-panel/scripts/control-plane-local.sh"
if [[ -f "$CONTROL_PLANE_SCRIPT" ]]; then
exec bash "$CONTROL_PLANE_SCRIPT" "$@"
fi
[[ -f "$BASH_DIR/dashboard-serve.sh" ]] || {
echo "casan: dashboard requires the Platform bundle" >&2
exit 1
}
exec bash "$BASH_DIR/dashboard-serve.sh" "$@" ;;
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" viewer "$@" ;;
version|-v|--version) version ;;
help|-h|--help) usage ;;
*) echo "casan: unknown command '$cmd'" >&2; usage >&2; exit 64 ;;
+78
View File
@@ -0,0 +1,78 @@
# CASAN report redesign — Design QA
## Comparison target
- Source visual truth:
- `/Users/thanhnguyen/Downloads/hình ảnh (1).png` — 1844 × 862 px.
- `/Users/thanhnguyen/Downloads/hình ảnh (2).png` — 1796 × 795 px.
- Rendered implementation:
- `tmp/design-qa/h6-default-final.png` — 866 × 2165 px.
- Product state: Core Local Assurance Viewer, H6, stale/insufficient telemetry,
six successful runs, technical disclosures collapsed.
- Browser viewport: 866 × 1043 CSS px, DPR 2. The browser capture is stored at
CSS-pixel width; the reference screens are wider desktop compositions, so
comparison is by hierarchy, density and visualization pattern rather than
pixel-identical scaling.
- Responsive verification: 390 × 844 CSS px, document `scrollWidth` 390 px.
## Full-view comparison evidence
The two reference reports and the final implementation were opened together in
one comparison input. CASAN intentionally preserves the references' strongest
patterns—headline metrics, measured distributions, explicit provenance and
forensic detail—while moving from an all-dark engineering sheet to the existing
CASAN light decision workspace.
## Required fidelity surfaces
- Fonts and typography: the references use dense monospace throughout. CASAN
intentionally uses the existing system sans hierarchy for decision content
and reserves monospace for IDs, timestamps and evidence values. Headings,
labels and body copy wrap without clipping.
- Spacing and layout rhythm: four executive metrics, decision findings, outcome
distribution and evidence quality form a consistent vertical sequence.
Technical content is removed from the default reading path.
- Colors and tokens: navy, cyan, green, amber and red follow the existing CASAN
semantic palette. Every colored state also has a visible label or value.
- Image and asset fidelity: neither reference contains raster product imagery.
No placeholder image, generated asset or replacement illustration is needed.
- Copy and content: all values and warnings come from canonical report data.
Missing token/cost values remain unavailable and no maturity claim is inferred.
## Focused-region comparison
The final full-page capture is readable at original resolution, including the
executive metrics, decision findings, outcome bar, coverage bars and collapsed
technical disclosure. A separate crop was not needed.
## Interaction and accessibility checks
- Filter and technical sections are native `details`/`summary` controls.
- Technical telemetry becomes visible when expanded and hidden when collapsed.
- Responsive navigation opens from one labeled button and closes with Escape.
- At 390 px the document has no horizontal overflow.
- Reduced-motion CSS remains present.
- Browser console warning/error check returned no entries.
## Comparison history
### Iteration 1
- Finding: **P2 — tablet navigation consumed too much report width** at the
default 866 px browser viewport.
- Evidence: `tmp/design-qa/h6-default.png`.
- Fix: raised the local viewer navigation-collapse breakpoint from 820 px to
960 px.
### Iteration 2
- Post-fix evidence: `tmp/design-qa/h6-default-final.png`.
- Result: the report uses the full tablet canvas, the menu remains keyboard
operable, all content is readable, and no actionable P0/P1/P2 mismatch remains.
## Follow-up polish
- A multi-run trend chart can be added only after the report contract exposes a
truthful ordered series. It must not be inferred from incomplete snapshots.
final result: passed
+12 -3
View File
@@ -11,7 +11,12 @@ services:
environment:
CASAN_PROFILE: prod
CASAN_CP_STRICT: "1"
CASAN_CP_TRUST_AUTH_PROXY: "1"
CASAN_CP_AUTH_MODE: jwt
CASAN_CP_JWT_ISSUER: http://localhost:18082
CASAN_CP_JWT_AUDIENCE: casan-control-panel
CASAN_CP_JWT_PUBLIC_KEY_FILE: /run/casan-idp/idp-public.pem
CASAN_CP_JWT_ROLE_CLAIM: groups
CASAN_CP_JWT_CLOCK_SKEW_SECONDS: "30"
CP_BIND: 0.0.0.0
CP_PORT: "3010"
CASAN_APP_ROOT: /app
@@ -63,6 +68,7 @@ services:
- ./apps/service-desk:/app/apps/service-desk
- ./apps/projects:/app/apps/projects
- ./packages/casan-harness/config/project-registry.json:/app/packages/casan-harness/config/project-registry.json
- ./tmp/control-panel-local/idp-public.pem:/run/casan-idp/idp-public.pem:ro
expose:
- "3010"
networks:
@@ -111,8 +117,8 @@ services:
- --oidc-groups-claim=groups
- --reverse-proxy=true
- --set-xauthrequest=true
- --pass-access-token=false
- --pass-authorization-header=false
- --pass-access-token=true
- --pass-authorization-header=true
- --skip-provider-button=true
- --ssl-insecure-skip-verify=true
expose:
@@ -129,7 +135,10 @@ services:
CASAN_IDP_SUB: oidc-ops
CASAN_IDP_EMAIL: oidc-ops@example.com
CASAN_IDP_GROUPS: casan-org-admin,casan-approver,casan-project:AINative_OKR_CASAN4
CASAN_IDP_PRIVATE_KEY_FILE: /run/casan-idp/idp-private.pem
CASAN_APPROVAL_SIGNER_TOKEN: ${CASAN_APPROVAL_SIGNER_TOKEN:-local-approval-signer-secret}
volumes:
- ./tmp/control-panel-local/idp-private.pem:/run/casan-idp/idp-private.pem:ro
ports:
- "18082:8080"
networks:
+6 -6
View File
@@ -3,9 +3,9 @@
# `production-preflight.sh` validates all files, endpoints, Object Lock and
# images before `docker compose up` is allowed.
#
# oauth2-proxy must emit X-Auth-Request-User and X-Auth-Request-Groups.
# Nginx overwrites X-CASAN-* headers before proxying to the API; the API maps
# groups such as casan-approver -> approver via rbac-check.py map-claim.
# oauth2-proxy must emit a signed OIDC access token. Nginx removes caller-supplied
# identity headers and forwards only that bearer token; the API verifies issuer,
# audience, expiry and signature in-process before deriving CASAN identity.
services:
control-panel-api:
@@ -19,7 +19,6 @@ services:
environment:
CASAN_PROFILE: prod
CASAN_CP_STRICT: "1"
CASAN_CP_TRUST_AUTH_PROXY: "1"
CP_BIND: 0.0.0.0
CP_PORT: "3010"
CASAN_APP_ROOT: /app
@@ -29,6 +28,7 @@ services:
volumes:
- ${CASAN_CP_STATE_DIR:?Set CASAN_CP_STATE_DIR}:/app/.specify
- ${CASAN_CP_OUTPUT_DIR:?Set CASAN_CP_OUTPUT_DIR}:/app/docs/output:ro
- ${CASAN_CP_IDP_PUBLIC_KEY:?Set CASAN_CP_IDP_PUBLIC_KEY}:/run/casan-idp/idp-public.pem:ro
expose:
- "3010"
networks:
@@ -57,8 +57,8 @@ services:
- --http-address=0.0.0.0:4180
- --reverse-proxy=true
- --set-xauthrequest=true
- --pass-access-token=false
- --pass-authorization-header=false
- --pass-access-token=true
- --pass-authorization-header=true
- --skip-provider-button=true
- --cookie-secure=true
- --cookie-httponly=true
+141
View File
@@ -0,0 +1,141 @@
# CASAN Assurance Kernel
Status: implemented contract layer, version `1.0.0` (2026-08-02).
## Purpose and maturity
CASAN now has a framework-independent wire contract and deterministic policy
primitives outside the native Bash topology. The native harness remains
operational and is the first compatibility adapter; the Agentic Bridge is the
second adapter. This is an incremental extraction, not a rewrite of H1–H7.
CASAN may describe this layer as an **Assurance Kernel contract and policy
foundation with two conforming adapters**. It must not claim that every legacy
loop, graph, report, or third-party runtime has migrated to the kernel.
## Boundary
```text
Execution runtime
-> runtime adapter
-> canonical Assurance Kernel envelope
-> deterministic policy / verification / evidence services
-> runtime-specific enforcement mechanisms
```
The kernel does not execute models. It does not infer security decisions from
model output. Runtime-specific data is allowed only under a namespace such as
`casan.runtime.casan-native-harness` or `casan.runtime.agentic-bridge`.
## Canonical contract
The canonical source is
`packages/casan-harness/schemas/assurance-kernel.schema.json`. It is JSON Schema
Draft 2020-12 and uses `schema_version: 1.0.0`.
The envelope defines:
- `ExecutionRun`: identity, parent, scope, environment, runtime, mode, goal,
timestamps, status, risk summary, evidence manifest and correlation.
- `ExecutionStep`: causal position, actor/action/resource, context references,
policy decisions, tool invocation, verification, evidence and outcome.
- `Actor`: human, agent, child agent, model, tool, service account, runtime,
approver or policy engine with issuer, trust and authentication evidence.
- `Delegation`: authority, resource scope, expiry, maximum depth, parent,
revocation and approval.
- `ContextItem`: origin, trust, content hash, transforms, compression lineage,
instruction/data classification and injection scan.
- `Action` and `ResourceAccess`: canonical class, operation, authority,
side-effect level, resource and environment.
- `PolicyDecision`: versioned policy, structured decision, reason codes, risk
facts, enforcement point, engine identity, time and evidence reference.
- `RuntimeConstraint` and `RuntimeCapabilities`: honest limits and supported
cancellation/intervention mechanisms.
- `VerificationRequirement` and `VerificationResult`: expected versus actual
result, evidence, independence and failure severity.
- `Claim`, `EvidenceItem` and `TraceLink`: bidirectional claim support,
integrity metadata, producer, artifact, validation, retention and typed links.
- `Approval` and `Intervention`: explicit decisions and runtime support status.
- `Outcome`: four independent results for execution, assurance,
certification and business effect.
## Deterministic kernel services
The stdlib-only Python implementation is under
`packages/casan-harness/kernel/`.
`policy.py` provides:
- Machine-readable action classification from
`config/action-classes.json`.
- Effective risk as the maximum of content, action, resource, identity and
environment risk.
- H2 registry configuration validation and fail-closed dependency decisions.
- Single-step failure-policy selection with `halt`, `quarantine`,
`require_approval` and `record_only`.
- Production trust capability evaluation.
`contracts.py` provides builders and an enforcement-boundary verifier. The
verifier rejects unsupported schema versions, invalid actor/step references,
observed-only certification, broken claim/evidence links, evidence digest
tampering, delegation cycles and excess delegation depth.
`taxonomy.py` separates `RuntimeControl.Hn`, `ReadinessCheck.Hn`,
`ReportDimension.Hn` and `CertificationClaim.Hn`. A legacy `Hn` remains a
display alias only and never authorizes cross-category interpretation.
`supervision.py` negotiates interventions against declared runtime
capabilities. Unsupported pause, rollback, cancellation, authority reduction
or quarantine returns `unsupported`; it never reports a fictitious success.
## Action policy
The action registry includes read, write, delete, database mutation,
migration, deployment, release, credential access, identity/permission change,
external network side effect, infrastructure modification and unknown actions.
High-impact actions have a high minimum risk, require an identified actor,
explicit approval and evidence. Benign text cannot reduce that floor. Unknown
shell operations also fail toward high risk. H2 registry enforcement and H5
approval remain separate defense-in-depth decisions.
## Outcome semantics
The following states are deliberately distinct:
```text
execution_result = success | failed | cancelled | quarantined | ...
assurance_result = passed | failed | degraded | not_evaluated | ...
certification_result = certified | non_certified | ineligible | ...
business_result = achieved | not_achieved | partial | not_evaluated | ...
```
A telemetry append can succeed while `execution_result=failed`. A failed
side-effecting command defaults to `halt` in enforce/production mode and cannot
produce a successful completion or normal certification.
## Evidence integrity and trust
Canonical evidence metadata is linked to claims and carries a SHA-256 digest.
This detects local mutation and broken references. It is not, by itself, an
external trust root or WORM guarantee. Production certification separately
requires external signing and an external immutable anchor.
## Compatibility rules
- New envelopes use semantic version `1.0.0`; incompatible schema changes
require a new major version.
- Additive runtime fields belong under a namespaced `extensions` object.
- The native and Agentic Bridge formats remain readable and are dual-emitted
with canonical envelopes during migration.
- Legacy evidence is not silently reinterpreted as kernel-validated evidence.
- Observe-mode and unsafe-development-bypass runs are never certified.
- Audit chain format v1 remains verifiable while new records use v2 fields.
## Claims and residual limits
CASAN can claim deterministic shared action policy, canonical execution and
evidence contracts, and cross-runtime conformance for the native harness and
Agentic Bridge. It cannot yet claim universal runtime adoption, complete
multi-runtime supervision, external infrastructure availability, or
data-backed H1–H7 dossiers beyond the reports actually backed by evidence.
+113
View File
@@ -0,0 +1,113 @@
# CASAN Control Plane
Status: production-capable packaged authentication and trust configuration;
external infrastructure remains operator-provisioned.
## Deployment modes
### Local development
- Default bind: `127.0.0.1`.
- `CASAN_CP_AUTH_MODE=local` is allowed only on loopback and outside production.
- Local actor headers are explicitly development identity and are not a
production authentication claim.
### Networked or production
- `CASAN_CP_AUTH_MODE=jwt` is mandatory.
- Production requires RS256 verification, a mounted public key, exact issuer
and audience, and a clock skew from 0 through 300 seconds.
- Missing or invalid configuration refuses startup before NestJS listens.
- HS256 is retained only for deterministic non-production tests.
## Packaged identity boundary
```text
Browser
-> enterprise OIDC
-> oauth2-proxy session
-> Nginx auth_request
removes X-CASAN-* and forwarded identity headers
forwards signed Bearer access token
-> NestJS AuthProvider
verifies RS256 signature, iss, aud, sub, exp, nbf and iat
-> VerifiedClaims
-> CASAN RBAC role mapping and scoped request identity
```
Nginx is a TLS/session boundary, not the identity authority for the API. The
API ignores caller-supplied identity assertions and derives actor, role,
tenant and project only after token verification.
## Authentication implementation
`backend/src/common/auth-provider.ts` defines:
- `AuthProvider` and `JwtAuthProvider`.
- Typed `VerifiedClaims` and `AuthenticationDecision`.
- RS256/HS256 signature verification using Node's standard crypto APIs.
- Issuer, audience, expiry, not-before, issued-at and bounded-skew checks.
- Safe tenant/project syntax validation.
- Structured authentication audit evidence without raw tokens.
- Middleware that overwrites request identity only from verified claims.
`auth-context.ts` refuses direct header use in JWT mode unless the middleware
has marked the identity verified. Existing RBAC services then map verified
groups and continue to produce governance/authorization evidence.
## Production configuration
Required runtime variables:
```text
CASAN_PROFILE=prod
CASAN_CP_AUTH_MODE=jwt
CASAN_CP_JWT_ISSUER=https://...
CASAN_CP_JWT_AUDIENCE=...
CASAN_CP_JWT_PUBLIC_KEY_FILE=/run/casan-idp/idp-public.pem
CASAN_CP_JWT_ROLE_CLAIM=groups
CASAN_CP_JWT_TENANT_CLAIM=casan_tenant
CASAN_CP_JWT_PROJECT_CLAIM=casan_project
CASAN_CP_JWT_CLOCK_SKEW_SECONDS=60
```
The host path is configured as `CASAN_CP_IDP_PUBLIC_KEY` and mounted read-only.
oauth2-proxy must set xauthrequest output and pass the access token. Images for
API, UI and oauth2-proxy must be digest-pinned.
`production-preflight.sh` validates:
- TLS hostname, expiry and matching private key.
- IdP public key readability and RSA public-key format.
- HTTPS OIDC issuer, secure cookies and token forwarding.
- Matching oauth2-proxy client ID and API audience.
- Matching issuer and bounded JWT clock skew.
- External Vault/KMS signing and S3 Object Lock provider selection.
- Live short-lived non-root Vault token lookup.
- A real Object Lock COMPLIANCE anchor write.
- Digest-pinned images and valid Compose/Nginx configuration.
## Authentication evidence
Each production authentication decision appends a JSONL record under
`CASAN_STATE_ROOT/logs/auth/decisions.jsonl` with category, policy, decision,
reason, actor, issuer, scope and request method/path. Tokens and secrets are
never written. A failure to authenticate returns HTTP 401 with a stable reason
code; it is not converted to viewer access.
## Local production-like smoke
`docker-compose.control-panel.local.yml` uses the same bearer-token path. The
smoke script generates an ephemeral RSA keypair, mounts the private key into
the mock IdP and the public key into the API, and enables token forwarding.
This validates the boundary without treating the mock IdP as production.
## Residual limitations
- Public-key rotation currently requires replacing the mounted key and
restarting the API; automated JWKS discovery and rollover are not present.
- Enterprise IdP, managed TLS, Vault and S3 are not provisioned by the repo.
- Local tests validate crypto and provider contracts, but do not prove a real
tenant's claim mapping, token lifecycle, network policy or key rotation.
- The Control Plane is production-capable for this packaged deployment model;
it is not yet a universal multi-runtime scheduler or supervisor.
@@ -0,0 +1,95 @@
# CASAN Execution Adapters
## Adapter contract
An execution adapter translates runtime events into the canonical Assurance
Kernel envelope without replacing H2, H4, H5 or other existing controls.
```text
runtime event
-> adapter mapping
-> ExecutionRun / ExecutionStep / Actor / Action
-> PolicyDecision / VerificationResult / EvidenceItem / Outcome
-> common verifier
```
Adapter code lives in `packages/casan-harness/kernel/adapters.py`. The canonical
schema and policy code must not import a native runtime.
## Native harness adapter
`NativeHarnessAdapter` maps the Bash harness execution identity, action,
command, actor, environment, policy decisions, evidence and final outcome. The
native entrypoint in `scripts/bash/casan-harness.sh` continues to emit its
existing metrics and completion protocol while atomically writing a canonical
bundle to `CASAN_STATE_ROOT/logs/kernel/`.
If a phase exits non-zero, the harness best-effort emits a canonical failed
bundle before propagating the original exit code. This emission never masks
the command failure. A successful run reports execution, assurance and
certification separately.
The adapter is compatibility code: existing H gates remain the enforcement
implementation. The kernel does not duplicate them.
## Agentic Bridge adapter
`AgenticBridgeAdapter` maps lifecycle admission, pre-tool, post-tool, telemetry
and finalize events. The bridge now:
- Uses the shared action classifier and risk floors.
- Defaults H2 registry enforcement on in enforce mode.
- Converts missing, unreadable, timed-out, malformed or failed H2 dependencies
into structured policy decisions.
- Distinguishes failed tool outcome from successful telemetry recording.
- Applies the single-step failure policy.
- Restricts certification using enforcement mode, assurance strength and trust
capability.
- Dual-emits the canonical bundle alongside its v20 lifecycle records.
The Agentic Bridge remains a single-model integration: it performs admission,
policy, evidence and finalization but does not invoke a model itself.
## Cross-runtime conformance
`tests/assurance-kernel-tests.py` runs the same invariants through both
adapters. It proves:
1. Dangerous action classification is identical.
2. Missing actor identity denies the same high-impact action.
3. Missing H2 enforcement denies in enforce mode.
4. Approval and evidence requirements are identical.
5. Claim/evidence relationships use the same contract.
6. Observed-only execution cannot be certified.
7. Execution, assurance and certification outcomes remain distinct.
8. Correlation and parent causation are preserved.
9. The same verifier detects evidence tampering.
10. Runtime details are namespaced extensions rather than kernel assumptions.
The conformance suite currently covers two paths. A future adapter must pass
the same suite before being described as kernel-conforming.
## Capability negotiation
Adapters declare `canCancel`, `canPause`, `canResume`, `canRollback`,
`canReduceAuthority` and `canQuarantine`. These are conservative. The native
harness currently declares no general asynchronous intervention capability;
the Agentic Bridge declares quarantine support only where its lifecycle can
represent it. Unsupported interventions return an explicit result.
## Adding an adapter
1. Preserve the runtime's existing security controls.
2. Map stable run, step, actor, correlation and causation identities.
3. Use the shared action policy; do not create a weaker parallel classifier.
4. Emit policy decisions and evidence references, not boolean-only results.
5. Put runtime-only fields in `casan.runtime.<adapter>` extensions.
6. Validate with `validate_bundle` and the JSON Schema.
7. Add the full cross-runtime invariant matrix.
8. Do not mark legacy or observe-only records certified.
## Residual migration scope
Custom loop runners, graphs and every historical event producer have not been
rewritten. They may continue using legacy formats, but they cannot inherit
kernel-conformance claims until they receive an adapter and pass conformance.
+109
View File
@@ -0,0 +1,109 @@
# CASAN Trust Boundaries
## Trust model
CASAN separates enforcement, evidence, identity, execution isolation and
external trust. Passing one boundary never implies that another passed.
| Boundary | Development/local | Production/strict |
|---|---|---|
| Control Plane identity | Explicit loopback local identity | In-process RS256 JWT verification required |
| H2 tool authority | Enforce defaults on; explicit bypass is high-severity/non-certifiable | Enforce on; bypass configuration rejected |
| H5 high-impact action | Approval and actor required by action floor | Strict signed approval plus external trust requirements |
| Tool isolation | Static fallback only when explicitly enabled in dev/test | Container isolation required; unavailable backend denies |
| Audit signing | Local OpenSSL permitted and labelled local | Vault/KMS external signing required |
| Immutable anchor | Local hash chain permitted and labelled local | External Object Lock/WORM provider required |
| Certification | Local/observed limitations disclosed | Unsafe fallback or emergency override cannot normally certify |
## H2 enforcement dependency
H2 returns a structured `PolicyDecision`, never a boolean-only decision. Missing
file, unreadable file, timeout, malformed protocol, missing runtime and internal
execution errors have stable reason codes. In enforce mode all dependency
errors deny. In observe mode the operation may be observed, but assurance is
degraded and certification is forbidden.
Registry enforcement defaults on for side-effecting native and Agentic Bridge
paths. `CASAN_H2_REGISTRY=0` or `CASAN_AGENTIC_H2_REGISTRY=0` is an explicit
unsafe development/test bypass. Production treats the bypass as invalid.
## H5 governance and action risk
`config/action-classes.json` is the common policy source. Effective risk is the
maximum of content, action, resource, identity and environment factors.
Deployment, release, migration, database mutation, delete,
identity/permission modification and infrastructure modification cannot be
downgraded by benign wording. Credential access and unknown commands are also
high-risk. High-impact operations require actor identity, approval and
evidence.
Production governance requires strict approval and external signing/anchor
capability. Governance audit format v2 binds action class, risk factors and
evidence requirements into the hash. The verifier continues to recognize
legacy v1 records without upgrading their claims.
## Execution isolation
`sandbox-run.sh` detects its selected capability and writes structured sandbox
evidence. In enforce/production mode a side-effecting path requires the
container backend. If Docker or the required backend is unavailable, execution
is denied with `sandbox_isolation_backend_unavailable`; there is no silent
timeout/static fallback.
The implemented container contract uses:
- Read-only root filesystem.
- Explicit workspace bind mount as the writable scope.
- Network disabled by default.
- Non-root container user.
- Dropped Linux capabilities and `no-new-privileges`.
- PID, memory, CPU and timeout limits.
- Restricted working directory and filtered environment.
- Output-size and exit-code capture at the harness boundary.
- Rootless Docker requirement in production.
CASAN does not claim AppArmor, SELinux, a custom seccomp profile, per-domain
network allowlisting or complete host isolation where those mechanisms are not
configured. Development static fallback is policy filtering, not isolation.
## Control Plane identity boundary
Arbitrary `X-CASAN-*`, `X-Forwarded-User` and similar headers are untrusted.
Nginx removes them, oauth2-proxy provides a signed access token, and the API
verifies the token before creating `VerifiedClaims`. A non-loopback local mode
or an invalid production verifier refuses startup.
## Signing and immutable evidence boundary
Local SHA-256 chains and evidence digests provide tamper detection under the
local host's trust. They are not externally trusted immutability. Normal
production certification requires both:
1. `CASAN_SIGNING_PROVIDER=vault_kms` (or another explicitly supported external
trust provider) with an HTTPS endpoint and short-lived credential.
2. `CASAN_IMMUTABLE_ANCHOR_PROVIDER=s3_object_lock` (or an explicitly supported
external WORM provider) with bucket and KMS configuration.
The packaged production path verifies Vault Transit readiness and performs an
S3 Object Lock COMPLIANCE anchor write. It does not provision either service.
`CASAN_TRUST_EMERGENCY_OVERRIDE=1` is break-glass behavior. It emits critical
evidence, keeps readiness failed and makes the run non-certifiable. It is not a
normal production fallback.
## Secret and evidence handling
- Raw JWTs and authentication secrets are not logged.
- Tool and prompt evidence remains subject to existing redaction/scanning.
- Authentication evidence records identity metadata and stable reason codes.
- Canonical evidence digests detect local metadata tampering, while external
signing/anchoring supplies production trust.
- A successful evidence append never changes a failed execution into success.
## Infrastructure not proven by repository tests
Repository-local tests use cryptographic fixtures and narrow service stubs.
Docker isolation tests are skip-aware when Docker is absent. Real enterprise
OIDC traffic, Vault policy enforcement, KMS/HSM custody, S3 retention policy,
rootless Docker host hardening and managed network controls require deployment
evidence before production claims are made.
@@ -0,0 +1,221 @@
# Assurance Kernel and Control Plane Upgrade — Implementation Log
Date: 2026-08-02
## Executive implementation result
All seven P0 items and all five requested P1 items were implemented with
targeted regression coverage. The native harness remains operational. The
Assurance Kernel now has a versioned canonical contract, deterministic shared
policy, a native adapter, an Agentic Bridge adapter and a 10-invariant
cross-runtime conformance suite. The Control Plane packages in-process RS256
identity verification and refuses unsafe network startup.
P2 was intentionally limited: taxonomy and runtime capability negotiation were
implemented, but new H4/H5/H2 report dossiers were not represented as
data-backed because the shared evidence-query migration was not safe to finish
within this change.
## Verified pre-existing behavior
- H4 already blocked injection/secrets before model execution.
- The agent bridge already denied sensitive pre-tool adapter failures in many
paths and disclosed observed-only degradation.
- The native harness already propagated command exit status; the misleading
completion gap was primarily in Agentic Bridge finalize semantics, where a
later `Stop(completed)` could obscure a failed tool.
- Container isolation already implemented read-only root, no network,
non-root execution, capability dropping and resource bounds.
- Production preflight already checked live Vault/S3 prerequisites, and
`sign-audit-head.sh` already refused local fallback for `CASAN_PROFILE=prod`.
- H6 already had a data-backed report and operational test suite; other report
dimensions remained `contract_ready`.
- H5 strict signed approval, separation of duties and OIDC approval fixtures
already existed.
## Verified gaps
- Agentic H2 returned allow when the gate script was absent.
- Agentic registry enforcement defaulted off.
- H5 risk followed text risk, allowing benign deployment text to avoid the
inherent action floor.
- Agentic telemetry and finalization did not model failed execution separately
enough to prevent misleading successful completion.
- `sandbox-run.sh` could silently fall back to weaker timeout/static behavior.
- Control Plane identity trusted forwarded identity headers when a flag was set.
- Production governance/certification did not uniformly require both external
signing and immutable anchoring capabilities.
- No stable canonical execution contract or two-runtime conformance suite
existed.
- H1–H7 categories were structurally ambiguous.
## Code changed in this task
### P0.1 — H2 missing-gate behavior
Selected implementation: `evaluate_h2_gate` returns a structured
`PolicyDecision` with stable reason codes. Missing file, permission denial,
timeout, malformed response and internal exception deny in enforce mode.
Observe mode yields `observe_only`, degraded and non-certifiable evidence.
Proof: kernel unit tests and Agentic Bridge integration tests cover every error
class and both modes.
Residual: external shell gates remain a runtime dependency; the change makes
dependency failure honest and safe rather than eliminating it.
### P0.2 — registry defaults
Selected implementation: both side-effecting native and Agentic Bridge paths
use shared configuration validation and default registry enforcement on.
Development/test bypass is explicit, high-severity and non-certifiable;
production bypass is invalid.
Proof: unit, bridge integration and legacy Agentic Bridge suites.
### P0.3 — action-class risk floors
Selected implementation: `config/action-classes.json` is the deterministic
source. Effective risk is the maximum of five factors. H5 governance and both
adapters consume it; high-impact actions require actor, approval and evidence.
Proof: benign deployment regression in unit, bridge and shell integration
tests. Existing H5 strict approval remains 12/12.
### P0.4 — single-step failure semantics
Selected implementation: kernel failure-policy selection plus bridge
post-tool/finalize changes. Execution failure, telemetry recording, assurance
continuation and certification are separate. Production side-effect
`record_only` is rejected. Native phase failure emits a failed canonical bundle
and preserves the original non-zero exit.
Proof: all four policy values, invalid/production configuration, Agentic failed
write and native exit-7 regressions.
### P0.5 — isolation fallback
Selected implementation: enforce/production chooses container isolation and
denies when unavailable. Static fallback requires an explicit dev/test flag.
Capability evidence discloses the actual backend. Production requires a
digest-pinned image and rootless Docker.
Proof: unavailable-backend and explicit-development-fallback tests. Existing
container escape tests are present but were skipped locally because Docker was
unavailable.
### P0.6 — Control Plane identity
Selected implementation: typed `AuthProvider`, RS256 JWT verification,
`VerifiedClaims`, safe scope checks, audit evidence, header overwrite and
startup validation. Nginx passes only the access token after removing identity
headers. Production preflight verifies the packaged boundary.
Proof: missing/invalid/expired/wrong-audience/wrong-issuer tests, valid RS256,
header spoofing and production startup refusal. Backend has 57 passing tests.
Residual: mounted static public key requires coordinated restart for rotation;
JWKS discovery/automatic rollover is not yet implemented.
### P0.7 — trust-root enforcement
Selected implementation: shared capability evaluation, governance enforcement,
Vault signing, Object Lock anchoring and certification restrictions. Production
local fallback is refused. Emergency override is critical, readiness-failing
and non-certifiable.
Proof: unit, bridge, shell and production configuration tests.
Residual: real Vault/KMS and S3 are customer infrastructure and were not
provisioned or contacted in this local run.
### P1 — contract and adapters
Selected implementation: JSON Schema `1.0.0`, Python builders/verifier,
namespaced extensions, dual emission, native compatibility adapter and Agentic
Bridge adapter. Evidence integrity and bidirectional claim links use one
verifier. Delegation depth/cycles are validated.
Proof: 30 kernel/unit/conformance tests, including the exact 10 cross-runtime
invariants and evidence tampering.
### P1 — H taxonomy
Selected implementation: separate typed namespaces for runtime controls,
readiness checks, report dimensions and certification claims. Structured
readiness, report and certification evidence now includes a category/type.
Legacy `Hn` remains a display mapping only.
Proof: cross-category unit test and additive Control Plane report tests.
### Limited P2 — runtime supervision
Selected implementation: conservative runtime capabilities and intervention
negotiation. Unsupported actions are explicit, not silently successful.
Residual: no general scheduler, durable supervisor state, cross-process cancel,
pause/resume implementation, budget service or rollback engine was added.
## Tests added
- `assurance-kernel-tests.py`: 30 unit and conformance tests.
- `assurance-upgrade-integration-tests.py`: 7 Agentic Bridge integration tests.
- `phase-assurance-upgrade-tests.sh`: 11 shell integration groups.
- `auth-provider.test.ts`: JWT and production identity tests.
- Existing auth-context/report tests extended for spoofing and taxonomy.
- Production preflight tests extended to a full valid contract plus audience,
local trust and missing-key failures.
## Verification results
| Suite | Result |
|---|---:|
| Kernel unit + two-adapter conformance | 30/30 pass |
| Draft 2020-12 JSON Schema validation | pass for both adapters |
| Agentic upgrade integration | 7/7 pass |
| Assurance upgrade shell integration | 11/11 pass |
| Existing Agentic Bridge acceptance/threat suite | 42/42 pass |
| Control Plane backend | 57/57 pass |
| Control Plane backend TypeScript build | pass |
| Control Plane frontend typecheck/Vite build | pass |
| Existing H5 approval identity | 12/12 pass |
| Sandbox isolation suite | 8/8 accounted; live Docker cases skipped |
| Production handoff | 8/8 pass |
| Existing H6 AgentOps | 20/20 pass |
| Readiness compatibility | 5/5 pass |
| Production and local Compose config | pass |
Legacy suites that generated tracked audit/dashboard output were run with
temporary state where supported; known generated artifacts were restored after
verification. Existing user-owned `docs/evidence/` was not modified.
## Infrastructure-dependent work not possible locally
- Live rootless Docker isolation and network/filesystem escape tests.
- Enterprise IdP login and real key-rotation behavior.
- Live Vault Transit signing against a managed policy/token.
- Live S3 Object Lock retention and organization SCP/IAM enforcement.
- Managed TLS/DNS and external endpoint smoke.
The provider interfaces, fail-closed validation and deterministic local
contract tests are implemented for each missing dependency.
## Remaining gaps
- H4/H5/H2 and remaining H1/H3/H7 dossiers still need a shared canonical
evidence-query implementation before `contract_ready` can be removed.
- Static RS256 key mount lacks JWKS rollover.
- Kernel adoption is proven for two adapters, not every loop/graph/runtime.
- Runtime supervision is capability negotiation, not a full durable supervisor.
- External trust and production host hardening require deployment evidence.
- Cross-runtime policy-bundle distribution and remote adapter admission are not
yet centralized by the Control Plane.
## Current maturity and honest claims
After this change, CASAN is a strong assurance-enabled harness with an
implemented framework-independent Assurance Kernel foundation and a packaged,
production-capable Control Plane boundary. Framework independence is proven for
two adapters under the canonical contract. Full platform-wide kernel adoption,
universal supervision, complete dossiers and provisioned external trust remain
future work.
@@ -0,0 +1,126 @@
# CASAN Assurance Kernel Migration
## Scope
This migration preserves existing native and Agentic Bridge protocols while
adding canonical kernel envelopes and changing unsafe defaults. It is designed
for incremental adoption; no big-bang H1–H7 rewrite is required.
## Behavior changes
1. H2 registry enforcement defaults on in enforce mode.
2. A missing or failed H2 dependency denies in enforce mode.
3. High-impact action classes use deterministic risk floors and require actor,
approval and evidence.
4. Failed side-effecting single-step execution defaults to halt in enforce or
production mode.
5. Enforce/production sandbox execution refuses timeout/static-only fallback.
6. Networked/production Control Plane startup requires verified JWT identity;
`CASAN_CP_TRUST_AUTH_PROXY` no longer authorizes header trust.
7. Production certification requires an external signing provider and external
immutable anchor.
8. Canonical schema `1.0.0` is dual-emitted with legacy runtime records.
## Development migration
Existing loopback development works with:
```text
CASAN_PROFILE=development
CASAN_CP_AUTH_MODE=local
CP_BIND=127.0.0.1
```
For short-lived compatibility testing only, H2 can be bypassed explicitly:
```text
CASAN_H2_REGISTRY=0
CASAN_AGENTIC_H2_REGISTRY=0
```
The bypass is high-severity, observe-only/non-certifiable and rejected in
production. Prefer registering the tool instead of retaining this setting.
Failure handling can be selected with
`CASAN_SINGLE_STEP_FAILURE_POLICY=halt|quarantine|require_approval|record_only`.
Production side effects reject `record_only` and fail closed to `halt`.
## Production Control Plane migration
1. Export the enterprise IdP's RS256 public key.
2. Configure issuer, audience, claim names and bounded clock skew in
`runtime.env` using `infra/production/runtime.env.example`.
3. Configure oauth2-proxy to pass access tokens and authorization headers.
4. Set `CASAN_CP_IDP_PUBLIC_KEY` in `casan-prod.env` for the read-only mount.
5. Add a digest-pinned `CASAN_CP_OAUTH2_PROXY_IMAGE`.
6. Remove `CASAN_CP_TRUST_AUTH_PROXY` from every environment.
7. Configure Vault/KMS and Object Lock provider variables.
8. Run `production-preflight.sh` before starting Compose.
9. Run managed endpoint smoke with an authenticated enterprise session.
The current API loads a static public key at startup. Coordinate IdP rotation by
installing the next public key and restarting the API after token overlap has
been handled operationally. Automated JWKS rollover is not yet implemented.
## Adapter migration
The native harness and Agentic Bridge already dual-emit to
`CASAN_STATE_ROOT/logs/kernel/`. Existing consumers can keep reading legacy
records. New consumers should:
1. Require `schema_version=1.0.0`.
2. Run the common verifier.
3. Respect the four separate outcome fields.
4. Reject observed-only certification.
5. Treat runtime extensions as optional namespaced data.
6. Avoid certifying legacy runs that have no canonical evidence.
For a new runtime, implement an adapter and pass the complete 10-invariant
conformance matrix before advertising kernel conformance.
## Audit compatibility
New H5 audit records use v2 hashing with action class and risk factors. The
audit verifier and head signer support v1 and v2 records. This is compatibility,
not claim upgrading: v1 records do not gain v2 risk evidence retroactively.
## Verification commands
```bash
python3 packages/casan-harness/tests/assurance-kernel-tests.py
python3 packages/casan-harness/tests/assurance-upgrade-integration-tests.py
bash packages/casan-harness/tests/phase-assurance-upgrade-tests.sh
bash packages/casan-harness/tests/phase-agentic-bridge-tests.sh
npm --workspace packages/casan-control-panel/backend test
npm --workspace packages/casan-control-panel/backend run build
npm --workspace packages/casan-control-panel/frontend run build
bash packages/casan-harness/tests/phase-production-preflight-tests.sh
```
Redirect `CASAN_STATE_ROOT` to a temporary directory for legacy suites that
write runtime evidence.
## Rollback
Code rollback can restore the previous adapters because legacy emission remains
present. Before rollback:
1. Stop new runs and preserve canonical/audit evidence.
2. Record the last v2 audit head and external anchor.
3. Roll back API, UI, Nginx and oauth2-proxy as one deployment unit.
4. Do not restore trusted-header identity on a networked deployment.
5. Do not disable production registry, isolation or trust-root requirements.
6. If operational continuity requires emergency local trust, use the explicit
break-glass override, retain the critical evidence and do not certify runs.
A rollback that reintroduces production header trust, missing-gate allow, or
silent sandbox/trust fallback is not a safe compatibility rollback.
## Expected warnings
- Development registry bypass: high severity and non-certifiable.
- Missing H2 in observe mode: degraded assurance.
- Local signer/local ledger: permitted locally, untrusted for production.
- Unsupported runtime intervention: explicit `unsupported`.
- Legacy evidence without kernel envelope: compatibility-readable but not
kernel-validated.
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -0,0 +1,52 @@
# Core-only commercial product audit
Audit date: 2026-07-29
Reference project: `Basic Design (Screen&Report)_v2.7`
## Problem reproduced
The previous empty-state overview displayed READY and operational-looking
zeroes before any governed run existed. Core, Domain Pipeline, and Provider
Telemetry were not separated, so a customer could not tell which capability
was installed, optional, or waiting for evidence.
Evidence:
- `00-init-empty-state.png`
- `01-assurance-overview.png`
- `02-run-dossier-h1-h7.png`
- `03-h6-trace-filtered.png`
## Corrected product contract
- `casan init --edition core` writes `.casan/discovery.json` and
`.casan/readiness.json`.
- Core can be `ready` while Domain Pipeline is `not_configured`/`discovered`
and Provider Telemetry is `optional_unavailable`.
- The overview hides operational KPIs until a governed run exists.
- The empty H6 view hides failure, latency, token, cost, coverage, chart and
export controls rather than displaying fabricated zeroes.
- HTML/JSON export remains on-demand.
- `casan pipeline --help` never executes a pipeline; a missing project runner
returns `CASAN_PIPELINE_NOT_CONFIGURED` without marking Core failed.
Evidence:
- `04-commercial-readiness-empty-state.png`
- `05-commercial-h6-empty-state.png`
- `06-basic-design-commercial-overview.png`
- `07-basic-design-commercial-h6.png`
## Acceptance evidence
- clean Core-only vendored init: Core `ready`, Domain Pipeline
`not_configured`, Provider Telemetry `optional_unavailable`;
- no application source or `apps/` directory created by Core init;
- Basic Design discovery: Core `ready_with_attention` because Claude/Codex are
healthy while the selected VS Code route lacks its extension; Domain
Pipeline `discovered`; Provider Telemetry `available`;
- Basic Design H1–H7 domain smoke: seven controls passed;
- Inspect latest run opens the interactive H1–H7 dossier;
- Open H6 opens the trace-filtered AgentOps dossier;
- Core and DevKit release artifacts build and pass extracted-artifact smoke;
- hybrid install suite: 104 pass, 0 fail.
+24
View File
@@ -13,6 +13,7 @@ sh install.sh --level devkit
cd /path/to/my-project
casan init --project ticketing --client claude,codex
casan doctor
casan readiness --refresh
casan verify-harness
```
@@ -20,6 +21,27 @@ The project defaults to Level 1/Core with runtime mode `managed`. Core remains
under `$CASAN_HOME`; the repository receives `.casan` config/lock/bootstrap and
the selected client hooks. The CLI output states the resolved runtime path.
After any prompt, visual assurance is available from Core itself:
```bash
casan report latest
casan view
```
The viewer is single-project, offline-capable and read-only. It starts lazily on
`127.0.0.1`, so projects do not need Platform, Node/npm or an always-running
service merely to inspect H1–H7 and H6.
The dashboard and CLI expose three independent states:
- Core: installation acceptance and client activation;
- Domain Pipeline: optional project-specific SRS→test configuration;
- Provider Telemetry: optional model/provider token and cost coverage.
`not_configured` Domain Pipeline and `optional_unavailable` Provider Telemetry
do not block Core. `casan report export` is on-demand and must not run after
every prompt.
Use this for developer workstations and managed CI runners. CI must install the
same release recorded by `.casan/version.lock` before running gates.
@@ -88,6 +110,8 @@ bin/casan pipeline --manifest apps/<project>/domain/project.manifest.json
- **Project lock/config** → `.casan/config.json` and `.casan/version.lock`.
- **Your domain data** → `apps/<project>/domain/` (via `CASAN_DOMAIN_ROOT`).
- **Runtime state** → `.specify/` (logs, audit, governance — created on first run).
- **Local viewer state** → `.specify/state/local-viewer.json` (`0600` on POSIX,
project ACL on Windows; ephemeral loopback port/session token; removed on stop).
The bootstrap resolves the mode/path from the project lock and verifies the live
Core hash before dispatch.
+15 -5
View File
@@ -9,7 +9,7 @@ contents + maturity: [`packaging/levels.json`](../../packaging/levels.json).
| Edition | Package names | Status | What it is |
|---|---|:--:|---|
| **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 |
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | Minimal H1–H7 production runtime, native Codex/Claude operator façade, plus zero-dependency Local Assurance Viewer: latest/history, interactive H1–H7, H6 and on-demand export |
| **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. |
| **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. |
@@ -42,6 +42,12 @@ Docker images (see `DOCKER_GUIDE.md`): `casan-harness:X.Y.Z` ✅ · `casan-platf
Every bundle carries `BUNDLE-MANIFEST.txt` + `SHA256SUMS`; preview bundles also carry
`PREVIEW-INCOMPLETE.txt`.
Core and every cumulative bundle also carry `.codex-plugin/`, `.claude-plugin/`
and `skills/casan/`. These files expose an optional native `$casan` operator
surface. They do not install the shared runtime or enable project hooks; the
production trust path remains global/vendored runtime installation followed by
`casan init` and client hook review.
## Production runtime boundary
`packaging/runtime-layout.json` is the canonical allowlist used by macOS/Linux,
@@ -53,7 +59,7 @@ Production Core/DevKit artifacts never include:
- 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;
- Platform-only centralized Control Plane/local-infrastructure helpers;
- source documentation, installer copies, Dockerfiles, caches, or release tooling
inside the installed version directory.
@@ -63,10 +69,11 @@ its tests so CASAN itself remains verifiable; only the install/release boundary
minimal.
## Who adopts what
- **Governance-harness-only / BJT initial / CI gate** → `casan-core`.
- **Governance-harness-only / BJT initial / CI gate / single-project visual assurance** → `casan-core`.
- **New project adopting CASAN** → `casan-devkit` (install.sh scaffolds domain + CI).
- **Want dashboards/visibility** → `casan-platform` (preview; live H1–H7,
H6 coverage and on-demand run/H6 exports today).
- **Want centralized multi-project operations, RBAC and approvals** →
`casan-platform` (preview). Core already provides single-project H1–H7, H6,
history and on-demand run/H6 exports.
- **Enterprise governed console** → future; building blocks (RBAC/tenant/KMS/WORM/approval)
already live in core.
@@ -76,6 +83,9 @@ packages/casan-harness/ # L1 core (implemented)
packages/casan-devkit/ # L2 adoption kit (implemented)
packages/casan-platform/ # L3 structure-only (preview; README)
packages/casan-enterprise/ # L4 structure-only (future; README)
skills/casan/ # native operator skill shared by Codex + Claude
.codex-plugin/ # Codex plugin manifest
.claude-plugin/ # Claude plugin + marketplace manifests
bin/casan # CLI
scripts/package-release.sh # release packager
packaging/levels.json # bundle contents + maturity (source of truth)
@@ -0,0 +1,64 @@
# CASAN Commercial Readiness Contract
## Promise
After `casan init`, a repository can use CASAN Core prompt assurance and the
Local Assurance Viewer without modifying application source, installing a
Domain Pack, running a domain pipeline, or exporting HTML after every prompt.
CASAN never turns missing evidence into zero and never calls an installation
READY when a required client activation step remains.
## State model
| Dimension | Ready state | Non-blocking state | Owner |
|---|---|---|---|
| Core | `ready`, `ready_with_attention` | — | CASAN runtime + selected client |
| Domain Pipeline | `configured` | `discovered`, `not_configured` | Project team |
| Provider Telemetry | `available` | `optional_unavailable` | Client/provider adapter |
Core installation acceptance uses H1–H7 as an installation contract:
1. H1 project configuration exists and is valid;
2. H2 selected client integration is installed and trust is explicit;
3. H3 local report capability exists in the resolved runtime;
4. H4 enforcement mode is disclosed (`observe` is a warning);
5. H5 evidence directories are writable;
6. H6 provider token/cost telemetry is measured when available and explicitly
optional when unavailable;
7. H7 bootstrap and version/hash pin exist.
When at least one selected client route is operational but another selected
route fails the deep doctor check, Core is `ready_with_attention`: reports and
governed prompts remain usable through the healthy route, while the broken
route is shown as an explicit activation action.
These checks do not claim that a real prompt run is H1–H7 certified. Run
certification is created only from canonical lifecycle evidence after a prompt.
## CASAN-owned files
- `.casan/discovery.json`: bounded read-only discovery projection;
- `.casan/readiness.json`: shared CLI/viewer state;
- `.casan/domain.json`: optional reference to a project-owned manifest.
No command above creates requirements, traceability mappings, tests, source
files or a fake runnable pipeline.
## Operator path
```bash
casan init
casan doctor
casan readiness --refresh
casan view
# Optional, only for project-specific SRS→test acceptance
casan domain discover
casan domain configure path/to/project.manifest.json
casan pipeline --help
```
`casan pipeline` runs only when the project provides
`scripts/run-casan-pipeline.mjs`. Otherwise it returns
`CASAN_PIPELINE_NOT_CONFIGURED` while leaving Core fully usable.
@@ -0,0 +1,127 @@
# CASAN Core Local Assurance Viewer
## Product decision
A project that installs only CASAN Core can inspect production-quality visual
reports. Platform is not a prerequisite for basic visibility.
Core owns the single-project review loop:
- commercial readiness split into Core, Domain Pipeline, and Provider Telemetry;
- latest assurance receipt and run history;
- interactive H1→H7 evidence spine;
- H6 runtime, reliability, token/cost coverage, freshness and findings;
- loading, empty, error, legacy and partial-telemetry states;
- self-contained HTML and machine-auditable JSON export on demand.
Platform remains the centralized operations layer: multi-project fleet views,
organization RBAC, shared approval queues, remote ingestion, governed settings
and managed retention.
## Lifecycle
```text
Prompt finishes
└─ hook writes canonical trace/events/H6 + latest-run receipt
└─ no server, no HTML export, no second model call
Developer runs `casan view`
└─ Core starts or reuses a loopback viewer
├─ reads bounded evidence projections
├─ opens latest/specified trace
└─ generates HTML/JSON only when Export is selected
```
Do not run `casan report export` after every prompt. Export is an independent
review artifact, not the evidence source of truth.
## Commands
```bash
casan report latest
casan readiness --refresh
casan view [trace-id] [--no-open]
casan report export [trace-id] --format html|json [-o path]
casan report export --h6 --format html|json [-o path]
casan dashboard start [--port 0]
casan dashboard open
casan dashboard status
casan dashboard stop
```
When Platform code is installed, `casan dashboard` manages the centralized
Control Plane. `casan view` remains the predictable Core single-project viewer.
An empty project must not show operational zeroes or a fabricated READY
verdict. Before the first governed prompt, the overview shows installation
readiness and actionable onboarding only. Operational KPIs appear after
canonical run evidence exists.
## Runtime and packaging contract
- Python standard library only; no Node/npm or network dependency.
- Static HTML/CSS/JS ships inside `casan-core`.
- System fonts only; no CDN, analytics or external asset requests.
- Managed and vendored Core use the same runtime allowlist.
- Runtime tests and source-only tooling do not cross the release boundary.
- Evidence remains under the project's `.specify/` tree.
## Security model
- binds only to `127.0.0.1` on an ephemeral port by default;
- rejects non-loopback clients and non-loopback `Host` headers;
- requires a high-entropy session token for every API/export request;
- stores viewer state as `0600` on POSIX and under the project ACL on Windows;
- exposes GET-only evidence APIs; mutation verbs return `405`;
- sends CSP, `no-store`, `nosniff`, frame denial and no-referrer headers;
- validates trace/project filters and never resolves a request path as a file;
- bounds source reads, record counts and response size;
- rotates the non-evidence request log at 1 MiB and never logs the session token;
- redacts prompt bodies, credentials, tool input/output and authorization data;
- keeps missing token/cost values `null`, never fabricated as zero.
The UI shell itself is non-sensitive and may load without a token. All project
metadata, evidence and exports require the session token.
## Presentation and disclosure policy
Reports use two information layers:
1. the executive layer shows verdict, governed volume, reliability, latency,
evidence quality, decision findings and measured distributions;
2. the technical layer preserves filters, provider counters, canonical paths,
event history, sanitized manifests and raw breakdown tables behind native
disclosure controls.
The technical layer is collapsed by default in interactive viewers. It remains
available for investigation and is included in print-ready export appendices.
This is progressive disclosure, not evidence deletion.
Every visual must be derived from the report contract. Missing token, cost,
freshness or provenance values remain unavailable; the UI must not invent a
zero, trend, governance verdict or maturity claim. Charts include visible
labels and values so meaning does not depend on color alone.
## Production acceptance gate
The Core viewer is releasable only when all of these pass:
1. unit contracts for empty, legacy, partial, certified and unsafe-input states;
2. clean-project test from the built `casan-core` tarball;
3. no `packages/casan-control-panel`, `package.json` or Node dependency in Core;
4. unauthorized API, mutation and Host-header checks fail closed;
5. run/H6 HTML and JSON export work offline;
6. browser validation covers the report hierarchy, interaction and responsive
navigation;
7. existing harness, DevKit installation and packaging suites remain green.
8. a clean `casan init --edition core` project reports Core ready without
requiring a Domain Pack, provider token/cost telemetry, or app-source edits.
## Maturity statement
This viewer improves evidence usability; it does not grant CASAN Maturity L4.
Maturity is assessed from real operational evidence, ownership, controls and
repeatability. The interface must show the recorded maturity status and must
not infer a level from the installed edition.
+12 -2
View File
@@ -1,7 +1,8 @@
# CASAN Domain Pack Guide
A **Domain Pack** is the per-project data the harness needs to govern YOUR domain. It lives
at `apps/<project>/domain/` and is selected via `CASAN_DOMAIN_ROOT`. The harness code
A **Domain Pack** is optional per-project data the harness needs for a
project-specific SRS→test pipeline. Core prompt assurance and visual reporting
do not require it. A pack commonly lives at `apps/<project>/domain/`. The harness code
(`packages/casan-harness/`) never contains domain data — this is what makes it reusable.
Scaffold template: `packages/casan-devkit/templates/domain-pack/`.
@@ -39,9 +40,18 @@ apps/<project>/domain/
## Wire it up
```bash
casan domain discover
casan domain configure apps/<project>/domain/project.manifest.json
casan domain status
# Existing automation remains supported
export CASAN_PROJECT_MANIFEST=apps/<project>/domain/project.manifest.json
bin/casan gate # runs domain-dependent suites against your pack
```
`casan domain configure` writes only `.casan/domain.json`; it references an
existing in-project manifest and never copies, rewrites or invents application
requirements, source mappings or tests.
`domain_root` is also recorded per-project in `packages/casan-harness/config/project-registry.json`
so `bin/casan reuse` can prove multi-project reuse.
@@ -15,9 +15,11 @@ CASAN Maturity L1–L5 describes evidence-backed operational adoption.
| Gitea CI gate template | — | Included | Included | — |
| Control Panel: runs, governance, security, cost, approvals | — | — | Included | Not a shipped edition |
| Prompt assurance receipt + latest-run discovery | Included | Included | Included | Building blocks only |
| Clickable trace deep link when enrolled | Included | Included | Included | Building blocks only |
| One-command local Control Plane launcher | — | — | Included | Not a shipped edition |
| H1–H7 live assurance rail + per-run HTML/JSON export | — | — | Included | Not a shipped edition |
| Core Local Assurance Viewer (single project, offline, read-only) | Included | Included | Included | Building blocks only |
| H1–H7 assurance rail + per-run HTML/JSON export | Included | Included | Included | Building blocks only |
| H6 AgentOps dossier + history + filters + HTML/JSON export | Included | Included | Included | Building blocks only |
| Clickable local trace deep link; central link when enrolled | Included | Included | Included | Building blocks only |
| One-command centralized Control Plane launcher | — | — | Included | Not a shipped edition |
| Async HMAC telemetry delivery with durable local spool | Included | Included | Included | Building blocks only |
| Evidence Pack Viewer | — | — | Included | Not a shipped edition |
| Governed chat/operator/codegen MVP | — | — | Included, preview | Not a shipped edition |
@@ -31,11 +33,13 @@ CASAN Maturity L1–L5 describes evidence-backed operational adoption.
## Edition rules
- **Core** is the first paid-PoC product: use it with the customer’s existing
AI coding tools and CI. It is self-hosted and evidence-oriented.
AI coding tools and CI. It includes a production-quality single-project
Local Assurance Viewer without Node/npm or a Platform dependency.
- **DevKit** adds repeatable adoption material. It does not add a managed
service or a security certification.
- **Platform Preview** includes the Control Panel and the Evidence Pack Viewer,
but is not production enterprise software. The bundle has a
- **Platform Preview** includes the Control Panel and Evidence Pack Viewer,
plus centralized multi-project operations, RBAC and approvals. It is not
production enterprise software. The bundle has a
`PREVIEW-INCOMPLETE.txt` marker by design.
- **Enterprise** remains `future` and `package-release.sh enterprise` refuses
to generate an artifact. Do not advertise it as a purchasable edition.
+1 -1
View File
@@ -57,7 +57,7 @@
| **04 Self-improve** | � core done+test | `packages/casan-harness/scripts/bash/self-improve.py`: `propose` đọc metrics/drift → proposal dry-run (không ghi); `apply` bắt buộc approval, áp qua governed store (audit); sensitive/loosen luôn cần duyệt. `phase-selfimprove-tests.sh` 7/0 (WSL), nối CI. Còn: luật đề xuất phong phú hơn (corpus/model escalation), chạy định kỳ CI (05). |
| **05 CI/CD** | 🟡 CI gate MVP done | `packages/casan-harness/scripts/bash/ci-harness-gate.sh` chạy các suite harness/hardening/sourcegen/traceability/frontend theo thứ tự an toàn, có timeout/filter; `.gitea/workflows/harness-ci.yml` gọi gate trên push/PR. Filtered local verify PASS=2/0. Còn: full gate xanh trên runner thật, xử lý A6 nếu còn chậm/treo, bật Docker infra lab nếu runner hỗ trợ, package/release artifact `fpt-casan-sdd-harness`. |
| **06 Onboard dự án 2** | 📋 chưa bắt đầu (🔓 **đã mở khoá** — 01 done) | Chứng minh reuse: cắm 1 repo khác + golden/corpus/input, đăng ký qua `verify-harness-reuse.sh`, không sửa gate. Phụ thuộc 01 ✅. App mới chỉ cần `apps/<project>/domain/` + set `CASAN_DOMAIN_ROOT` (đã có `domain_root` per-project trong `project-registry.json`). |
| **08 Context compression** | � MVP done+test | **CASAN-native token-killer** (Track 3) đã có: `context-compress.py` (dedup/extractive/structural, must-keep, tee, gate fail-able), `phase08-compression-tests.sh` 7/0 (WSL), nối CI. Còn: Track 1 nén INPUT + Track 2 nén VIEW liên-bước + Track 4 abstractive (gated) + nối H4/H5 trong pipeline thật. |
| **08 Context compression** | 🟡 Track 3 hardened+test | **CASAN-native token-killer** đã có: severity `[ERR]/[WRN]`, multiplicity-preserving must-keep gate, raw fallback/halt fail-safe, hash-bound JSON evidence, estimate/provider labeling; `phase08-compression-tests.sh` 19/0, SEC-09 7/0, loop consumer 16/0. Log thật NEHOPS giữ 4/4 warning; 1.620→76 chỉ là whitespace estimate, chưa phải ROI claim. Còn: Track 1 INPUT + Track 2 VIEW + Track 4 abstractive + H4/H5 pipeline binding + A/B provider telemetry. |
| **12 Domain Pack SDK** | 📋 chưa bắt đầu (🔓 01 done, còn chờ 06) | Onboard bằng khai báo (golden/corpus/policy theo domain). Phụ thuộc 01 ✅, 06. |
| **01 Restructure** | ✅ **done+test (2026-07-08, merged main)** | Đã tách: harness code → `packages/casan-harness/`, domain OKR → `apps/okr/domain/`, runtime state ở `.specify/`; **app promote lên git root** (hết wrapper `AINative_OKR_CASAN5/`); facade `.specify` symlink **gỡ sạch** (hard cutoff); path resolve qua `casan-paths.sh` (marker walk-up). Full gate **PASS=64 FAIL=0 SKIP=3** từ cấu trúc mới. CI (`.gitea/workflows/ci.yml`) + docs đã đồng bộ. Mở khoá 06/12. Chi tiết: `CASAN_PLAN_01_RESTRUCTURE.md`. |
| **13 Control Plane** | 🟡 core + **Track 1/2/3 + FinOps/SLO + Command Center + local-prod TLS/OIDC smoke** done+test | **Sửa kiến trúc: là tài sản harness, KHÔNG nằm trong OKR.** Governance core đã dời vào harness: `packages/casan-harness/scripts/bash/control-plane-settings.py` (settings versioned + audit hash-chain + deny-by-default + approval + rollback), `phase-control-plane-tests.sh` 9/0 + `phase-control-plane-hitl-tests.sh` 9/0 (nối CI). Đã gỡ khỏi `apps/okr` (OKR sạch: 46/0/3skip + 16/16). **Track 1 DONE**: Ops Console read-only telemetry. **Track 2 DONE**: settings API/UI tại `packages/casan-control-panel/` bọc `control-plane-settings.py` + `rbac-check.py`. **Track 3 DONE**: kill-switch API/UI; approval inbox/delegation/oversight API/UI bọc harness `approval-inbox.py` + `delegation-policy.yaml`, SoD, apply setting proposal qua governed store; IdP group claim→RBAC role mapping tested. **Track 4 local-prod DONE**: FinOps/SLO page + Docker/nginx/oauth2-proxy deploy scaffold; `local-prod-smoke.sh` passes `CP_LOCAL_SMOKE_PASS https_oidc=true actor=oidc-ops role=org-admin` and invokes authenticated `managed-prod-smoke.sh` on the local-prod endpoint (`CP_MANAGED_SMOKE_PASS actor=oidc-ops role=org-admin widgets=9`); `prod-readiness-check.sh` validates managed-prod TLS/OIDC/nginx prerequisites. **Command Center §8.6 DONE**: `GET /api/v1/command` + `/command` UI, 9 evidence-backed widgets (gồm `chat_loop`), provenance envelope, executive briefing VI/EN, live ticker, evidence drawer. `npm run console:test` **26/0**, `npm run console:build` xanh. Targeted related suites: RBAC 12/0, C7 15/0. Còn: live managed prod deploy với cert/enterprise IdP/host thật (07 T2); RAI view thuộc Plan-15 follow-up. |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,13 +2,19 @@
> Năng lực **nén prompt/context để giảm token · latency · cost** — nhưng **không phá governance**.
>
> Status 2026-07-06: **🟡 Track 3 MVP implemented + tested (CASAN-native token-killer).**
> `packages/casan-harness/scripts/bash/context-compress.py` — compressor deterministic của CASAN
> (KHÔNG dùng lại RTK): modes `dedup`/`extractive`/`structural`, must-keep luôn giữ,
> tee raw-passthrough khi lệnh fail, báo `token_saved`/`ratio`, và gate must-keep
> fail-able. Test `packages/casan-harness/tests/phase08-compression-tests.sh` **7/0 (WSL)**; đã nối
> vào `ci-harness-gate.sh` (`phase08-compression`). Còn: Track 1 nén INPUT + Track 2
> nén VIEW liên-bước + Track 4 abstractive (gated) + nối H4/H5 scan/hash trong pipeline thật.
> Status 2026-07-29: **🟡 Track 3 hardened + regression-tested (CASAN-native token-killer).**
> `packages/casan-harness/scripts/bash/context-compress.py` — compressor deterministic của CASAN
> (KHÔNG dùng lại RTK): modes `dedup`/`extractive`/`structural`; nhận diện severity
> `[ERR]/[WRN]`; giữ multiplicity của warning/must-keep; invalid regex/input rỗng sau nén
> đều fail-closed; preservation fail mặc định trả RAW + exit 1; có lựa chọn `halt`; JSON
> evidence bind SHA-256 raw/candidate/output và ghi rõ `whitespace_estimate` khác provider
> telemetry. Test `packages/casan-harness/tests/phase08-compression-tests.sh` **19/0 (macOS)**;
> SEC-09 **7/0**; loop consumer **16/0**; đã nối `ci-harness-gate.sh`
> (`phase08-compression`). Log thật NEHOPS `analyzer-20260507.log` đo **1.620 → 76
> whitespace-estimate units**, giữ đủ **4/4 `[WRN]`**, preservation pass; con số này vẫn
> **không phải provider token saving claim** cho đến khi qua H3 end-to-end/A-B. Còn:
> Track 1 nén INPUT + Track 2 nén VIEW liên-bước + Track 4 abstractive (gated) + nối
> H4 scan raw/compressed và H5 audit binding trong pipeline thật.
>
> Nhãn: [có] tồn tại thật · [đo] đã kiểm chứng · [mới] cần làm · [chưa tự động] có đo/người quyết.
> Phụ thuộc: **01** (đường dẫn sau restructure) · **07** (thứ tự scan/audit an toàn, V19/V5) · **03** (chế độ abstractive dùng model).
+23 -13
View File
@@ -10,20 +10,26 @@ This document defines the boundary between the local workstation experience and
flowchart LR
B["Browser"] -->|TLS + OIDC cookie| N["Nginx"]
N -->|auth_request| O["oauth2-proxy"]
N -->|overwritten identity headers| A["Control Panel API"]
N -->|signed bearer token; identity headers removed| A["Control Panel API"]
A -->|verify RS256 + issuer + audience + time claims| I["VerifiedClaims"]
A -->|tenant-scoped encrypted store| S["CASAN state"]
A -->|random bridge token, local only| H["Mac host bridge"]
H -->|official CLI auth| P["Codex / Claude"]
```
The API must not be published directly. Nginx is the only ingress and overwrites `X-CASAN-Actor`, `X-CASAN-Groups`, and `X-CASAN-Role`. The API refuses a non-loopback strict bind unless `CASAN_CP_TRUST_AUTH_PROXY=1` is explicit.
The API must not be published directly. Nginx is the only ingress and removes
`X-CASAN-*` and forwarded identity headers. The API refuses every production or
non-loopback bind unless `CASAN_CP_AUTH_MODE=jwt` has a valid cryptographic
verifier. Proxy headers alone are never authenticated identity.
## Implemented controls
- TLS at Nginx; production accepts only TLS 1.2/1.3.
- OIDC authentication through oauth2-proxy.
- Secure, HttpOnly, SameSite=Lax session cookies with bounded expiry/refresh.
- Browser-supplied identity headers are overwritten at Nginx.
- Browser-supplied identity headers are removed at Nginx.
- The API verifies RS256 signature, issuer, audience, subject, expiry,
not-before/issued-at bounds and clock skew before deriving request identity.
- CSP, frame denial, MIME sniffing protection, referrer and browser permission restrictions.
- API request body capped at 1 MiB.
- SSE buffering disabled for trace streams; no intermediate proxy cache.
@@ -53,21 +59,26 @@ Before production deployment:
2. Do not deploy `provider-auth-bridge.py`.
3. Use managed OpenAI/Anthropic credentials from Vault/KMS or workload identity.
4. Use an enterprise IdP and explicit group-to-role mapping.
5. Use a CA-issued certificate and a fixed production hostname.
6. Set a digest-pinned `CASAN_CP_API_IMAGE`, `CASAN_CP_UI_IMAGE`, and `CASAN_CP_OAUTH2_PROXY_IMAGE`.
7. Keep the API on a private container/network segment with Nginx as its only caller.
8. Restrict egress from the API to allowlisted model providers, Vault/KMS, object storage and required observability endpoints.
9. Store state on encrypted storage; send audit heads to KMS/WORM/Object Lock.
10. Export rate-limit, auth failure, bridge-disabled and audit-chain metrics to alerting.
11. Back up and restore-test tenant state before enabling write actions.
12. Run the production preflight and security suites against the exact deployment images.
5. Mount the IdP RS256 public key and configure exact issuer/audience; coordinate
key rotation with an API restart until JWKS rollover is implemented.
6. Use a CA-issued certificate and a fixed production hostname.
7. Set a digest-pinned `CASAN_CP_API_IMAGE`, `CASAN_CP_UI_IMAGE`, and `CASAN_CP_OAUTH2_PROXY_IMAGE`.
8. Keep the API on a private container/network segment with Nginx as its only caller.
9. Restrict egress from the API to allowlisted model providers, Vault/KMS, object storage and required observability endpoints.
10. Store state on encrypted storage; send audit heads to KMS/WORM/Object Lock.
11. Export rate-limit, auth failure, bridge-disabled and audit-chain metrics to alerting.
12. Back up and restore-test tenant state before enabling write actions.
13. Run the production preflight and security suites against the exact deployment images.
## Known residual risks
- Local bridge authorization is bearer-token based; a process with access to the token file can call it.
- In-memory rate windows reset when the API or bridge restarts. Production should also rate-limit at ingress.
- Goal output is stored in tenant-scoped local state but is not currently envelope-encrypted as a whole.
- A compromised container on the private Control Panel network may attempt to forge proxy headers. Production network policy must keep unrelated workloads off that network.
- Static public-key rotation requires a coordinated file replacement and API
restart; automated JWKS rollover is not implemented.
- A compromised ingress container can interfere with availability or bearer
forwarding, but cannot mint a valid IdP signature without the IdP key.
- Developer account subscriptions have provider-specific quotas and are not an availability SLA.
- The local mock IdP and self-signed TLS do not prove enterprise SSO readiness.
@@ -83,4 +94,3 @@ packages/casan-harness/scripts/bash/local-full.sh start
This terminates the bridge, deletes the old token and creates a new one. Then inspect `tmp/control-panel-local/auth-bridge/model-audit.jsonl` for unexpected provider/status/hash activity. The log contains no raw prompts.
If a goal audit chain is suspected of tampering, stop new goal submissions, preserve `.specify/logs/audit/goal-orchestrator.jsonl` and its head, and compare each `prev_hash`/`record_hash` before restoring service.
+7 -1
View File
@@ -10,6 +10,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlencode, urlparse
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
@@ -33,7 +34,12 @@ USERS = {
},
}
KID = "casan-local-prod-idp"
KEY = rsa.generate_private_key(public_exponent=65537, key_size=2048)
KEY_FILE = os.environ.get("CASAN_IDP_PRIVATE_KEY_FILE", "")
if KEY_FILE:
with open(KEY_FILE, "rb") as key_handle:
KEY = serialization.load_pem_private_key(key_handle.read(), password=None)
else:
KEY = rsa.generate_private_key(public_exponent=65537, key_size=2048)
CODES = {}
APPROVAL_SIGNER_TOKEN = os.environ.get("CASAN_APPROVAL_SIGNER_TOKEN", "")
+12 -3
View File
@@ -12,8 +12,10 @@ not be invented or committed.
it at `/opt/casan-control-panel/tls/tls.crt` and `tls.key` (mode `0600`).
2. Register `https://<fqdn>/oauth2/callback` with the enterprise IdP. Map the
`groups` claim to CASAN groups such as `casan-org-admin` and
`casan-approver`. Copy `oauth2-proxy.env.example` outside the repo and fill
it using the secret manager.
`casan-approver`. Export the IdP's RS256 verification public key to
`/opt/casan-control-panel/idp-public.pem`; rotation requires replacing this
file and restarting the API. Copy `oauth2-proxy.env.example` outside the
repo and fill it using the secret manager.
3. Create a Vault Transit key `casan-audit-key` with `exportable=false`; give a
workload identity only `transit/sign/casan-audit-key` and read-key metadata.
Render a short-lived token into `vault.env` outside Git. Never use Vault dev
@@ -35,6 +37,7 @@ sudo install -d -m 0700 /opt/casan-control-panel/tls /srv/casan/state /srv/casan
sudo install -m 0600 /dev/null /opt/casan-control-panel/oauth2-proxy.env
sudo install -m 0600 /dev/null /opt/casan-control-panel/runtime.env
sudo install -m 0600 /dev/null /opt/casan-control-panel/vault.env
sudo install -m 0644 /secure/export/idp-public.pem /opt/casan-control-panel/idp-public.pem
bash packages/casan-harness/scripts/bash/production-preflight.sh infra/production/casan-prod.env
set -a; source infra/production/casan-prod.env; set +a
@@ -43,9 +46,15 @@ docker compose -f docker-compose.control-panel.yml up -d
The preflight performs a real Object Lock anchor write. It will reject local
URLs, image tags, expired/near-expiry TLS, dev/root Vault tokens, incomplete
OIDC, missing paths, and buckets without Object Lock. After login, run
OIDC, issuer/audience mismatches, missing IdP keys, local trust providers,
missing paths, and buckets without Object Lock. After login, run
`managed-prod-smoke.sh` with an authenticated cookie jar.
The API does not trust `X-CASAN-*` or forwarded-user headers from Nginx.
oauth2-proxy returns the signed access token, Nginx removes caller-supplied
identity headers, and the API verifies RS256 signature, issuer, audience,
expiry and clock bounds before deriving actor, role, tenant and project.
## Operational anchors
Run `audit-ship-s3.sh` after every audit seal from the same workload identity.
+2
View File
@@ -9,10 +9,12 @@ CASAN_CP_RUNTIME_ENV=/opt/casan-control-panel/runtime.env
CASAN_CP_VAULT_ENV=/opt/casan-control-panel/vault.env
CASAN_CP_STATE_DIR=/srv/casan/state
CASAN_CP_OUTPUT_DIR=/srv/casan/output
CASAN_CP_IDP_PUBLIC_KEY=/opt/casan-control-panel/idp-public.pem
# CI must publish immutable image references, not tags such as :latest.
CASAN_CP_API_IMAGE=registry.example.internal/casan/control-panel-api@sha256:replace-with-64-hex-digest
CASAN_CP_UI_IMAGE=registry.example.internal/casan/control-panel-ui@sha256:replace-with-64-hex-digest
CASAN_CP_OAUTH2_PROXY_IMAGE=quay.io/oauth2-proxy/oauth2-proxy@sha256:replace-with-64-hex-digest
# WORM anchor destination. The deploy identity needs only PutObject and
# GetObjectLockConfiguration for this bucket/prefix; use workload identity,
+2 -2
View File
@@ -8,6 +8,6 @@ OAUTH2_PROXY_COOKIE_SECURE=true
OAUTH2_PROXY_REDIRECT_URL=https://control.casan.company.internal/oauth2/callback
OAUTH2_PROXY_OIDC_GROUPS_CLAIM=groups
OAUTH2_PROXY_SET_XAUTHREQUEST=true
OAUTH2_PROXY_PASS_ACCESS_TOKEN=false
OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER=false
OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER=true
OAUTH2_PROXY_SCOPE="openid profile email groups"
+10 -1
View File
@@ -2,10 +2,19 @@
# companion Vault file and the production preflight validation.
CASAN_PROFILE=prod
CASAN_CP_STRICT=1
CASAN_CP_TRUST_AUTH_PROXY=1
CASAN_CP_AUTH_MODE=jwt
CASAN_CP_JWT_ISSUER=https://id.example.internal/realms/casan
CASAN_CP_JWT_AUDIENCE=casan-control-plane
CASAN_CP_JWT_PUBLIC_KEY_FILE=/run/casan-idp/idp-public.pem
CASAN_CP_JWT_ROLE_CLAIM=groups
CASAN_CP_JWT_TENANT_CLAIM=casan_tenant
CASAN_CP_JWT_PROJECT_CLAIM=casan_project
CASAN_CP_JWT_CLOCK_SKEW_SECONDS=60
CP_BIND=0.0.0.0
CP_PORT=3010
CASAN_APP_ROOT=/app
CASAN_DASHBOARD_STALE_S=300
CASAN_PREFLIGHT=1
CASAN_CHAT_MODEL_MODE=deterministic
CASAN_SIGNING_PROVIDER=vault_kms
CASAN_IMMUTABLE_ANCHOR_PROVIDER=s3_object_lock
+12 -4
View File
@@ -43,6 +43,7 @@ server {
auth_request /oauth2/auth;
auth_request_set $auth_user $upstream_http_x_auth_request_user;
auth_request_set $auth_groups $upstream_http_x_auth_request_groups;
auth_request_set $auth_access_token $upstream_http_x_auth_request_access_token;
proxy_pass http://control-panel-api:3010/api/v1/;
proxy_http_version 1.1;
@@ -55,16 +56,23 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# Overwrite browser-supplied identity headers. The API maps these IdP
# group claims through rbac-check.py map-claim.
proxy_set_header X-CASAN-Actor $auth_user;
proxy_set_header X-CASAN-Groups $auth_groups;
# Remove every caller-supplied identity assertion. The API derives
# actor/role/scope only after cryptographic JWT verification.
proxy_set_header X-CASAN-Actor "";
proxy_set_header X-CASAN-Groups "";
proxy_set_header X-CASAN-Role "";
proxy_set_header X-CASAN-Project "";
proxy_set_header X-CASAN-Tenant "";
proxy_set_header X-Forwarded-User "";
proxy_set_header X-Forwarded-Groups "";
proxy_set_header Authorization "Bearer $auth_access_token";
}
location = /healthz {
auth_request /oauth2/auth;
auth_request_set $auth_access_token $upstream_http_x_auth_request_access_token;
proxy_pass http://control-panel-api:3010/healthz;
proxy_set_header Authorization "Bearer $auth_access_token";
}
location / {
+19 -10
View File
@@ -101,8 +101,10 @@ Goal workspace context:
a different actor. The executor verifies the artifact hash, applies it, runs fixed
project build/test commands, and reverses the patch if verification fails.
Local management headers: `x-casan-actor`, `x-casan-role`, `x-casan-project`,
`x-casan-tenant`. Missing role defaults to `viewer`, so writes fail closed.
In explicit loopback development mode only, local management headers are
`x-casan-actor`, `x-casan-role`, `x-casan-project`, and `x-casan-tenant`.
Missing role defaults to `viewer`, so writes fail closed. JWT mode discards
these caller assertions and derives them only from verified token claims.
Kill-switch management:
@@ -169,12 +171,19 @@ App root + telemetry paths resolve via the same marker walk-up as `casan-paths.s
compatibility aliases. Freshness is calculated independently from each file's mtime using
`CASAN_DASHBOARD_STALE_S` (default `3600`).
## Security posture (MVP)
Binds `127.0.0.1` by default. Refuses a non-loopback bind under `CASAN_PROFILE=prod` /
`CASAN_CP_STRICT=1` unless `CASAN_CP_TRUST_AUTH_PROXY=1` is set for an authenticated reverse
proxy that overwrites identity headers. Management endpoints are RBAC-gated via the harness
`rbac-check.py`; IdP group claims such as `casan-approver` are mapped to RBAC roles through
the same harness engine.
## Security posture
Binds `127.0.0.1` in explicit local development mode. Every non-loopback bind,
and every production profile, requires `CASAN_CP_AUTH_MODE=jwt`. Production
requires an RS256 public key plus configured issuer and audience; missing or
invalid configuration refuses startup. The API verifies signature, expiry,
issuer, audience, `nbf`/`iat`, and bounded clock skew in-process, then maps
verified group claims through `rbac-check.py`. Arbitrary `X-CASAN-*` and
forwarded-user headers are not an authentication mechanism.
Production variables are documented in `infra/production/runtime.env.example`.
The packaged boundary is oauth2-proxy → Nginx header stripping/bearer forwarding
→ API cryptographic verification. The current provider uses a mounted RS256
public key; automated JWKS discovery/rotation remains future work.
## Test
```bash
@@ -189,8 +198,8 @@ bash packages/casan-control-panel/scripts/local-prod-smoke.sh
The scaffold includes `Dockerfile.control-panel-api`, `Dockerfile.control-panel-ui`, and
`nginx/control-panel.conf`. Nginx protects UI/API through oauth2-proxy `auth_request`,
overwrites browser-supplied `X-CASAN-*` headers, and passes IdP group claims to the API for
RBAC mapping. The local smoke starts a self-signed HTTPS stack with a mock OIDC IdP and
removes browser-supplied identity headers, and forwards the signed bearer token for API
verification. The local smoke starts a self-signed HTTPS stack with a mock OIDC IdP and
expects `CP_LOCAL_SMOKE_PASS https_oidc=true actor=oidc-ops role=org-admin`; it also
asserts the Command Center returns all nine widgets with provenance envelopes and invokes
`managed-prod-smoke.sh` with the authenticated mock-IdP cookie jar. A passing local run
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { APP_ROOT } from './app-root.js';
import type { SettingsActor } from '../settings/settings.service.js';
import { UnauthorizedException } from '@nestjs/common';
const RBAC_CLI = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash', 'rbac-check.py');
const LOCAL_ROLES = new Set(['org-admin', 'project-admin', 'approver', 'operator', 'viewer', 'auditor']);
@@ -23,7 +24,7 @@ function mapClaim(claim: string): string | null {
}
}
function roleFromClaim(raw: string | undefined): string {
export function roleFromClaim(raw: string | undefined): string {
if (!raw) return 'viewer';
const claims = raw.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
for (const claim of claims) {
@@ -33,6 +34,23 @@ function roleFromClaim(raw: string | undefined): string {
return 'viewer';
}
export interface VerifiedClaimInput {
subject: string;
issuer: string;
roles: string[];
project: string;
tenant: string;
}
export function actorFromVerifiedClaims(claims: VerifiedClaimInput): SettingsActor {
return {
actor: claims.subject,
role: roleFromClaim(claims.roles.join(',')),
project: /^[A-Za-z0-9._-]+$/.test(claims.project) ? claims.project : 'default',
tenant: /^[A-Za-z0-9._-]+$/.test(claims.tenant) ? claims.tenant : 'default',
};
}
function projectFromClaims(raw: string | undefined): string | undefined {
if (!raw) return undefined;
const prefix = 'casan-project:';
@@ -42,6 +60,9 @@ function projectFromClaims(raw: string | undefined): string | undefined {
}
export function actorFromHeaders(headers: Record<string, string | string[] | undefined>): SettingsActor {
if (process.env.CASAN_CP_AUTH_MODE === 'jwt' && firstHeader(headers['x-casan-identity-verified']) !== '1') {
throw new UnauthorizedException('AUTH_VERIFIED_IDENTITY_REQUIRED');
}
const actor = firstHeader(headers['x-casan-actor'])
|| firstHeader(headers['x-auth-request-user'])
|| firstHeader(headers['x-forwarded-user'])
@@ -0,0 +1,270 @@
import { createHash, createHmac, createPublicKey, createVerify, timingSafeEqual } from 'node:crypto';
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from 'node:fs';
import { dirname, join } from 'node:path';
import type { NextFunction, Request, Response } from 'express';
import { APP_ROOT } from './app-root.js';
import { actorFromVerifiedClaims, type VerifiedClaimInput } from './auth-context.js';
export type AuthMode = 'local' | 'jwt';
export type JwtAlgorithm = 'RS256' | 'HS256';
export interface ControlPlaneAuthConfig {
mode: AuthMode;
profile: string;
bind: string;
issuer?: string;
audience?: string;
algorithm?: JwtAlgorithm;
publicKey?: string;
hmacSecret?: string;
clockSkewSeconds: number;
roleClaim: string;
tenantClaim: string;
projectClaim: string;
}
export interface VerifiedClaims {
subject: string;
issuer: string;
audience: string[];
expiresAt: number;
issuedAt?: number;
roles: string[];
tenant: string;
project: string;
authenticationMethod: 'jwt';
}
export interface AuthenticationDecision {
allowed: boolean;
reasonCode: string;
claims?: VerifiedClaims;
}
export interface AuthProvider {
authenticate(headers: Record<string, string | string[] | undefined>, nowSeconds?: number): AuthenticationDecision;
}
interface JwtHeader {
alg?: string;
typ?: string;
}
type JwtPayload = Record<string, unknown>;
const IDENTITY_HEADERS = [
'x-casan-actor', 'x-casan-role', 'x-casan-groups', 'x-casan-project', 'x-casan-tenant',
'x-auth-request-user', 'x-auth-request-groups', 'x-forwarded-user', 'x-forwarded-groups',
'x-casan-identity-verified', 'x-casan-identity-issuer',
];
function firstHeader(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}
function decodeSegment<T>(segment: string): T {
const decoded = Buffer.from(segment, 'base64url').toString('utf8');
const payload: unknown = JSON.parse(decoded);
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('jwt_segment_not_object');
return payload as T;
}
function stringClaim(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
function stringListClaim(value: unknown): string[] {
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
if (typeof value === 'string') return value.split(/[\s,]+/).filter(Boolean);
return [];
}
function audienceClaim(value: unknown): string[] {
return typeof value === 'string' ? [value] : stringListClaim(value);
}
function numericClaim(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function safeScope(value: string | undefined, fallback: string): string {
return value && /^[A-Za-z0-9._-]+$/.test(value) ? value : fallback;
}
function verifySignature(input: string, signature: Buffer, config: ControlPlaneAuthConfig): boolean {
if (config.algorithm === 'RS256' && config.publicKey) {
const verifier = createVerify('RSA-SHA256');
verifier.update(input);
verifier.end();
return verifier.verify(config.publicKey, signature);
}
if (config.algorithm === 'HS256' && config.hmacSecret) {
const expected = createHmac('sha256', config.hmacSecret).update(input).digest();
return expected.length === signature.length && timingSafeEqual(expected, signature);
}
return false;
}
export class JwtAuthProvider implements AuthProvider {
constructor(private readonly config: ControlPlaneAuthConfig) {}
authenticate(headers: Record<string, string | string[] | undefined>, nowSeconds = Math.floor(Date.now() / 1000)): AuthenticationDecision {
const authorization = firstHeader(headers.authorization);
if (!authorization?.startsWith('Bearer ')) return { allowed: false, reasonCode: 'auth_bearer_token_required' };
const token = authorization.slice('Bearer '.length).trim();
const parts = token.split('.');
if (parts.length !== 3 || parts.some((part) => !part)) return { allowed: false, reasonCode: 'auth_token_malformed' };
try {
const header = decodeSegment<JwtHeader>(parts[0]);
const payload = decodeSegment<JwtPayload>(parts[1]);
if (header.alg !== this.config.algorithm) return { allowed: false, reasonCode: 'auth_algorithm_mismatch' };
if (!verifySignature(`${parts[0]}.${parts[1]}`, Buffer.from(parts[2], 'base64url'), this.config)) {
return { allowed: false, reasonCode: 'auth_signature_invalid' };
}
const issuer = stringClaim(payload.iss);
const audience = audienceClaim(payload.aud);
const subject = stringClaim(payload.sub);
const expiresAt = numericClaim(payload.exp);
const notBefore = numericClaim(payload.nbf);
const issuedAt = numericClaim(payload.iat);
const skew = this.config.clockSkewSeconds;
if (!issuer || issuer !== this.config.issuer) return { allowed: false, reasonCode: 'auth_issuer_invalid' };
if (!this.config.audience || !audience.includes(this.config.audience)) return { allowed: false, reasonCode: 'auth_audience_invalid' };
if (!subject) return { allowed: false, reasonCode: 'auth_subject_required' };
if (!expiresAt || nowSeconds - skew >= expiresAt) return { allowed: false, reasonCode: 'auth_token_expired' };
if (notBefore !== undefined && nowSeconds + skew < notBefore) return { allowed: false, reasonCode: 'auth_token_not_yet_valid' };
if (issuedAt !== undefined && issuedAt > nowSeconds + skew) return { allowed: false, reasonCode: 'auth_issued_at_invalid' };
const roles = stringListClaim(payload[this.config.roleClaim]);
const rawTenant = stringClaim(payload[this.config.tenantClaim]);
const rawProject = stringClaim(payload[this.config.projectClaim]);
if ((rawTenant && safeScope(rawTenant, '') === '') || (rawProject && safeScope(rawProject, '') === '')) {
return { allowed: false, reasonCode: 'auth_scope_invalid' };
}
const tenant = safeScope(rawTenant, 'default');
const project = safeScope(rawProject, 'default');
return {
allowed: true,
reasonCode: 'auth_verified',
claims: { subject, issuer, audience, expiresAt, issuedAt, roles, tenant, project, authenticationMethod: 'jwt' },
};
} catch {
return { allowed: false, reasonCode: 'auth_token_malformed' };
}
}
}
export function loadAuthConfig(env: NodeJS.ProcessEnv = process.env): ControlPlaneAuthConfig {
const profile = env.CASAN_CP_STRICT === '1' ? 'strict' : (env.CASAN_PROFILE || 'development').toLowerCase();
const mode = (env.CASAN_CP_AUTH_MODE || 'local').toLowerCase() as AuthMode;
const bind = env.CP_BIND || '127.0.0.1';
const publicKeyPath = env.CASAN_CP_JWT_PUBLIC_KEY_FILE;
const publicKey = publicKeyPath && existsSync(publicKeyPath) ? readFileSync(publicKeyPath, 'utf8') : undefined;
const skew = Number(env.CASAN_CP_JWT_CLOCK_SKEW_SECONDS ?? 60);
return {
mode,
profile,
bind,
issuer: env.CASAN_CP_JWT_ISSUER,
audience: env.CASAN_CP_JWT_AUDIENCE,
algorithm: publicKey ? 'RS256' : env.CASAN_CP_JWT_HS256_SECRET ? 'HS256' : undefined,
publicKey,
hmacSecret: env.CASAN_CP_JWT_HS256_SECRET,
clockSkewSeconds: Number.isFinite(skew) ? skew : -1,
roleClaim: env.CASAN_CP_JWT_ROLE_CLAIM || 'groups',
tenantClaim: env.CASAN_CP_JWT_TENANT_CLAIM || 'casan_tenant',
projectClaim: env.CASAN_CP_JWT_PROJECT_CLAIM || 'casan_project',
};
}
export function validateAuthConfig(config: ControlPlaneAuthConfig): string[] {
const errors: string[] = [];
const production = config.profile === 'prod' || config.profile === 'production' || config.profile === 'strict';
const nonLoopback = !['127.0.0.1', 'localhost', '::1'].includes(config.bind);
if (!['local', 'jwt'].includes(config.mode)) errors.push('auth_mode_invalid');
if ((production || nonLoopback) && config.mode !== 'jwt') errors.push('verified_identity_required');
if (config.clockSkewSeconds < 0 || config.clockSkewSeconds > 300) errors.push('auth_clock_skew_invalid');
if (config.mode === 'jwt') {
if (!config.issuer) errors.push('auth_issuer_required');
if (!config.audience) errors.push('auth_audience_required');
if (!config.algorithm) errors.push('auth_verification_key_required');
if (production && config.algorithm !== 'RS256') errors.push('auth_asymmetric_key_required_in_production');
if (config.algorithm === 'RS256') {
try {
if (!config.publicKey || createPublicKey(config.publicKey).asymmetricKeyType !== 'rsa') {
errors.push('auth_rsa_public_key_invalid');
}
} catch {
errors.push('auth_rsa_public_key_invalid');
}
}
if (config.algorithm === 'HS256' && (!config.hmacSecret || Buffer.byteLength(config.hmacSecret) < 32)) {
errors.push('auth_hmac_secret_too_short');
}
}
return errors;
}
function auditAuthentication(decision: AuthenticationDecision, request: Request): void {
const stateRoot = process.env.CASAN_STATE_ROOT || join(APP_ROOT, '.specify');
const path = join(stateRoot, 'logs', 'auth', 'decisions.jsonl');
mkdirSync(dirname(path), { recursive: true });
const record = {
schema_version: '1.0.0', category: 'runtime_control', policy_id: 'casan.control-plane.authentication',
timestamp: new Date().toISOString(), decision: decision.allowed ? 'allow' : 'deny', reason_code: decision.reasonCode,
actor: decision.claims?.subject, issuer: decision.claims?.issuer, tenant: decision.claims?.tenant,
project: decision.claims?.project, method: decision.claims?.authenticationMethod,
request: { method: request.method, path: request.path },
};
const fd = openSync(path, 'a', 0o600);
try {
writeSync(fd, `${JSON.stringify(record)}\n`);
fsyncSync(fd);
} finally {
closeSync(fd);
}
}
function overwriteVerifiedHeaders(request: Request, claims: VerifiedClaims): void {
for (const header of IDENTITY_HEADERS) delete request.headers[header];
const input: VerifiedClaimInput = {
subject: claims.subject,
issuer: claims.issuer,
roles: claims.roles,
tenant: claims.tenant,
project: claims.project,
};
const actor = actorFromVerifiedClaims(input);
request.headers['x-casan-actor'] = actor.actor;
request.headers['x-casan-role'] = actor.role;
request.headers['x-casan-project'] = actor.project;
request.headers['x-casan-tenant'] = actor.tenant;
request.headers['x-casan-identity-verified'] = '1';
request.headers['x-casan-identity-issuer'] = claims.issuer;
}
export function createAuthMiddleware(config: ControlPlaneAuthConfig) {
const provider = config.mode === 'jwt' ? new JwtAuthProvider(config) : undefined;
return (request: Request, response: Response, next: NextFunction): void => {
if (!provider) {
request.headers['x-casan-identity-verified'] = 'local-development-only';
next();
return;
}
const headers = request.headers as Record<string, string | string[] | undefined>;
const decision = provider.authenticate(headers);
auditAuthentication(decision, request);
if (!decision.allowed || !decision.claims) {
response.status(401).json({ success: false, error: { code: decision.reasonCode, message: 'Authentication failed' } });
return;
}
overwriteVerifiedHeaders(request, decision.claims);
next();
};
}
export function tokenFingerprint(token: string): string {
return createHash('sha256').update(token).digest('hex').slice(0, 12);
}
@@ -3,23 +3,28 @@ import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module.js';
import { APP_ROOT } from './common/app-root.js';
import { createAuthMiddleware, loadAuthConfig, validateAuthConfig } from './common/auth-provider.js';
// Ops Console API (Plan-13). Binds loopback by default and refuses a non-loopback
// bind under CASAN_PROFILE=prod / CASAN_CP_STRICT=1 unless an authenticated reverse
// proxy is explicitly configured to overwrite identity headers.
// Ops Console API (Plan-13). Binds loopback by default. Networked and production
// deployments require in-process cryptographic identity verification.
async function bootstrap() {
const authConfig = loadAuthConfig();
const authErrors = validateAuthConfig(authConfig);
if (authErrors.length > 0) {
console.error(`CP_AUTH_CONFIGURATION_INVALID reasons=${authErrors.join(',')}`);
process.exit(2);
}
const app = await NestFactory.create(AppModule, { cors: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.use(createAuthMiddleware(authConfig));
const port = Number(process.env.CP_PORT ?? 3010);
let host = process.env.CP_BIND ?? '127.0.0.1';
const strict = process.env.CASAN_PROFILE === 'prod' || process.env.CASAN_CP_STRICT === '1';
const authProxy = process.env.CASAN_CP_TRUST_AUTH_PROXY === '1';
if (strict && host !== '127.0.0.1' && host !== 'localhost' && !authProxy) {
// The console must not expose telemetry/management off-loopback without TLS/OIDC
// at the reverse proxy, which must overwrite X-CASAN-* identity headers.
if (strict && host !== '127.0.0.1' && host !== 'localhost' && authConfig.mode !== 'jwt') {
// Networked production requires in-process cryptographic verification.
// eslint-disable-next-line no-console
console.error(`CP_REFUSE_NONLOOPBACK host=${host} (set up TLS/OIDC per Plan-13 Track 4 first)`);
console.error(`CP_REFUSE_NONLOOPBACK host=${host} reason=verified_identity_required`);
process.exit(2);
}
@@ -32,6 +32,18 @@ function coverage(label: string, value: number, detail: string): string {
</div>`;
}
function distributionRows(report: H6Report): string {
const maximum = Math.max(...report.details.by_status.map((row) => row.count), 1);
if (report.details.by_status.length === 0) return '<p class="empty">No measured outcome distribution is available.</p>';
return `<ol class="distribution">${report.details.by_status.map((row) => {
const value = Math.max(row.count, 0);
const width = value === 0 ? 0 : Math.max((value / maximum) * 100, 3);
const tone = row.status === 'failed' ? 'fail' : row.status === 'degraded' ? 'attention' : 'pass';
const share = report.summary.runs ? Math.round((value / report.summary.runs) * 100) : 0;
return `<li><div><span><strong>${escapeHtml(row.status.replaceAll('_', ' '))}</strong><small>${share}% of selected runs</small></span><b>${number(value)}</b></div><i><em class="${tone}" style="width:${width}%"></em></i></li>`;
}).join('')}</ol>`;
}
export function renderH6ReportHtml(report: H6Report): string {
const verdictClass = report.verdict === 'pass' ? 'pass' : report.verdict === 'fail' ? 'fail' : report.verdict === 'attention' ? 'attention' : 'neutral';
const scope = [
@@ -59,6 +71,7 @@ export function renderH6ReportHtml(report: H6Report): string {
const warnings = report.data_quality.warnings.length
? report.data_quality.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')
: '<li class="positive">All required sources are present and no estimation warning was detected.</li>';
const outcomes = distributionRows(report);
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -73,10 +86,12 @@ export function renderH6ReportHtml(report: H6Report): string {
.section{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;border-bottom:1px solid #f2f4f7;padding:17px 20px}.kicker{color:#087e8b;font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.section-head h2{margin:3px 0 0;font-size:16px;letter-spacing:-.01em}.section-head p{max-width:580px;margin:3px 0 0;color:var(--muted);font-size:10px}.section-body{padding:18px 20px}
.findings{display:grid;gap:10px}.finding{display:grid;grid-template-columns:38px minmax(0,1fr);gap:12px;border:1px solid #eaecf0;border-left:4px solid var(--cyan);border-radius:10px;padding:12px}.finding.warning{border-left-color:var(--amber);background:#fffcf5}.finding.critical{border-left-color:var(--red);background:#fffbfa}.finding.success{border-left-color:var(--green);background:#f6fef9}.finding-index{color:#98a2b3;font:15px ui-monospace,SFMono-Regular,Menlo,monospace}.finding-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.finding-head strong{font:10px ui-monospace,SFMono-Regular,Menlo,monospace}.finding-head code{color:#667085;font-size:8px}.finding p{margin:5px 0 0;color:#475467;font-size:11px}
.quality-grid{display:grid;grid-template-columns:.75fr 1.25fr;gap:20px}.coverage+.coverage{margin-top:20px}.coverage-head{display:flex;justify-content:space-between;gap:12px}.coverage-head span{color:#667085;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.coverage-head strong{font:13px ui-monospace,SFMono-Regular,Menlo,monospace}.track{height:6px;margin-top:7px;overflow:hidden;border-radius:99px;background:#f2f4f7}.track i{display:block;height:100%;border-radius:99px;background:#d0d5dd}.track i.good{background:var(--green)}.track i.warn{background:var(--amber)}.coverage small{display:block;margin-top:6px;color:#98a2b3}.disclosure{border:1px solid #eaecf0;border-radius:12px;background:#f9fafb;padding:14px}.disclosure strong{font-size:10px;text-transform:uppercase;letter-spacing:.1em}.disclosure ul{margin:10px 0 0;padding-left:17px;color:#475467;font-size:10px}.disclosure li+li{margin-top:6px}.disclosure .positive{color:#027a48}
.distribution{display:grid;gap:15px;margin:0;padding:0;list-style:none}.distribution li>div{display:flex;align-items:end;justify-content:space-between;gap:16px}.distribution span strong,.distribution span small{display:block}.distribution span strong{font-size:11px;text-transform:capitalize}.distribution span small{margin-top:3px;color:var(--muted);font-size:9px}.distribution b{font:10px ui-monospace,SFMono-Regular,Menlo,monospace}.distribution i{display:block;overflow:hidden;height:7px;margin-top:7px;border-radius:99px;background:#eaecf0}.distribution em{display:block;height:100%;border-radius:inherit;background:var(--cyan)}.distribution em.pass{background:var(--green)}.distribution em.attention{background:var(--amber)}.distribution em.fail{background:var(--red)}
.technical{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.technical>summary{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:17px 20px;cursor:pointer;list-style:none}.technical>summary::-webkit-details-marker{display:none}.technical>summary strong,.technical>summary small{display:block}.technical>summary strong{font-size:13px}.technical>summary small{margin-top:3px;color:var(--muted);font-size:10px}.technical>summary b{color:var(--soft);font-size:18px;font-weight:400}.technical[open]>summary b{transform:rotate(45deg)}.technical-body{border-top:1px solid #f2f4f7;padding:18px 20px}.technical .summary{margin:0 0 16px}.technical .section{box-shadow:none}
.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;font-size:10px}th{border-block:1px solid #eaecf0;background:#f9fafb;padding:9px 10px;color:#667085;font-size:8px;letter-spacing:.1em;text-align:left;text-transform:uppercase}td{border-bottom:1px solid #f2f4f7;padding:10px;vertical-align:top}tr:last-child td{border-bottom:0}.numeric{text-align:right;font:9px ui-monospace,SFMono-Regular,Menlo,monospace}.negative{color:#b42318;font-weight:700}td code{display:block;max-width:420px;color:#98a2b3;font-size:8px;word-break:break-all}.status{display:inline-flex;border:1px solid #d0d5dd;border-radius:999px;padding:3px 7px;color:#475467;font-size:8px;font-weight:900;text-transform:uppercase}.status.fresh{border-color:#a6f4c5;background:#ecfdf3;color:#027a48}.status.stale{border-color:#fedf89;background:#fffaeb;color:#b54708}.status.missing{border-color:#fecdca;background:#fef3f2;color:#b42318}.empty{padding:30px;color:#98a2b3;text-align:center}
.footer{display:flex;justify-content:space-between;gap:20px;margin-top:20px;border-top:1px solid var(--line);padding-top:12px;color:#98a2b3;font-size:9px}
@media(max-width:820px){main{padding:18px 12px 44px}.cover{padding:24px}.cover h1{font-size:28px}.cover-foot{display:block}.document-meta{margin-top:18px;text-align:left}.summary{grid-template-columns:repeat(2,1fr)}.quality-grid{grid-template-columns:1fr}.finding-head{display:block}.finding-head code{display:block;margin-top:4px}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.section,.metric,.finding{break-inside:avoid}.cover h1{font-size:30px}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section,.technical{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.section,.metric,.finding{break-inside:avoid}.cover h1{font-size:30px}.technical>summary b{display:none}.technical-body{display:block!important}}
</style></head><body><main>
<header class="cover">
<div class="eyebrow">CASAN · H6 AgentOps assurance</div>
@@ -93,31 +108,33 @@ export function renderH6ReportHtml(report: H6Report): string {
<div class="metric"><span>Failure rate</span><strong>${report.summary.failure_rate_pct}%</strong><small>${number(report.summary.failed)} failed · ${number(report.summary.alerts)} alerts</small></div>
<div class="metric"><span>P95 latency</span><strong>${number(report.summary.latency_ms.p95)} ms</strong><small>P50 ${number(report.summary.latency_ms.p50)} ms</small></div>
<div class="metric"><span>Data quality</span><strong>${escapeHtml(report.data_quality.status.replaceAll('_', ' '))}</strong><small>Coverage disclosed below</small></div>
<div class="metric"><span>Provider tokens</span><strong>${report.summary.coverage.token_records > 0 ? number(report.summary.tokens.provider_total ?? report.summary.tokens.total ?? 0) : '<span class="unavailable">Unavailable</span>'}</strong><small>${report.summary.coverage.token_pct}% record coverage</small></div>
<div class="metric"><span>Actual provider cost</span><strong>${report.summary.cost_usd.provider_actual !== null ? money(report.summary.cost_usd.provider_actual) : '<span class="unavailable">Unavailable</span>'}</strong><small>${number(report.summary.provider_calls)} provider calls</small></div>
<div class="metric"><span>Cost attribution</span><strong>${report.summary.coverage.cost_pct}%</strong><small>${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records</small></div>
<div class="metric"><span>Retries</span><strong>${number(report.summary.retries)}</strong><small>Observed reroute attempts</small></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Decision brief</div><h2>What requires attention</h2><p>Threshold findings are operational review signals, not maturity claims.</p></div><span class="status ${verdictClass}">${escapeHtml(report.verdict)}</span></div>
<div class="section-body"><div class="findings">${findings}</div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Outcome mix</div><h2>Run distribution</h2><p>Canonical runtime status values, normalized against the largest observed group.</p></div><span class="status">${number(report.summary.runs)} total</span></div>
<div class="section-body">${outcomes}</div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Evidence quality</div><h2>Coverage and disclosure</h2><p>Unavailable telemetry remains null and is never rendered as zero.</p></div><span class="status">${escapeHtml(report.data_quality.status)}</span></div>
<div class="section-body"><div class="quality-grid"><div>${coverage('Token attribution', report.summary.coverage.token_pct, `${report.summary.coverage.token_records}/${report.summary.coverage.runtime_records} runtime records`)}${coverage('Cost attribution', report.summary.coverage.cost_pct, `${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records`)}</div><div class="disclosure"><strong>Disclosure notes</strong><ul>${warnings}</ul></div></div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Source integrity</div><h2>Evidence provenance</h2><p>Freshness and presence are evaluated independently for every canonical source.</p></div></div>
<div class="table-wrap"><table><thead><tr><th>Source</th><th>State</th><th class="numeric">Age</th><th class="numeric">Records</th><th>Canonical path</th></tr></thead><tbody>${sourceRows}</tbody></table></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Runtime</div><h2>Execution profile by step</h2></div></div>
<div class="table-wrap"><table><thead><tr><th>Step</th><th class="numeric">Runs</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_step)}</tbody></table></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Provider</div><h2>Model usage and provenance</h2><p>Only provider-reported token and actual cost values are shown.</p></div></div>
<div class="table-wrap"><table><thead><tr><th>Provider · model</th><th class="numeric">Calls</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_provider)}</tbody></table></div>
</section>
<details class="technical">
<summary><span><strong>Technical evidence and breakdowns</strong><small>Provider counters, canonical paths and raw tables are preserved for audit.</small></span><b>+</b></summary>
<div class="technical-body">
<section class="summary">
<div class="metric"><span>Provider tokens</span><strong>${report.summary.coverage.token_records > 0 ? number(report.summary.tokens.provider_total ?? report.summary.tokens.total ?? 0) : '<span class="unavailable">Unavailable</span>'}</strong><small>${report.summary.coverage.token_pct}% record coverage</small></div>
<div class="metric"><span>Actual provider cost</span><strong>${report.summary.cost_usd.provider_actual !== null ? money(report.summary.cost_usd.provider_actual) : '<span class="unavailable">Unavailable</span>'}</strong><small>${number(report.summary.provider_calls)} provider calls</small></div>
<div class="metric"><span>Cost attribution</span><strong>${report.summary.coverage.cost_pct}%</strong><small>${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records</small></div>
<div class="metric"><span>Retries</span><strong>${number(report.summary.retries)}</strong><small>Observed reroute attempts</small></div>
</section>
<section class="section"><div class="section-head"><div><div class="kicker">Source integrity</div><h2>Evidence provenance</h2></div></div><div class="table-wrap"><table><thead><tr><th>Source</th><th>State</th><th class="numeric">Age</th><th class="numeric">Records</th><th>Canonical path</th></tr></thead><tbody>${sourceRows}</tbody></table></div></section>
<section class="section"><div class="section-head"><div><div class="kicker">Runtime</div><h2>Execution profile by step</h2></div></div><div class="table-wrap"><table><thead><tr><th>Step</th><th class="numeric">Runs</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_step)}</tbody></table></div></section>
<section class="section"><div class="section-head"><div><div class="kicker">Provider</div><h2>Model usage and provenance</h2></div></div><div class="table-wrap"><table><thead><tr><th>Provider · model</th><th class="numeric">Calls</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_provider)}</tbody></table></div></section>
</div>
</details>
<footer class="footer"><span>Generated from CASAN runtime evidence. No maturity score or telemetry value is hard-coded in this document.</span><span>Contract v${report.schema_version} · ${escapeHtml(report.report_id)}</span></footer>
</main></body></html>`;
}
@@ -298,6 +298,8 @@ export function buildH6Report(input: H6ReportInput, query: H6ReportQuery): H6Rep
return {
schema_version: 1,
category: 'report_dimension',
dimension_id: 'ReportDimension.H6',
report_id: `H6-${input.now.toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`,
harness: 'H6',
title: 'H6 · AgentOps Report',
@@ -36,6 +36,8 @@ export interface HarnessReportEvidenceSource extends SourceFreshness {
export interface HarnessReport<TSummary, TDetails> {
schema_version: 1;
category: 'report_dimension';
dimension_id: `ReportDimension.${HarnessReportId}`;
report_id: string;
harness: HarnessReportId;
title: string;
@@ -71,6 +73,8 @@ export interface HarnessReportCatalogEntry {
title: string;
description: string;
contract_version: 1;
category: 'report_dimension';
dimension_id: `ReportDimension.${HarnessReportId}`;
endpoint: string;
availability: 'implemented' | 'contract_ready';
}
@@ -78,6 +82,8 @@ export interface HarnessReportCatalogEntry {
export const HARNESS_REPORT_CATALOG: HarnessReportCatalogEntry[] = HARNESS_REPORT_DEFINITIONS.map((definition) => ({
...definition,
contract_version: 1,
category: 'report_dimension',
dimension_id: `ReportDimension.${definition.id}`,
endpoint: `/api/v1/reports/${definition.id.toLowerCase()}`,
availability: definition.id === 'H6' ? 'implemented' : 'contract_ready',
}));
@@ -60,8 +60,9 @@ main{max-width:1180px;margin:0 auto;padding:38px 24px 64px}.cover{position:relat
.spine{position:relative;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));padding:24px 18px 20px}.spine:before{position:absolute;left:8%;right:8%;top:43px;height:1px;background:var(--line);content:""}.spine-node{position:relative;text-align:center}.spine-dot{display:flex;width:38px;height:38px;margin:0 auto;align-items:center;justify-content:center;border:2px solid #d0d5dd;border-radius:99px;background:#fff;color:#667085;font-weight:900;box-shadow:0 0 0 5px #fff}.spine-node.pass .spine-dot{border-color:#6ce9a6;color:#027a48}.spine-node.warn .spine-dot{border-color:#fec84b;color:#b54708}.spine-node.fail .spine-dot{border-color:#fda29b;color:#b42318}.spine-node strong{display:block;margin-top:8px;color:#98a2b3;font-size:9px;letter-spacing:.13em}.spine-node small{display:block;margin-top:3px;overflow:hidden;color:#344054;font-size:9px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}
.controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:18px}.gate{display:grid;grid-template-columns:48px minmax(0,1fr);overflow:hidden;border:1px solid var(--line);border-radius:14px;background:#fff;break-inside:avoid}.gate-index{display:flex;align-items:flex-start;justify-content:center;border-right:1px solid var(--line);padding-top:18px;color:#98a2b3;font-size:12px;font-weight:900}.gate.pass{border-top:3px solid var(--green)}.gate.warn{border-top:3px solid var(--amber)}.gate.fail{border-top:3px solid var(--red)}.gate-copy{padding:15px}.gate-heading{display:flex;align-items:center;justify-content:space-between;gap:10px}.gate-heading h3{margin:0;font-size:14px}.badge{border:1px solid #d0d5dd;border-radius:999px;padding:3px 7px;color:#475467;font-size:8px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.badge.pass{border-color:#a6f4c5;background:#ecfdf3;color:#027a48}.badge.warn{border-color:#fedf89;background:#fffaeb;color:#b54708}.badge.fail{border-color:#fecdca;background:#fef3f2;color:#b42318}.gate-description{min-height:34px;margin:6px 0;color:var(--muted);font-size:10px}.decision{margin-top:10px;border-left:3px solid #d0d5dd;background:#f9fafb;padding:9px 10px}.decision span,.evidence-title{display:block;color:#667085;font-size:8px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.decision strong{display:block;margin-top:4px;font-size:10px;font-weight:600}.decision small{display:block;margin-top:4px;color:#98a2b3;font:8px ui-monospace,SFMono-Regular,Menlo,monospace}.evidence-title{margin-top:12px}.manifest{margin:6px 0 0;border:1px solid #eaecf0;border-radius:8px}.manifest div{display:grid;grid-template-columns:110px minmax(0,1fr);border-bottom:1px solid #eaecf0}.manifest div:last-child{border-bottom:0}.manifest dt,.manifest dd{margin:0;padding:6px 7px;font:8px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.manifest dt{background:#f9fafb;color:#667085;font-weight:700}.manifest dd{color:#344054}.empty-evidence{margin-top:6px;border:1px dashed #d0d5dd;border-radius:8px;padding:10px;color:#98a2b3;font-size:9px;text-align:center}
.certification{display:grid;grid-template-columns:180px minmax(0,1fr);gap:20px;padding:19px 20px}.certification label{color:#667085;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.certification strong{display:block;margin-top:5px;font-size:15px}.certification p{margin:0;color:#475467;font-size:11px}.footer{display:flex;justify-content:space-between;gap:20px;margin-top:20px;border-top:1px solid var(--line);padding-top:12px;color:#98a2b3;font-size:9px}
.technical{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.technical>summary{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:17px 20px;cursor:pointer;list-style:none}.technical>summary::-webkit-details-marker{display:none}.technical>summary strong,.technical>summary small{display:block}.technical>summary strong{font-size:13px}.technical>summary small{margin-top:3px;color:var(--muted);font-size:10px}.technical>summary b{color:var(--soft);font-size:18px;font-weight:400}.technical[open]>summary b{transform:rotate(45deg)}.technical-body{border-top:1px solid #f2f4f7;padding:18px}.technical .summary{margin:0 0 16px}.technical .section{box-shadow:none}
@media(max-width:820px){main{padding:18px 12px 44px}.cover{padding:24px}.cover h1{font-size:28px}.cover-grid{grid-template-columns:1fr}.summary{grid-template-columns:repeat(2,1fr)}.spine{min-width:720px}.spine-wrap{overflow:auto}.controls{grid-template-columns:1fr}.document-meta{grid-template-columns:1fr 1fr}.certification{grid-template-columns:1fr}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.spine{padding-inline:8px}.controls{grid-template-columns:repeat(2,1fr);padding:12px}.section,.metric,.gate{break-inside:avoid}.footer{position:relative}.cover h1{font-size:30px}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section,.technical{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.spine{padding-inline:8px}.controls{grid-template-columns:repeat(2,1fr);padding:12px}.section,.metric,.gate{break-inside:avoid}.footer{position:relative}.cover h1{font-size:30px}.technical>summary b{display:none}.technical-body{display:block!important}}
</style></head><body><main>
<header class="cover">
<div class="eyebrow">CASAN · independent assurance dossier</div>
@@ -82,23 +83,27 @@ main{max-width:1180px;margin:0 auto;padding:38px 24px 64px}.cover{position:relat
<div class="metric"><span>Evidence source</span><strong>${evidenceState}</strong><small>Trace + graph reconstruction</small></div>
<div class="metric"><span>H6 quality</span><strong>${escapeHtml(report.summary.telemetry_quality)}</strong><small>Telemetry disclosure status</small></div>
<div class="metric"><span>Duration</span><strong>${report.summary.duration_ms === null ? 'Unavailable' : `${report.summary.duration_ms} ms`}</strong><small>End-to-end governed run</small></div>
<div class="metric"><span>Tool calls</span><strong>${report.summary.tool_calls}</strong><small>Observed side-effect attempts</small></div>
<div class="metric"><span>Failures</span><strong>${report.summary.failures}</strong><small>Recorded execution failures</small></div>
<div class="metric"><span>Token usage</span><strong>${tokenValue}</strong><small>Never inferred as zero</small></div>
<div class="metric"><span>Actual cost</span><strong>${costValue}</strong><small>Provider attribution only</small></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Control sequence</div><h2>Live assurance rail · H1 → H7</h2><p>The evidence spine shows the terminal state of every governed control.</p></div><span class="badge ${badgeClass(report.verdict)}">${escapeHtml(report.verdict)}</span></div>
<div class="spine-wrap"><div class="spine">${spine}</div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Control dossiers</div><h2>Decision and evidence detail</h2><p>Every field below is sanitized before persistence. Raw prompts and tool output are excluded.</p></div></div>
<div class="controls">${gates}</div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Certification</div><h2>Assurance conclusion</h2></div></div>
<div class="certification"><div><label>Strength</label><strong>${escapeHtml(report.certification.strength ?? 'unknown')}</strong></div><p>${escapeHtml(report.certification.reasons.join(' · ') || 'No certification reason recorded.')}</p></div>
</section>
<details class="technical">
<summary><span><strong>Technical evidence and control dossiers</strong><small>Execution counters and sanitized field manifests are preserved for audit.</small></span><b>+</b></summary>
<div class="technical-body">
<section class="summary">
<div class="metric"><span>Tool calls</span><strong>${report.summary.tool_calls}</strong><small>Observed side-effect attempts</small></div>
<div class="metric"><span>Failures</span><strong>${report.summary.failures}</strong><small>Recorded execution failures</small></div>
<div class="metric"><span>Token usage</span><strong>${tokenValue}</strong><small>Never inferred as zero</small></div>
<div class="metric"><span>Actual cost</span><strong>${costValue}</strong><small>Provider attribution only</small></div>
</section>
<section class="section"><div class="section-head"><div><div class="kicker">Control dossiers</div><h2>Decision and evidence detail</h2><p>Every field below is sanitized before persistence. Raw prompts and tool output are excluded.</p></div></div><div class="controls">${gates}</div></section>
</div>
</details>
<footer class="footer"><span>Generated from CASAN runtime evidence. Unavailable values remain null.</span><span>Schema v${report.schema_version} · ${escapeHtml(report.trace_id)}</span></footer>
</main></body></html>`;
}
@@ -29,3 +29,17 @@ test('auth context fails closed to viewer for unknown role claim', () => {
});
assert.equal(actor.role, 'viewer');
});
test('JWT mode rejects direct spoofed identity headers without middleware verification', () => {
const prior = process.env.CASAN_CP_AUTH_MODE;
process.env.CASAN_CP_AUTH_MODE = 'jwt';
try {
assert.throws(() => actorFromHeaders({
'x-casan-actor': 'attacker',
'x-casan-role': 'org-admin',
}), /AUTH_VERIFIED_IDENTITY_REQUIRED/);
} finally {
if (prior === undefined) delete process.env.CASAN_CP_AUTH_MODE;
else process.env.CASAN_CP_AUTH_MODE = prior;
}
});
@@ -0,0 +1,110 @@
import { createHmac, createSign, generateKeyPairSync } from 'node:crypto';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
JwtAuthProvider,
validateAuthConfig,
type ControlPlaneAuthConfig,
} from '../src/common/auth-provider.js';
const secret = 'test-only-secret-with-sufficient-length';
const baseConfig: ControlPlaneAuthConfig = {
mode: 'jwt',
profile: 'test',
bind: '0.0.0.0',
issuer: 'https://issuer.test',
audience: 'casan-control-plane',
algorithm: 'HS256',
hmacSecret: secret,
clockSkewSeconds: 30,
roleClaim: 'groups',
tenantClaim: 'casan_tenant',
projectClaim: 'casan_project',
};
function token(payload: Record<string, unknown>, signingSecret = secret): string {
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signature = createHmac('sha256', signingSecret).update(`${header}.${body}`).digest('base64url');
return `${header}.${body}.${signature}`;
}
function rsToken(payload: Record<string, unknown>, privateKey: string): string {
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signer = createSign('RSA-SHA256');
signer.update(`${header}.${body}`);
signer.end();
return `${header}.${body}.${signer.sign(privateKey).toString('base64url')}`;
}
function claims(now: number, overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
iss: baseConfig.issuer,
aud: baseConfig.audience,
sub: 'verified-user',
exp: now + 300,
iat: now,
groups: ['project-admin'],
casan_tenant: 'tenant-a',
casan_project: 'project-a',
...overrides,
};
}
test('JWT provider verifies claims and ignores spoofed forwarded identity headers', () => {
const now = 1_800_000_000;
const provider = new JwtAuthProvider(baseConfig);
const decision = provider.authenticate({
authorization: `Bearer ${token(claims(now))}`,
'x-forwarded-user': 'attacker',
'x-casan-role': 'org-admin',
'x-casan-tenant': 'victim',
}, now);
assert.equal(decision.allowed, true);
assert.equal(decision.claims?.subject, 'verified-user');
assert.equal(decision.claims?.tenant, 'tenant-a');
assert.deepEqual(decision.claims?.roles, ['project-admin']);
});
test('JWT provider rejects missing, invalid, expired, wrong-audience and wrong-issuer tokens', () => {
const now = 1_800_000_000;
const provider = new JwtAuthProvider(baseConfig);
assert.equal(provider.authenticate({}, now).reasonCode, 'auth_bearer_token_required');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now), 'wrong-secret')}` }, now).reasonCode, 'auth_signature_invalid');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { exp: now - 31 }))}` }, now).reasonCode, 'auth_token_expired');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { aud: 'wrong' }))}` }, now).reasonCode, 'auth_audience_invalid');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { iss: 'https://wrong.test' }))}` }, now).reasonCode, 'auth_issuer_invalid');
});
test('production-compatible RS256 verification accepts a valid asymmetric token', () => {
const now = 1_800_000_000;
const keys = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
const provider = new JwtAuthProvider({
...baseConfig,
profile: 'production',
algorithm: 'RS256',
publicKey: keys.publicKey,
hmacSecret: undefined,
});
const decision = provider.authenticate({ authorization: `Bearer ${rsToken(claims(now), keys.privateKey)}` }, now);
assert.equal(decision.allowed, true);
assert.equal(decision.claims?.subject, 'verified-user');
});
test('production and non-loopback startup refuse local or symmetric identity modes', () => {
assert.deepEqual(
validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'production' }),
['verified_identity_required'],
);
assert.ok(validateAuthConfig({ ...baseConfig, profile: 'production' }).includes('auth_asymmetric_key_required_in_production'));
assert.ok(validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'development' }).includes('verified_identity_required'));
assert.ok(validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'strict', bind: '127.0.0.1' }).includes('verified_identity_required'));
assert.ok(validateAuthConfig({
...baseConfig, profile: 'production', algorithm: 'RS256', publicKey: 'not-a-public-key', hmacSecret: undefined,
}).includes('auth_rsa_public_key_invalid'));
});
@@ -145,6 +145,8 @@ test('HTML export is standalone, escaped and contains no hard-coded maturity sco
assert.doesNotMatch(html, /Average\s+\d|\/100|218 core tests/i);
assert.match(html, /No maturity score or telemetry value is hard-coded/);
assert.match(html, /Operational evidence, ready for a decision/);
assert.match(html, /Run distribution/);
assert.match(html, /<details class="technical">/);
assert.match(html, /Evidence provenance/);
assert.match(html, /@page\{size:A4/);
});
@@ -192,6 +194,7 @@ test('per-run assurance export carries H1-H7 and truthful H6 availability', () =
assert.match(html, /Live assurance rail · H1 → H7/);
assert.match(html, /Evidence fields/);
assert.match(html, /Decision and evidence detail/);
assert.match(html, /<details class="technical">/);
assert.match(html, /@page\{size:A4/);
assert.match(html, /Unavailable/);
assert.doesNotMatch(html, /\$0(?:\.0+)?/);
@@ -172,5 +172,80 @@ export function CoverageBar({
);
}
export function DistributionBars({
label,
rows,
}: {
label: string;
rows: Array<{
label: string;
value: number;
detail?: ReactNode;
tone?: Tone;
}>;
}) {
const maximum = Math.max(...rows.map((row) => row.value), 1);
return (
<div role="img" aria-label={label}>
<ol className="space-y-4">
{rows.map((row) => {
const bounded = Math.max(row.value, 0);
const width = bounded === 0 ? 0 : Math.max((bounded / maximum) * 100, 3);
const tone = row.tone ?? 'info';
return (
<li key={row.label}>
<div className="flex items-end justify-between gap-4">
<div className="min-w-0">
<p className="truncate text-xs font-semibold text-slate-800">{row.label}</p>
{row.detail && <div className="mt-1 text-[11px] text-slate-500">{row.detail}</div>}
</div>
<span className="shrink-0 font-mono text-xs font-semibold text-slate-900">{integerLabel(bounded)}</span>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-slate-100" aria-hidden="true">
<div className={`h-full rounded-full ${TONE[tone].line}`} style={{ width: `${width}%` }} />
</div>
</li>
);
})}
</ol>
{rows.length === 0 && <p className="text-sm text-slate-500">No measured distribution is available.</p>}
</div>
);
}
function integerLabel(value: number): string {
return new Intl.NumberFormat('en-US').format(value);
}
export function DisclosurePanel({
summary,
description,
badge,
children,
open = false,
}: {
summary: string;
description: string;
badge?: ReactNode;
children: ReactNode;
open?: boolean;
}) {
return (
<details open={open} className="group overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-[0_10px_28px_rgba(15,23,42,0.04)]">
<summary className="flex min-h-16 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 outline-none transition hover:bg-slate-50 focus-visible:ring-4 focus-visible:ring-inset focus-visible:ring-cyan-100 [&::-webkit-details-marker]:hidden sm:px-6">
<div>
<p className="text-sm font-semibold text-slate-900">{summary}</p>
<p className="mt-1 text-xs leading-5 text-slate-500">{description}</p>
</div>
<div className="flex shrink-0 items-center gap-3">
{badge}
<span className="select-none text-lg text-slate-400 transition-transform group-open:rotate-45" aria-hidden="true">+</span>
</div>
</summary>
<div className="border-t border-slate-100 p-5 sm:p-6">{children}</div>
</details>
);
}
export const reportActionPrimary = 'inline-flex min-h-11 items-center justify-center rounded-xl bg-white px-4 py-2.5 text-xs font-bold text-slate-950 shadow-sm transition hover:-translate-y-px hover:bg-cyan-50 focus:outline-none focus:ring-2 focus:ring-cyan-300';
export const reportActionSecondary = 'inline-flex min-h-11 items-center justify-center rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-2.5 text-xs font-bold text-slate-200 transition hover:-translate-y-px hover:border-slate-500 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-400';
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api, runReportExportUrl, type HarnessGateNode, type HarnessTraceGraph } from '../../lib/api';
import { ReportPanel } from '../report/ReportPrimitives';
import { DisclosurePanel, ReportPanel } from '../report/ReportPrimitives';
import { StatusBadge } from '../ui/Card';
import { AssuranceRail } from './AssuranceRail';
@@ -35,8 +35,10 @@ function GateDossier({ node }: { node: HarnessGateNode }) {
</div>
<p className="mt-3 text-sm font-medium leading-6 text-slate-800">{node.reason || 'No decision reason was emitted.'}</p>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-4">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Event history</p>
<DisclosurePanel
summary="Event history"
description={`${node.events.length} sanitized lifecycle events · hidden by default`}
>
<div className="mt-3 space-y-3">
{node.events.slice(-4).reverse().map((event, index) => (
<div key={`${event.timestamp}-${index}`} className="grid grid-cols-[0.65rem_minmax(0,1fr)] gap-3">
@@ -55,33 +57,30 @@ function GateDossier({ node }: { node: HarnessGateNode }) {
))}
{node.events.length === 0 && <p className="text-sm text-slate-500">No individual event was emitted for this control.</p>}
</div>
</div>
</DisclosurePanel>
</div>
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 text-slate-100 shadow-[0_18px_50px_rgba(15,23,42,0.18)]">
<div className="flex items-center justify-between gap-3 border-b border-slate-800 px-5 py-4">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-300">Safe evidence manifest</p>
<p className="mt-1 text-xs text-slate-500">Sanitized fields persisted by the harness</p>
</div>
<span className="rounded-full border border-slate-700 bg-slate-900 px-2.5 py-1 font-mono text-[10px] text-slate-400">{entries.length} fields</span>
</div>
<DisclosurePanel
summary="Safe evidence manifest"
description="Sanitized fields persisted by the harness · hidden by default"
badge={<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-mono text-[10px] text-slate-500">{entries.length} fields</span>}
>
{entries.length > 0 ? (
<dl className="divide-y divide-slate-800">
<dl className="-m-5 divide-y divide-slate-100 sm:-m-6">
{entries.map(([key, value]) => (
<div key={key} className="grid gap-1 px-5 py-3.5 sm:grid-cols-[10rem_minmax(0,1fr)] sm:gap-4">
<dt className="font-mono text-[10px] font-semibold uppercase tracking-[0.08em] text-slate-500">{key}</dt>
<dd className="break-all font-mono text-xs leading-5 text-slate-200"><EvidenceValue value={value} /></dd>
<dd className="break-all font-mono text-xs leading-5 text-slate-700"><EvidenceValue value={value} /></dd>
</div>
))}
</dl>
) : (
<div className="px-5 py-12 text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full border border-slate-700 text-slate-500">∅</div>
<p className="mt-3 text-sm text-slate-400">No safe evidence fields are available yet.</p>
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 text-slate-400">∅</div>
<p className="mt-3 text-sm text-slate-500">No safe evidence fields are available yet.</p>
</div>
)}
</div>
</DisclosurePanel>
</div>
);
}
@@ -3,6 +3,8 @@ import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
CoverageBar,
DisclosurePanel,
DistributionBars,
ReportHero,
ReportPanel,
SignalMetric,
@@ -140,11 +142,10 @@ export function H6ReportPage() {
) : undefined}
/>
<ReportPanel
eyebrow="Scope"
title="Evidence boundary"
description="Filters are applied server-side to the screen, JSON contract and print-ready dossier."
right={Object.values(draft).some(Boolean) ? <span className="rounded-full bg-cyan-50 px-3 py-1 text-[10px] font-black uppercase tracking-[0.12em] text-cyan-700">Filtered view</span> : <span className="text-xs text-slate-400">All available evidence</span>}
<DisclosurePanel
summary="Filter the evidence boundary"
description="Project, date and trace filters stay out of the executive view until they are needed."
badge={Object.values(draft).some(Boolean) ? <span className="rounded-full bg-cyan-50 px-3 py-1 text-[10px] font-black uppercase tracking-[0.12em] text-cyan-700">Filtered</span> : <span className="text-xs text-slate-400">All evidence</span>}
>
<form onSubmit={applyFilters} className="grid gap-4 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1fr_1.45fr_auto] xl:items-end">
<FilterField label="Project">
@@ -168,7 +169,7 @@ export function H6ReportPage() {
<button type="button" onClick={clearFilters} className="min-h-11 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-bold text-slate-600 transition hover:bg-slate-50">Clear</button>
</div>
</form>
</ReportPanel>
</DisclosurePanel>
{report.isLoading && <LoadingReport />}
{report.isError && (
@@ -187,24 +188,17 @@ export function H6ReportPage() {
<SignalMetric label="Telemetry integrity" value={data.data_quality.status.replaceAll('_', ' ')} detail={`${data.summary.coverage.token_pct}% token · ${data.summary.coverage.cost_pct}% cost coverage`} tone={reportTone(data.data_quality.status)} primary />
</section>
<section className="grid gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-[0_12px_32px_rgba(15,23,42,0.04)] sm:grid-cols-2 lg:grid-cols-4">
<div className="border-b border-slate-100 p-2 sm:border-b-0 sm:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Provider tokens</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{data.summary.coverage.token_records > 0 ? integer(data.summary.tokens.provider_total ?? data.summary.tokens.total ?? 0) : 'Unavailable'}</p>
</div>
<div className="border-b border-slate-100 p-2 sm:border-b-0 lg:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Actual cost</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{data.summary.cost_usd.provider_actual !== null ? money(data.summary.cost_usd.provider_actual) : 'Unavailable'}</p>
</div>
<div className="border-b border-slate-100 p-2 sm:border-b-0 sm:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Provider calls</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{integer(data.summary.provider_calls)}</p>
</div>
<div className="p-2">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Retries</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{integer(data.summary.retries)}</p>
</div>
</section>
<DisclosurePanel
summary="Provider telemetry"
description="Token, cost, call and retry counters are technical evidence, hidden by default."
>
<section className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<SignalMetric label="Provider tokens" value={data.summary.coverage.token_records > 0 ? integer(data.summary.tokens.provider_total ?? data.summary.tokens.total ?? 0) : 'Unavailable'} detail={`${data.summary.coverage.token_pct}% record coverage`} tone={data.summary.coverage.token_records > 0 ? 'info' : 'warning'} />
<SignalMetric label="Actual cost" value={data.summary.cost_usd.provider_actual !== null ? money(data.summary.cost_usd.provider_actual) : 'Unavailable'} detail={`${data.summary.coverage.cost_pct}% record coverage`} tone={data.summary.cost_usd.provider_actual !== null ? 'info' : 'warning'} />
<SignalMetric label="Provider calls" value={integer(data.summary.provider_calls)} detail="Provider-attributed requests" />
<SignalMetric label="Retries" value={integer(data.summary.retries)} detail="Observed reroute attempts" tone={data.summary.retries > 0 ? 'warning' : 'neutral'} />
</section>
</DisclosurePanel>
<div className="grid gap-5 xl:grid-cols-[1.18fr_0.82fr]">
<ReportPanel
@@ -236,37 +230,20 @@ export function H6ReportPage() {
</ReportPanel>
<ReportPanel
eyebrow="Source integrity"
title="Evidence provenance"
description="Freshness is evaluated independently for every canonical source."
right={<StatusBadge value={data.freshness.status} />}
eyebrow="Outcome mix"
title="Run distribution"
description="Counts are grouped from canonical runtime status values; the longest bar is the largest observed group."
right={<span className="font-mono text-xs text-slate-400">{integer(data.summary.runs)} total</span>}
>
<div className="space-y-3">
{data.evidence_sources.map((source) => {
const state = !source.present ? 'missing' : source.stale ? 'stale' : 'fresh';
return (
<article key={source.source} className="rounded-xl border border-slate-200 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-sm font-semibold capitalize text-slate-900">{source.source}</h3>
<p className="mt-1 font-mono text-[10px] text-slate-400">{source.records} records · {source.age_s === null ? 'no timestamp' : `${integer(source.age_s)}s old`}</p>
</div>
<StatusBadge value={state} />
</div>
<p className="mt-3 break-all border-t border-slate-100 pt-3 font-mono text-[10px] leading-4 text-slate-400">{source.path}</p>
</article>
);
})}
</div>
</ReportPanel>
</div>
<div className="grid gap-5 xl:grid-cols-2">
<ReportPanel eyebrow="Runtime" title="Execution profile by step" description="Latency, failure and attribution by governed lifecycle step.">
<BreakdownTable rows={data.details.by_step} subject="Step" />
</ReportPanel>
<ReportPanel eyebrow="Provider" title="Model usage and provenance" description="Only provider-reported token and actual cost values are shown.">
<BreakdownTable rows={data.details.by_provider} subject="Provider · model" />
<DistributionBars
label="Governed run outcome distribution"
rows={data.details.by_status.map((row) => ({
label: row.status.replaceAll('_', ' '),
value: row.count,
detail: `${data.summary.runs ? Math.round((row.count / data.summary.runs) * 100) : 0}% of selected runs`,
tone: row.status === 'failed' ? 'danger' as const : row.status === 'degraded' ? 'warning' as const : 'success' as const,
}))}
/>
</ReportPanel>
</div>
@@ -297,17 +274,47 @@ export function H6ReportPage() {
</div>
</ReportPanel>
<div className="rounded-2xl border border-slate-800 bg-slate-950 p-5 text-white shadow-[0_18px_48px_rgba(15,23,42,0.16)] sm:flex sm:items-center sm:justify-between sm:gap-6 sm:p-6">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-300">Independent evidence package</p>
<h2 className="mt-2 text-lg font-semibold">Same contract. Two review formats.</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-400">JSON is machine-auditable. HTML is a self-contained, print-ready assurance dossier generated only when requested.</p>
<DisclosurePanel
summary="Technical evidence and breakdowns"
description="Canonical paths, step tables and provider attribution are preserved for audit without crowding the decision view."
badge={<StatusBadge value={data.freshness.status} />}
>
<div className="space-y-6">
<section>
<div className="mb-4">
<p className="text-[10px] font-black uppercase tracking-[0.16em] text-cyan-700">Source integrity</p>
<h3 className="mt-1 text-base font-semibold text-slate-900">Evidence provenance</h3>
</div>
<div className="grid gap-3 lg:grid-cols-2">
{data.evidence_sources.map((source) => {
const state = !source.present ? 'missing' : source.stale ? 'stale' : 'fresh';
return (
<article key={source.source} className="rounded-xl border border-slate-200 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<h4 className="text-sm font-semibold capitalize text-slate-900">{source.source}</h4>
<p className="mt-1 font-mono text-[10px] text-slate-400">{source.records} records · {source.age_s === null ? 'no timestamp' : `${integer(source.age_s)}s old`}</p>
</div>
<StatusBadge value={state} />
</div>
<p className="mt-3 break-all border-t border-slate-100 pt-3 font-mono text-[10px] leading-4 text-slate-400">{source.path}</p>
</article>
);
})}
</div>
</section>
<div className="grid gap-5 xl:grid-cols-2">
<div>
<h3 className="mb-3 text-sm font-semibold text-slate-900">Execution profile by step</h3>
<BreakdownTable rows={data.details.by_step} subject="Step" />
</div>
<div>
<h3 className="mb-3 text-sm font-semibold text-slate-900">Model usage and provenance</h3>
<BreakdownTable rows={data.details.by_provider} subject="Provider · model" />
</div>
</div>
</div>
<div className="mt-5 flex shrink-0 flex-wrap gap-2 sm:mt-0">
<a href={h6ReportExportUrl(query, 'json')} className={reportActionSecondary}>Download JSON</a>
<a href={h6ReportExportUrl(query, 'html')} className={reportActionPrimary}>Download HTML</a>
</div>
</div>
</DisclosurePanel>
</>
)}
</div>
@@ -17,6 +17,14 @@ if [[ ! -f "$TLS_DIR/tls.crt" || ! -f "$TLS_DIR/tls.key" ]]; then
-subj "/CN=localhost" \
-days 1 >/dev/null 2>&1
fi
IDP_PRIVATE="$ROOT/tmp/control-panel-local/idp-private.pem"
IDP_PUBLIC="$ROOT/tmp/control-panel-local/idp-public.pem"
if [[ ! -f "$IDP_PRIVATE" || ! -f "$IDP_PUBLIC" ]]; then
openssl genrsa -out "$IDP_PRIVATE" 2048 >/dev/null 2>&1
openssl rsa -in "$IDP_PRIVATE" -pubout -out "$IDP_PUBLIC" >/dev/null 2>&1
chmod 0600 "$IDP_PRIVATE"
chmod 0644 "$IDP_PUBLIC"
fi
cleanup() {
docker compose -f "$COMPOSE" down --remove-orphans >/dev/null 2>&1 || true
+87 -3
View File
@@ -30,6 +30,7 @@ from __future__ import annotations
import argparse
from contextlib import contextmanager
import hashlib
import importlib.util
import json
import os
import queue
@@ -160,6 +161,9 @@ def _render_init(result):
("Clients", _client_names(result["clients"])),
("Files", "%d created or updated" % len(result["created"])),
("Harness", result["harness_version"]),
("Core readiness", result["readiness"]["dimensions"]["core"]["status"].replace("_", " ")),
("Domain Pipeline", result["readiness"]["dimensions"]["domain_pipeline"]["status"].replace("_", " ")),
("Provider Telemetry", result["readiness"]["dimensions"]["provider_telemetry"]["status"].replace("_", " ")),
])
print()
print(_color("1", "Integrations"))
@@ -178,9 +182,13 @@ def _render_init(result):
_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 a local Codex client, open `/hooks` and trust this project's hook.")
actions = result["readiness"].get("next_actions") or []
steps = [action["message"] for action in actions]
steps.append("Run `casan doctor` for the deep integrity and client smoke check.")
steps.append(
"After a governed prompt, run `casan view` to inspect its H1–H7 evidence.")
for index, step in enumerate(steps, 1):
print(" %d. %s" % (index, step))
def _render_verify(result):
@@ -1221,6 +1229,7 @@ def cmd_init(args):
cfg = {
"schema_version": "21.3",
"project_id": project,
"project_name": project,
"created_at": now_iso(),
"enforcement_mode": args.mode,
"integration_mode": args.integration_mode,
@@ -1413,6 +1422,40 @@ def cmd_init(args):
newly_owned_files.update(domain_files)
level_extras.append("apps/%s/domain (%d files)" % (project, n))
# ── Commercial readiness contract.
# Core acceptance never invents or modifies application-domain inputs.
# Domain Pipeline and Provider Telemetry have independent readiness states.
readiness_module_path = os.path.join(
active_harness, "scripts", "python", "readiness.py")
if not os.path.isfile(readiness_module_path):
sys.stderr.write(
"casan init: resolved Core is missing the readiness contract\n")
return 1
readiness_state_paths = (
os.path.join(target, ".casan", "discovery.json"),
os.path.join(target, ".casan", "readiness.json"),
)
for state_path in readiness_state_paths:
mark_owned_if_absent(state_path)
try:
spec = importlib.util.spec_from_file_location(
"casan_runtime_readiness", readiness_module_path)
readiness_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(readiness_module)
client_state = {}
if "codex" in clients:
client_state["codex"] = _codex_project_hook_trust(
target, timeout=3)
readiness = readiness_module.assess(
target, persist=True, client_state=client_state)
except (AttributeError, OSError, TypeError, ValueError) as error:
sys.stderr.write(
"casan init: cannot generate commercial readiness state: %s\n" %
error)
return 1
for state_path in readiness_state_paths:
created_add(state_path)
# ── manifest (so uninstall/verify know what init created) ──
owned_file_hashes = {
path: digest
@@ -1460,6 +1503,7 @@ def cmd_init(args):
"level_extras": level_extras,
"level_removed": level_removed,
"level_retained": level_retained,
"readiness": readiness,
"note": (
"managed runtime is referenced by version/hash lock"
if runtime_mode == "managed" else
@@ -1910,6 +1954,18 @@ def cmd_doctor(args):
ready = ready and client_ready
checks["client_checks"][client] = item
for client, item in checks["client_checks"].items():
if item.get("ready") or client == "codex":
continue
checks["required_actions"].append({
"code": "%s_activation" % client.replace("-", "_"),
"message": (
item.get("install_action")
or "Repair the selected %s client route, then run `casan doctor`."
% CLIENT_LABELS.get(client, client)
),
})
if "codex" in clients:
codex_check = checks["client_checks"].get("codex", {})
trust = codex_check.get("trust", {})
@@ -1964,6 +2020,31 @@ def cmd_doctor(args):
else "user_action_required" if checks["required_actions"]
else "ready"
)
if harness:
readiness_module_path = os.path.join(
harness, "scripts", "python", "readiness.py")
try:
if not os.path.isfile(readiness_module_path):
raise FileNotFoundError
spec = importlib.util.spec_from_file_location(
"casan_doctor_readiness", readiness_module_path)
readiness_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(readiness_module)
readiness_clients = {
client: {
**(item.get("trust", {}) if client == "codex" else {}),
"operational": bool(item.get("ready")),
}
for client, item in checks["client_checks"].items()
}
checks["readiness"] = readiness_module.assess(
target, persist=True, client_state=readiness_clients)
except FileNotFoundError:
pass
except (AttributeError, OSError, TypeError, ValueError) as error:
checks["warnings"].append(
"Commercial readiness state could not be refreshed: %s." %
error)
_emit_json_or_human(args, checks, _render_doctor)
return 0 if ready else 2
@@ -2150,6 +2231,9 @@ def cmd_uninstall(args):
".casan/version.lock",
".casan/agentic.env",
".casan/casan-hook.py",
".casan/domain.json",
".casan/discovery.json",
".casan/readiness.json",
".casan/init-manifest.json"):
_remove_owned_file(
os.path.join(target, *relative.split("/")), removed, target)
@@ -45,7 +45,7 @@ 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",
"adapters", "agentops", "assets", "config", "governance", "memory", "prompts",
"schemas", "scripts", "security", "templates",
}
assert {path.name for path in (root / "packages/casan-devkit").iterdir()} == {
@@ -70,10 +70,21 @@ for forbidden in (
):
assert not (root / forbidden).exists(), forbidden
for required in (
".codex-plugin/plugin.json",
".claude-plugin/plugin.json",
".claude-plugin/marketplace.json",
"skills/casan/SKILL.md",
"packages/casan-harness/assets/local-viewer/index.html",
"packages/casan-harness/assets/local-viewer/app.css",
"packages/casan-harness/assets/local-viewer/app.js",
"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",
"packages/casan-harness/scripts/python/local_report.py",
"packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/report_cli.py",
"packages/casan-harness/scripts/python/readiness.py",
):
assert (root / required).is_file(), required
registry = json.loads(
@@ -138,8 +149,11 @@ for forbidden in (
):
assert not any(name == forbidden or name.startswith(forbidden + "/") for name in names), forbidden
for required in (
"packages/casan-harness/assets/local-viewer/index.html",
"packages/casan-harness/config/tool-registry.yaml",
"packages/casan-harness/scripts/bash/casan-harness.sh",
"packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/readiness.py",
"packaging/runtime-layout.json",
"scripts/copy-runtime.py",
"install.sh",
@@ -151,9 +165,31 @@ echo "===== ② casan init (config only, no harness copy) ====="
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 ) \
&& pass "casan init completes" || fail "casan init failed"
for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do
for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .casan/discovery.json .casan/readiness.json .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do
[[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f"
done
python3 - "$PROJ/.casan/readiness.json" <<'PY' \
&& pass "Core readiness is independent from optional Domain/Provider states" \
|| fail "commercial readiness contract is invalid"
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["dimensions"]["core"]["status"] in {"ready", "ready_with_attention"}
assert d["dimensions"]["domain_pipeline"]["status"] == "not_configured"
assert d["dimensions"]["provider_telemetry"]["status"] == "optional_unavailable"
PY
CASAN_APP_ROOT="$PROJ" "$CASAN" pipeline --help 2>&1 \
| grep -q "optional Domain Pack" \
&& pass "pipeline help describes the optional capability without executing it" \
|| fail "pipeline help executed or hid the Core/Domain boundary"
set +e
PIPELINE_MISSING="$(CASAN_APP_ROOT="$PROJ" "$CASAN" pipeline 2>&1)"
PIPELINE_RC=$?
set -e
[[ "$PIPELINE_RC" -eq 2 && "$PIPELINE_MISSING" == *"CASAN_PIPELINE_NOT_CONFIGURED"* ]] \
&& pass "missing Domain Pipeline is explicit and does not impersonate a Core failure" \
|| fail "missing Domain Pipeline capability was not reported truthfully"
python3 - "$PROJ/.casan/config.json" <<'PY' \
&& pass "config declares supported and excluded client surfaces" \
|| fail "config client surface contract is incomplete"
@@ -343,6 +379,12 @@ echo "$VENDORED_OUT" | grep -q "Runtime.*Vendored" \
( cd "$VENDORED" && "$VENDORED_ROOT/bin/casan" version >/dev/null ) \
&& pass "project-vendored Core includes a working local CLI" \
|| fail "project-vendored CLI cannot resolve its Core runtime"
printf 'vendored consumer smoke\n' > "$VENDORED/input.txt"
( cd "$VENDORED" && "$VENDORED_ROOT/bin/casan" run input.txt output.txt agent_step -- printf 'ok\n' >/dev/null ) \
&& [ -s "$VENDORED/.specify/logs/cost/metrics.jsonl" ] \
&& [ ! -d "$VENDORED_ROOT/.specify" ] \
&& pass "vendored CLI exports the consumer project root to harness commands" \
|| fail "vendored CLI wrote governed state inside the runtime bundle"
( cd "$VENDORED" && "$DKC" verify-harness >/dev/null ) \
&& pass "global launcher resolves and verifies the project-vendored Core" \
|| fail "vendored Core is not honored by the global launcher"
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Validate the repository-root CASAN native plugin facades."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
VERSION = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
def load_json(relative_path: str) -> dict[str, object]:
payload = json.loads((ROOT / relative_path).read_text(encoding="utf-8"))
assert isinstance(payload, dict), relative_path
return payload
def main() -> None:
codex = load_json(".codex-plugin/plugin.json")
assert codex["name"] == "casan"
assert codex["version"] == VERSION
assert codex["skills"] == "./skills/"
assert "hooks" not in codex
assert codex["license"] == "Proprietary"
interface = codex["interface"]
assert isinstance(interface, dict)
for field in (
"displayName",
"shortDescription",
"longDescription",
"developerName",
"category",
"capabilities",
"defaultPrompt",
):
assert interface.get(field), field
claude = load_json(".claude-plugin/plugin.json")
assert claude["name"] == "casan"
assert claude["version"] == VERSION
assert claude["skills"] == ["./skills/"]
assert "hooks" not in claude
marketplace = load_json(".claude-plugin/marketplace.json")
plugins = marketplace["plugins"]
assert isinstance(plugins, list) and len(plugins) == 1
entry = plugins[0]
assert isinstance(entry, dict)
assert entry["name"] == "casan"
assert entry["source"] == "./"
assert entry["version"] == VERSION
skill = (ROOT / "skills/casan/SKILL.md").read_text(encoding="utf-8")
assert skill.startswith("---\nname: casan\n")
assert "description:" in skill
assert "[TODO:" not in skill
assert (ROOT / "skills/casan/agents/openai.yaml").is_file()
levels = load_json("packaging/levels.json")
level_map = levels["levels"]
assert isinstance(level_map, dict)
core = level_map["core"]
assert isinstance(core, dict)
includes = core["includes"]
assert isinstance(includes, list)
for required in (".codex-plugin", ".claude-plugin", "skills"):
assert required in includes, required
print(f"NATIVE_PLUGIN_PASS version={VERSION} codex=true claude=true")
if __name__ == "__main__":
main()
+15
View File
@@ -5,6 +5,7 @@ Populated incrementally by Plan-01 (Phase 0→6). During migration, files move h
`.specify/` wave-by-wave; the full harness gate must stay green (`PASS=64 FAIL=0`) after each phase.
Layout:
- `assets/local-viewer/` — production static UI for the Core Local Assurance Viewer
- `scripts/` — bash + powershell gate logic (H1→H7), path resolver `casan-paths.sh`
- `security/` — filter/policy rules (prompt-filter, pii-rules, output-policy, ...)
- `governance/`, `agentops/` — H5/H6 code
@@ -12,6 +13,20 @@ Layout:
- `templates/` — spec/plan templates
- `tests/` — source-hub verification only; excluded from production installs
Core visual reporting is intentionally lazy and zero-dependency:
```bash
casan report latest
casan view
casan report export --format html
casan report export --h6 --format html
```
Prompt hooks only write canonical evidence and a small receipt. `casan view`
starts a loopback-only, token-protected, read-only Python server on demand.
No Node/npm, external font/CDN, Platform service or per-prompt HTML generation
is required.
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
`apps/okr/domain/` under `CASAN_DOMAIN_ROOT`.
@@ -0,0 +1,316 @@
:root {
--ink: #101828;
--muted: #667085;
--faint: #98a2b3;
--line: #e4e7ec;
--paper: #f6f7f9;
--white: #fff;
--navy: #101828;
--navy-2: #1d2939;
--cyan: #0891b2;
--cyan-soft: #ecfeff;
--green: #067647;
--green-soft: #ecfdf3;
--amber: #b54708;
--amber-soft: #fffaeb;
--rose: #b42318;
--rose-soft: #fef3f2;
--shadow: 0 12px 36px rgba(16, 24, 40, 0.06);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible {
outline: 3px solid rgba(6, 182, 212, 0.28);
outline-offset: 2px;
}
.hidden { display: none !important; }
.skip-link {
position: fixed; z-index: 100; left: 16px; top: -60px; padding: 10px 14px;
border-radius: 10px; background: white; color: var(--ink); font-weight: 700;
}
.skip-link:focus { top: 16px; }
.sidebar {
position: fixed; inset: 0 auto 0 0; z-index: 30; width: 244px; display: flex;
flex-direction: column; border-right: 1px solid #253044; background: var(--navy);
color: white; padding: 22px 16px;
}
.brand { display: flex; align-items: center; gap: 12px; color: white; text-decoration: none; padding: 4px 8px 24px; }
.brand-mark {
display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid #344054;
border-radius: 12px; background: #172033; color: #67e8f9; font: 800 16px ui-monospace, monospace;
}
.brand strong, .brand small { display: block; }
.brand strong { font-size: 14px; letter-spacing: .08em; }
.brand small { margin-top: 3px; color: #98a2b3; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
.sidebar nav { display: grid; gap: 5px; }
.nav-item {
display: grid; grid-template-columns: 30px 1fr; align-items: center; min-height: 46px; width: 100%;
border: 1px solid transparent; border-radius: 12px; background: transparent; color: #98a2b3;
padding: 8px 12px; text-align: left; font-size: 13px; font-weight: 650; cursor: pointer; transition: .18s ease;
}
.nav-item span { color: #667085; font: 700 11px ui-monospace, monospace; }
.nav-item:hover { background: #172033; color: white; }
.nav-item.active { border-color: #344054; background: #1d2939; color: white; }
.nav-item.active span { color: #67e8f9; }
.local-seal {
display: grid; grid-template-columns: 8px 1fr; gap: 10px; align-items: start; margin-top: auto;
border: 1px solid #344054; border-radius: 14px; background: #172033; padding: 13px;
}
.local-seal strong, .local-seal small { display: block; }
.local-seal strong { font-size: 11px; }
.local-seal small { margin-top: 4px; color: #667085; font: 10px ui-monospace, monospace; }
.live-dot { width: 7px; height: 7px; margin-top: 3px; border-radius: 50%; background: #32d583; box-shadow: 0 0 0 4px rgba(50,213,131,.12); }
.shell { margin-left: 244px; min-height: 100vh; }
.topbar {
position: sticky; top: 0; z-index: 20; display: flex; align-items: center; min-height: 76px;
border-bottom: 1px solid var(--line); background: rgba(255,255,255,.92); padding: 12px clamp(20px,4vw,48px);
backdrop-filter: blur(16px);
}
.topbar h1 { margin: 2px 0 0; font-size: 18px; letter-spacing: -.02em; }
.top-eyebrow { margin: 0; color: var(--faint); font-size: 9px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
.top-actions { display: flex; align-items: center; gap: 9px; margin-left: auto; }
.icon-button {
display: grid; place-items: center; min-width: 44px; min-height: 44px; border: 1px solid var(--line);
border-radius: 12px; background: white; color: #475467; cursor: pointer; transition: .18s ease;
}
.icon-button:hover { border-color: #98a2b3; transform: translateY(-1px); }
.menu-button { display: none; margin-right: 12px; }
main { max-width: 1500px; margin: 0 auto; padding: 28px clamp(20px,4vw,48px) 64px; }
.view { display: grid; gap: 18px; }
.hero {
position: relative; overflow: hidden; border-radius: 28px; background: var(--navy); color: white;
padding: clamp(28px,5vw,48px); box-shadow: 0 24px 70px rgba(16,24,40,.16);
}
.hero::after {
content: ""; position: absolute; width: 360px; height: 360px; right: -160px; top: -230px;
border: 1px solid rgba(103,232,249,.15); border-radius: 50%; box-shadow: 0 0 0 60px rgba(103,232,249,.025), 0 0 0 120px rgba(103,232,249,.02);
}
.hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 28px; align-items: end; }
.eyebrow { margin: 0; color: var(--cyan); font-size: 10px; font-weight: 850; letter-spacing: .18em; text-transform: uppercase; }
.hero .eyebrow { color: #67e8f9; }
.hero h2 { max-width: 760px; margin: 13px 0 12px; font-size: clamp(30px,4vw,48px); line-height: 1.02; letter-spacing: -.045em; }
.hero-description { max-width: 760px; margin: 0; color: #98a2b3; font-size: 14px; line-height: 1.7; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 24px; color: #667085; font: 10px ui-monospace, monospace; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: flex-end; }
.trace-hero { border-radius: 18px; padding: 20px 22px; box-shadow: none; }
.run-dossier-anchor { scroll-margin-top: 94px; outline: none; }
.run-dossier-anchor:focus-visible {
border-radius: 20px;
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.22);
}
.trace-title { margin: 8px 0 !important; font-size: 18px !important; }
.trace-meta { margin-top: 8px; }
.rail-spaced { margin-top: 20px; }
.button {
display: inline-flex; align-items: center; justify-content: center; min-height: 44px; border: 1px solid transparent;
border-radius: 12px; padding: 9px 15px; text-decoration: none; font-size: 12px; font-weight: 750; cursor: pointer; transition: .18s ease;
}
.button:hover { transform: translateY(-1px); }
.button.primary { background: #67e8f9; color: var(--navy); }
.button.primary:hover { background: #a5f3fc; }
.button.secondary { border-color: #344054; background: #1d2939; color: #e4e7ec; }
.button.light { border-color: #d0d5dd; background: white; color: #344054; }
.pill {
display: inline-flex; align-items: center; min-height: 28px; border-radius: 999px; padding: 5px 10px;
font-size: 9px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap;
}
.pill.pass, .pill.certified, .pill.success, .pill.fresh, .pill.operational { background: var(--green-soft); color: var(--green); }
.pill.warning, .pill.attention, .pill.ready-with-attention, .pill.ready_with_attention, .pill.partial, .pill.stale, .pill.in-progress, .pill.in_progress { background: var(--amber-soft); color: var(--amber); }
.pill.error, .pill.failed, .pill.fail, .pill.blocked, .pill.non-certified, .pill.non_certified { background: var(--rose-soft); color: var(--rose); }
.pill.neutral, .pill.queued, .pill.unknown, .pill.no-data, .pill.no_data { background: #f2f4f7; color: #475467; }
.hero .pill { border: 1px solid #344054; background: #1d2939; color: #d0d5dd; }
.metric-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; }
.metric-grid.readiness-grid { grid-template-columns: repeat(3,minmax(0,1fr)); }
.readiness-grid .metric-value { font-size: 24px; text-transform: capitalize; }
.onboarding-actions { margin-bottom: 18px; }
.metric-card, .panel {
border: 1px solid var(--line); border-radius: 18px; background: white; box-shadow: var(--shadow);
}
.metric-card { min-height: 136px; padding: 19px; }
.metric-label { color: var(--faint); font-size: 9px; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; }
.metric-value { margin-top: 18px; font-size: 27px; font-weight: 720; letter-spacing: -.04em; }
.metric-detail { margin-top: 7px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.panel { overflow: hidden; }
.panel-head { display: flex; justify-content: space-between; gap: 20px; align-items: start; padding: 23px 24px 18px; }
.panel h2 { margin: 6px 0 0; font-size: 19px; letter-spacing: -.025em; }
.panel-description { max-width: 720px; margin: 7px 0 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
.panel-body { border-top: 1px solid #f2f4f7; padding: 22px 24px 24px; }
.disclosure-panel {
overflow: hidden; border: 1px solid var(--line); border-radius: 18px; background: white;
box-shadow: 0 10px 28px rgba(16,24,40,.04);
}
.disclosure-panel > summary {
display: flex; min-height: 66px; align-items: center; justify-content: space-between; gap: 18px;
padding: 15px 20px; list-style: none; cursor: pointer; transition: background .16s ease;
}
.disclosure-panel > summary::-webkit-details-marker { display: none; }
.disclosure-panel > summary:hover { background: #f9fafb; }
.disclosure-panel > summary strong, .disclosure-panel > summary small { display: block; }
.disclosure-panel > summary strong { color: #344054; font-size: 13px; }
.disclosure-panel > summary small { margin-top: 5px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.disclosure-side { display: flex; flex-shrink: 0; align-items: center; gap: 10px; }
.disclosure-side > b { color: var(--faint); font-size: 20px; font-weight: 400; transition: transform .16s ease; }
.disclosure-panel[open] .disclosure-side > b { transform: rotate(45deg); }
.disclosure-body { border-top: 1px solid #f2f4f7; padding: 20px; }
.disclosure-body > dl { margin: -20px; }
.disclosure-panel .manifest-row { border-bottom: 1px solid #f2f4f7; }
.disclosure-panel .manifest-row:last-child { border-bottom: 0; }
.disclosure-panel .manifest-row dt { color: var(--faint); font: 750 9px ui-monospace,monospace; letter-spacing: .07em; text-transform: uppercase; }
.disclosure-panel .manifest-row dd { margin: 0; overflow-wrap: anywhere; color: #344054; font: 11px/1.5 ui-monospace,monospace; }
.two-col { display: grid; grid-template-columns: minmax(0,1.2fr) minmax(330px,.8fr); gap: 18px; }
.rail-wrap { overflow-x: auto; padding-bottom: 4px; }
.evidence-rail { display: grid; grid-template-columns: repeat(7,minmax(116px,1fr)); min-width: 840px; gap: 7px; }
.gate-button {
position: relative; min-height: 92px; border: 1px solid var(--line); border-radius: 14px; background: #fafafa;
padding: 13px; color: #344054; text-align: left; cursor: pointer; transition: .18s ease;
}
.gate-button::after { content: ""; position: absolute; top: 27px; right: -8px; width: 8px; height: 1px; background: #d0d5dd; }
.gate-button:last-child::after { display: none; }
.gate-button:hover { border-color: #98a2b3; transform: translateY(-1px); }
.gate-button.selected { border-color: var(--cyan); background: var(--cyan-soft); box-shadow: 0 0 0 3px rgba(6,182,212,.09); }
.gate-index { color: var(--faint); font: 800 9px ui-monospace, monospace; letter-spacing: .1em; }
.gate-name { display: block; margin-top: 14px; font-size: 11px; font-weight: 750; }
.gate-state { display: flex; align-items: center; gap: 6px; margin-top: 8px; color: var(--muted); font-size: 9px; text-transform: uppercase; }
.state-dot { width: 7px; height: 7px; border-radius: 50%; background: #98a2b3; }
.state-dot.pass { background: #12b76a; }.state-dot.warning { background: #f79009; }.state-dot.error,.state-dot.blocked { background: #f04438; }.state-dot.running { background: #06b6d4; }
.dossier { display: grid; grid-template-columns: minmax(0,.9fr) minmax(360px,1.1fr); gap: 18px; margin-top: 20px; }
.decision-card { display: grid; gap: 12px; }
.inset { border: 1px solid var(--line); border-radius: 14px; background: #f9fafb; padding: 15px; }
.inset-label { color: var(--faint); font-size: 9px; font-weight: 850; letter-spacing: .13em; text-transform: uppercase; }
.inset p { margin: 9px 0 0; color: #344054; font-size: 12px; line-height: 1.6; }
.manifest { overflow: hidden; border: 1px solid #1d2939; border-radius: 16px; background: var(--navy); color: #e4e7ec; }
.manifest-head { display: flex; justify-content: space-between; gap: 16px; padding: 16px 18px; border-bottom: 1px solid #1d2939; }
.manifest-head .eyebrow { color: #67e8f9; }
.manifest dl { margin: 0; }
.manifest-row { display: grid; grid-template-columns: 140px minmax(0,1fr); gap: 16px; border-bottom: 1px solid #1d2939; padding: 12px 18px; }
.manifest-row:last-child { border-bottom: 0; }
.manifest dt { color: #667085; font: 750 9px ui-monospace,monospace; letter-spacing: .07em; text-transform: uppercase; }
.manifest dd { margin: 0; overflow-wrap: anywhere; color: #d0d5dd; font: 11px/1.5 ui-monospace,monospace; }
.timeline { display: grid; gap: 10px; margin-top: 12px; }
.timeline-item { display: grid; grid-template-columns: 8px minmax(0,1fr); gap: 10px; }
.timeline-line { display: flex; flex-direction: column; align-items: center; }
.timeline-line .state-dot { margin-top: 5px; }
.timeline-line::after { content:""; width: 1px; flex: 1; margin-top: 4px; background: var(--line); }
.timeline-item:last-child .timeline-line::after { display: none; }
.timeline-copy { padding-bottom: 7px; }
.timeline-copy strong { font-size: 11px; text-transform: capitalize; }
.timeline-copy time { float: right; color: var(--faint); font: 9px ui-monospace,monospace; }
.timeline-copy p { margin: 4px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.table-scroll { overflow-x: auto; }
table { width: 100%; min-width: 780px; border-collapse: collapse; font-size: 12px; }
th { background: #f9fafb; color: var(--faint); padding: 11px 15px; text-align: left; font-size: 9px; letter-spacing: .12em; text-transform: uppercase; }
td { border-top: 1px solid #f2f4f7; padding: 14px 15px; color: #344054; }
tbody tr { transition: background .15s ease; }
tbody tr:hover { background: #f0fdfa; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
.right { text-align: right; }
.empty { padding: 52px 20px; color: var(--muted); text-align: center; }
.empty-mark { display: grid; place-items: center; width: 42px; height: 42px; margin: 0 auto 13px; border: 1px solid var(--line); border-radius: 50%; color: var(--faint); }
.notice { border: 1px solid; border-radius: 14px; padding: 15px 18px; font-size: 12px; line-height: 1.55; }
.notice.warning { border-color: #fedf89; background: var(--amber-soft); color: var(--amber); }
.notice.error { border-color: #fecdca; background: var(--rose-soft); color: var(--rose); }
.notice.success { border-color: #abefc6; background: var(--green-soft); color: var(--green); }
.findings { display: grid; gap: 10px; }
.finding { display: grid; grid-template-columns: 30px minmax(0,1fr); gap: 12px; border-left: 4px solid var(--cyan); border-radius: 11px; background: #f9fafb; padding: 14px; }
.finding.critical { border-color: #f04438; background: var(--rose-soft); }
.finding.warning { border-color: #f79009; background: var(--amber-soft); }
.finding-index { color: var(--faint); font: 700 14px ui-monospace,monospace; }
.finding code { font-size: 10px; font-weight: 800; color: #344054; }
.finding p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.55; }
.source-grid { display: grid; gap: 10px; }
.source { border: 1px solid var(--line); border-radius: 13px; padding: 14px; }
.source-top { display: flex; justify-content: space-between; gap: 12px; }
.source h3 { margin: 0; font-size: 12px; text-transform: capitalize; }
.source-meta, .source-path { color: var(--faint); font: 9px/1.5 ui-monospace,monospace; }
.source-meta { margin-top: 5px; }.source-path { overflow-wrap:anywhere; border-top: 1px solid #f2f4f7; margin-top: 12px; padding-top: 10px; }
.distribution { display: grid; gap: 17px; margin: 0; padding: 0; list-style: none; }
.distribution-head { display: flex; align-items: end; justify-content: space-between; gap: 16px; }
.distribution-head span strong, .distribution-head span small { display: block; }
.distribution-head span strong { color: #344054; font-size: 12px; text-transform: capitalize; }
.distribution-head span small { margin-top: 4px; color: var(--muted); font-size: 10px; }
.distribution-head > b { color: var(--ink); font: 700 11px ui-monospace,monospace; }
.distribution-track { overflow: hidden; height: 8px; margin-top: 8px; border-radius: 99px; background: #eaecf0; }
.distribution-track i { display: block; height: 100%; border-radius: inherit; background: var(--cyan); }
.distribution-track i.success { background: #12b76a; }
.distribution-track i.warning { background: #f79009; }
.distribution-track i.danger { background: #f04438; }
.metric-grid.compact .metric-card { min-height: 116px; }
.technical-section { margin-top: 24px; border-top: 1px solid #f2f4f7; padding-top: 22px; }
.technical-section h3 { margin: 6px 0 14px; font-size: 16px; }
.coverage { display: grid; gap: 18px; }
.coverage-head { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; }
.coverage-track { overflow: hidden; height: 7px; margin-top: 8px; border-radius: 99px; background: #eaecf0; }
.coverage-fill { height: 100%; border-radius: inherit; background: var(--cyan); }
.coverage-fill.width-0 { width: 0; }.coverage-fill.width-5 { width: 5%; }
.coverage-fill.width-10 { width: 10%; }.coverage-fill.width-15 { width: 15%; }
.coverage-fill.width-20 { width: 20%; }.coverage-fill.width-25 { width: 25%; }
.coverage-fill.width-30 { width: 30%; }.coverage-fill.width-35 { width: 35%; }
.coverage-fill.width-40 { width: 40%; }.coverage-fill.width-45 { width: 45%; }
.coverage-fill.width-50 { width: 50%; }.coverage-fill.width-55 { width: 55%; }
.coverage-fill.width-60 { width: 60%; }.coverage-fill.width-65 { width: 65%; }
.coverage-fill.width-70 { width: 70%; }.coverage-fill.width-75 { width: 75%; }
.coverage-fill.width-80 { width: 80%; }.coverage-fill.width-85 { width: 85%; }
.coverage-fill.width-90 { width: 90%; }.coverage-fill.width-95 { width: 95%; }
.coverage-fill.width-100 { width: 100%; }
.success-copy { color: var(--green); }
.filters { display: grid; grid-template-columns: 1fr 1fr 1fr 1.5fr auto; gap: 12px; align-items: end; }
.field span { display: block; margin-bottom: 7px; color: var(--faint); font-size: 9px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
.field input, .field select { width: 100%; min-height: 44px; border: 1px solid #d0d5dd; border-radius: 11px; background: white; padding: 8px 11px; color: var(--ink); font-size: 12px; }
.field input:hover, .field select:hover { border-color: #98a2b3; }
.filter-actions { display: flex; gap: 7px; }
.breakdown-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.loading-state { display: grid; gap: 18px; }
.skeleton { position: relative; overflow: hidden; border-radius: 20px; background: #eaecf0; }
.skeleton::after { content:""; position:absolute; inset:0; transform:translateX(-100%); background:linear-gradient(90deg,transparent,#ffffff80,transparent); animation: shimmer 1.4s infinite; }
.hero-skeleton { height: 280px; }.metric-skeleton { height: 136px; }
@keyframes shimmer { 100% { transform: translateX(100%); } }
.nav-scrim { position: fixed; z-index: 25; inset: 0; background: rgba(16,24,40,.5); }
@media (max-width: 1120px) {
.metric-grid { grid-template-columns: repeat(2,1fr); }
.metric-grid.readiness-grid { grid-template-columns: 1fr; }
.two-col, .breakdown-grid, .dossier { grid-template-columns: 1fr; }
.hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; }
.panel-head { flex-wrap: wrap; }
.filters { grid-template-columns: 1fr 1fr; }
.filter-actions { grid-column: 1/-1; }
}
@media (max-width: 960px) {
.sidebar { transform: translateX(-100%); transition: transform .2s ease; box-shadow: 18px 0 50px rgba(16,24,40,.28); }
.sidebar.open { transform: translateX(0); }
.shell { margin-left: 0; }
.menu-button { display: grid; }
.hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; }
.dossier { grid-template-columns: 1fr; }
.top-actions .pill { display: none; }
}
@media (max-width: 560px) {
main { padding: 18px 14px 52px; }
.topbar { padding: 10px 14px; }
.metric-grid, .filters { grid-template-columns: 1fr; }
.hero { border-radius: 20px; padding: 25px 20px; }
.hero h2 { font-size: 31px; }
.panel-head, .panel-body { padding-left: 17px; padding-right: 17px; }
.manifest-row { grid-template-columns: 1fr; gap: 5px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
@media print {
.sidebar, .topbar { display: none; }
.shell { margin: 0; }
main { max-width: none; padding: 0; }
.hero, .metric-card, .panel { box-shadow: none; break-inside: avoid; }
}
@@ -0,0 +1,495 @@
(() => {
"use strict";
const params = new URLSearchParams(location.search);
const suppliedToken = params.get("token");
if (suppliedToken) sessionStorage.setItem("casan_viewer_token", suppliedToken);
const token = suppliedToken || sessionStorage.getItem("casan_viewer_token") || "";
params.delete("token");
history.replaceState({}, "", `${location.pathname}${params.size ? `?${params}` : ""}${location.hash}`);
const state = {
view: params.get("view") || (params.get("trace") ? "runs" : "overview"),
trace: params.get("trace") || "",
project: null,
registry: null,
h6: null,
run: null,
selectedGate: "H1-context",
};
const $ = (selector) => document.querySelector(selector);
const esc = (value) => String(value ?? "")
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll('"', "&quot;").replaceAll("'", "&#039;");
const slug = (value) => String(value || "unknown").toLowerCase().replaceAll("_", "-").replace(/[^a-z0-9-]/g, "");
const text = (value, fallback = "Unavailable") => value === null || value === undefined || value === "" ? fallback : String(value);
const number = (value) => value === null || value === undefined || !Number.isFinite(Number(value))
? "Unavailable" : new Intl.NumberFormat("en-US").format(Number(value));
const time = (value) => value ? String(value).replace("T", " ").replace("Z", " UTC") : "No timestamp";
const money = (value) => value === null || value === undefined ? "Unavailable" : `$${Number(value).toFixed(6)}`;
const statusLabel = (value) => String(value || "unknown").replaceAll("_", " ");
const pill = (value) => `<span class="pill ${slug(value)}">${esc(String(value || "unknown").replaceAll("_", " "))}</span>`;
async function api(path) {
const response = await fetch(path, {
headers: { "X-CASAN-Viewer-Token": token },
cache: "no-store",
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}
function downloadUrl(path, query = {}) {
const next = new URLSearchParams({ ...query, token });
return `${path}?${next}`;
}
function hero({ eyebrow, title, description, verdict, meta = [], actions = "" }) {
return `<article class="hero">
<div class="hero-grid">
<div>
<p class="eyebrow">${esc(eyebrow)}</p>
<h2>${esc(title)}</h2>
<p class="hero-description">${esc(description)}</p>
<div class="hero-meta">${meta.map((item) => `<span>${esc(item)}</span>`).join("")}</div>
</div>
<div class="hero-actions">${verdict ? pill(verdict) : ""}${actions}</div>
</div>
</article>`;
}
function metric(label, value, detail) {
return `<article class="metric-card"><div class="metric-label">${esc(label)}</div>
<div class="metric-value">${esc(text(value))}</div><div class="metric-detail">${esc(detail)}</div></article>`;
}
function panel(eyebrow, title, description, body, right = "") {
return `<article class="panel"><header class="panel-head"><div><p class="eyebrow">${esc(eyebrow)}</p>
<h2>${esc(title)}</h2><p class="panel-description">${esc(description)}</p></div>${right}</header>
<div class="panel-body">${body}</div></article>`;
}
function disclosure(summary, description, body, badge = "") {
return `<details class="disclosure-panel"><summary><span><strong>${esc(summary)}</strong><small>${esc(description)}</small></span>
<span class="disclosure-side">${badge}<b aria-hidden="true">+</b></span></summary><div class="disclosure-body">${body}</div></details>`;
}
function distribution(label, rows) {
const maximum = Math.max(...rows.map((row) => Number(row.value) || 0), 1);
return `<ol class="distribution" role="img" aria-label="${esc(label)}">${rows.map((row) => {
const value = Math.max(Number(row.value) || 0, 0);
const width = value === 0 ? 0 : Math.max((value / maximum) * 100, 3);
return `<li><div class="distribution-head"><span><strong>${esc(row.label)}</strong><small>${esc(row.detail || "")}</small></span><b>${esc(number(value))}</b></div>
<div class="distribution-track" aria-hidden="true"><i class="${slug(row.tone || "info")}" style="width:${width}%"></i></div></li>`;
}).join("")}</ol>`;
}
function empty(title, detail) {
return `<div class="empty"><div class="empty-mark">∅</div><strong>${esc(title)}</strong><p>${esc(detail)}</p></div>`;
}
function setView(view, push = true) {
state.view = ["overview", "runs", "h6"].includes(view) ? view : "overview";
document.querySelectorAll(".nav-item").forEach((item) => {
const active = item.dataset.view === state.view;
item.classList.toggle("active", active);
if (active) item.setAttribute("aria-current", "page");
else item.removeAttribute("aria-current");
});
document.querySelectorAll(".view").forEach((item) => item.classList.add("hidden"));
$(`#${state.view}-view`).classList.remove("hidden");
$("#page-title").textContent = {
overview: "Assurance overview", runs: "Run assurance registry", h6: "H6 AgentOps dossier",
}[state.view];
if (push) {
const next = new URL(location.href);
next.searchParams.set("view", state.view);
if (state.trace) next.searchParams.set("trace", state.trace);
else next.searchParams.delete("trace");
history.pushState({}, "", `${next.pathname}?${next.searchParams}`);
}
if (state.view === "runs") renderRuns();
if (state.view === "h6") renderH6();
closeNav();
}
function renderOverview() {
const profile = state.project;
const registry = state.registry;
const latest = registry.latest || {};
const recent = registry.runs || [];
const failures = recent.filter((row) => row.status === "failed").length;
const certified = latest.certified === true;
const latestTrace = latest.trace_id || recent[0]?.trace_id;
const maturity = profile.maturity || {};
const readiness = profile.readiness || {};
const dimensions = readiness.dimensions || {};
const core = dimensions.core || { status: "not_assessed", summary: "Core readiness has not been assessed." };
const domain = dimensions.domain_pipeline || { status: "not_assessed", summary: "Domain Pipeline readiness has not been assessed." };
const telemetry = dimensions.provider_telemetry || { status: "not_assessed", summary: "Provider Telemetry readiness has not been assessed." };
const nextActions = readiness.next_actions || [];
const coreReady = ["ready", "ready_with_attention"].includes(core.status);
const readinessActionList = `<div class="findings">${nextActions.map((action, index) => `<div class="finding warning">
<span class="finding-index">${String(index + 1).padStart(2, "0")}</span>
<div><code>${esc(action.code)}</code><p>${esc(action.message)}</p></div>
</div>`).join("")}</div>`;
const readinessCards = `<section class="metric-grid readiness-grid" aria-label="Commercial readiness">
${metric("Core", statusLabel(core.status), core.summary)}
${metric("Domain Pipeline", statusLabel(domain.status), domain.summary)}
${metric("Provider Telemetry", statusLabel(telemetry.status), telemetry.summary)}
</section>`;
const actions = latestTrace
? `<button class="button primary" type="button" data-open-trace="${esc(latestTrace)}">Inspect latest run</button>
<button class="button secondary" type="button" data-open-h6="${esc(latestTrace)}">Open H6</button>`
: `<button class="button secondary" type="button" data-view-target="runs">View registry</button>`;
$("#overview-view").innerHTML =
hero({
eyebrow: "CASAN Core · Local assurance",
title: latestTrace ? "One prompt. One evidence trail." :
coreReady ? "Core reporting is ready." : "Finish Core activation.",
description: latestTrace
? "A read-only, offline-capable assurance workspace reconstructed from canonical project evidence. No Platform service or HTML generation on the prompt hot path."
: coreReady
? "Core visual reporting works without a Domain Pack or application-source changes. Send a governed prompt to create the first run dossier."
: "Core visual reporting works without a Domain Pack or application-source changes. Complete the activation action below, then send a governed prompt.",
verdict: latestTrace ? (certified ? "certified" : "review required") : core.status,
meta: [
`${profile.project_name} · ${profile.edition}`,
latestTrace ? `Trace ${latestTrace}` : "No finalized trace yet",
`Maturity ${maturity.level == null ? "not assessed" : `L${maturity.level}`} · ${statusLabel(maturity.status)}`,
],
actions,
}) + readinessCards +
(latestTrace && nextActions.length ? panel(
"Activation attention",
"A selected client route needs action.",
"Healthy routes remain operational; complete this step before relying on the affected integration.",
readinessActionList,
pill(core.status),
) : "") +
(latestTrace ? `<section class="metric-grid">
${metric("Governed records", number(registry.count), `${recent.length} recent records loaded`)}
${metric("Latest H6 quality", latest.telemetry_quality || "Unavailable", "Missing values are disclosed, never coerced to zero")}
${metric("Observed failures", number(failures), `${recent.length ? Math.round((failures / recent.length) * 100) : 0}% of loaded records`)}
${metric("Evidence gates", latestTrace ? "H1 → H7" : "Waiting", latestTrace ? "Control-by-control reconstruction available" : "Evidence appears after a prompt finalizes")}
</section>` : panel(
"First-run onboarding",
"No operational KPIs are shown until evidence exists.",
"CASAN does not turn missing telemetry into zeroes or claim a run is certified before a governed prompt finishes.",
`${nextActions.length ? `<div class="onboarding-actions">${readinessActionList}</div>` : ""}
<div class="two-col">
<div class="notice success"><strong>Available now</strong><br>Local viewer, H1–H7 run dossier, history, H6 AgentOps and on-demand JSON/HTML export.</div>
<div class="notice warning"><strong>Optional expansion</strong><br>Install a Domain Pack only when the team needs project-specific SRS→test acceptance.</div>
</div>`
)) +
disclosure(
"About this local report",
"Edition boundaries and implementation notes are hidden until needed.",
`<div class="two-col">
<div class="notice success"><strong>Included in Core</strong><br>Latest run, history, interactive H1–H7 evidence, H6 AgentOps, and on-demand JSON/HTML export.</div>
<div class="notice warning"><strong>Platform boundary</strong><br>Multi-project fleet views, centralized RBAC, shared approvals, ingestion and organization-wide retention.</div>
</div>`
) +
recentRunsPanel(recent.slice(0, 6), "Latest governed evidence");
bindActions();
}
function recentRunsPanel(rows, title = "Recent governed runs") {
const body = rows.length ? `<div class="table-scroll"><table>
<thead><tr><th>Observed at</th><th>Lifecycle step</th><th>Verdict</th><th class="right">Latency</th><th class="right">Tokens</th><th class="right">Evidence</th></tr></thead>
<tbody>${rows.map((row) => `<tr>
<td class="mono">${esc(time(row.timestamp))}</td><td><strong>${esc(row.step || row.harness || "Agentic turn")}</strong></td>
<td>${pill(row.status)}</td><td class="right mono">${esc(row.latency_ms == null ? "Unavailable" : `${number(row.latency_ms)} ms`)}</td>
<td class="right mono">${esc(number(row.total_tokens))}</td>
<td class="right">${row.trace_id ? `<button class="button light" type="button" data-open-trace="${esc(row.trace_id)}">Inspect H1–H7</button>` : "—"}</td>
</tr>`).join("")}</tbody></table></div>` : empty("No governed run yet", "Complete a prompt in a configured client, then refresh this viewer.");
return panel("Evidence index", title, "Operational values are shown only when present in canonical telemetry.", body);
}
function renderRuns() {
const rows = state.registry.runs || [];
const selected = state.run;
const hasRuns = rows.length > 0;
const body = hero({
eyebrow: "Run assurance registry",
title: hasRuns ? "Every governed run, one evidence trail." : "No governed runs yet.",
description: hasRuns
? "Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent dossier."
: "The registry will populate after a configured client finalizes its first governed prompt.",
verdict: hasRuns
? (rows.some((row) => row.status === "failed") ? "attention" : "operational")
: "waiting",
meta: hasRuns
? [`${state.registry.count} records`, `${rows.filter((row) => row.status === "failed").length} failures`, "Local canonical evidence"]
: ["No operational counters yet", "Local canonical evidence"],
actions: hasRuns
? `<button class="button primary" type="button" data-view-target="h6">Open H6 dossier</button>`
: "",
}) + (selected ? renderTrace(selected) : "") + recentRunsPanel(rows);
$("#runs-view").innerHTML = body;
bindActions();
bindGates();
}
function renderTrace(report) {
const gates = report.gates || [];
let selected = gates.find((gate) => gate.id === state.selectedGate) || gates[0];
if (!selected) return "";
const rail = gates.map((gate, index) => `<button type="button" aria-pressed="${gate.id === selected.id}" class="gate-button ${gate.id === selected.id ? "selected" : ""}" data-gate="${esc(gate.id)}">
<span class="gate-index">0${index + 1}</span><span class="gate-name">${esc(gate.title)}</span>
<span class="gate-state"><span class="state-dot ${slug(gate.status)}"></span>${esc(gate.status)}</span></button>`).join("");
const events = (selected.events || []).slice(-4).reverse();
const timeline = events.length ? `<div class="timeline">${events.map((event) => `<div class="timeline-item">
<div class="timeline-line"><span class="state-dot ${slug(event.status)}"></span></div>
<div class="timeline-copy"><strong>${esc(event.status)}</strong><time>${esc(time(event.timestamp))}</time><p>${esc(event.reason)}</p></div>
</div>`).join("")}</div>` : `<p class="metric-detail">No individual event was emitted for this control.</p>`;
const entries = Object.entries(selected.evidence || {});
const manifest = entries.length ? `<dl>${entries.map(([key, value]) => `<div class="manifest-row"><dt>${esc(key)}</dt><dd>${esc(typeof value === "string" ? value : JSON.stringify(value))}</dd></div>`).join("")}</dl>`
: empty("No safe evidence fields", "This control has not emitted a sanitized evidence manifest.");
const actions = `<a class="button secondary" href="${downloadUrl(`/api/v1/reports/run/${encodeURIComponent(report.trace_id)}/export`, { format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl(`/api/v1/reports/run/${encodeURIComponent(report.trace_id)}/export`, { format: "html" })}">Export dossier</a>`;
return `<section id="run-dossier" class="run-dossier-anchor" tabindex="-1">${panel(
"Run assurance",
"Governed execution dossier",
"A control-by-control reconstruction backed by sanitized lifecycle evidence.",
`<div class="hero trace-hero">
<div class="hero-grid"><div><p class="eyebrow">Trace provenance</p><h2 class="trace-title">${esc(report.trace_id)}</h2>
<div class="hero-meta trace-meta"><span>${report.summary.gates_observed}/7 controls observed</span><span>${report.project.edition} edition</span></div></div>
<div>${pill(report.verdict)}</div></div></div>
<div class="rail-wrap rail-spaced"><div class="evidence-rail">${rail}</div></div>
<div class="dossier">
<div class="decision-card"><div><p class="eyebrow">Selected control</p><h2>${esc(selected.title)}</h2><p class="panel-description">${esc(selected.description)}</p></div>
<div class="inset"><span class="inset-label">Latest decision · ${esc(time(selected.updated_at))}</span><p>${esc(selected.reason)}</p></div>
${disclosure("Event history", `${events.length} recent lifecycle events · hidden by default`, timeline)}
</div>
${disclosure("Safe evidence manifest", "Sanitized fields persisted by the harness · hidden by default", manifest, pill(`${entries.length} fields`))}
</div>`,
actions,
)}</section>`;
}
function renderH6() {
const data = state.h6;
if (!data) return;
const hasRuns = Number(data.summary?.runs) > 0;
const query = data.scope || {};
const queryObject = Object.fromEntries(Object.entries(query).filter(([, value]) => value));
const actions = `<a class="button secondary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "html" })}">Export dossier</a>`;
if (!hasRuns) {
const provider = state.project?.readiness?.dimensions?.provider_telemetry || {};
$("#h6-view").innerHTML =
hero({
eyebrow: "H6 · AgentOps assurance",
title: "H6 begins with the first governed run.",
description: "No runtime record exists yet, so CASAN hides failure, latency, token and cost counters instead of presenting fabricated zeroes.",
verdict: "waiting",
meta: [data.report_id, `Generated ${time(data.generated_at)}`, "No operational evidence yet"],
actions: "",
}) +
panel(
"H6 onboarding",
"Provider telemetry is optional.",
provider.summary || "Provider token and cost telemetry has not been observed; this does not block Core.",
`<div class="two-col">
<div class="notice success"><strong>Core remains ready</strong><br>Send a prompt through a configured client. CASAN will persist runtime evidence automatically.</div>
<div class="notice warning"><strong>No automatic export</strong><br>Open H6 or export a dossier only when a review artifact is needed.</div>
</div>`,
);
return;
}
const findings = data.findings.length ? `<div class="findings">${data.findings.map((item, index) => `<article class="finding ${slug(item.severity)}">
<span class="finding-index">${String(index + 1).padStart(2, "0")}</span><div><code>${esc(item.code)}</code><p>${esc(item.message)}</p></div></article>`).join("")}</div>`
: `<div class="notice success">No threshold breach was detected in the selected evidence boundary.</div>`;
const sources = data.evidence_sources.map((source) => {
const status = !source.present ? "missing" : source.stale ? "stale" : "fresh";
return `<article class="source"><div class="source-top"><div><h3>${esc(source.source)}</h3><div class="source-meta">${source.records} records · ${source.age_s == null ? "no timestamp" : `${number(source.age_s)}s old`}</div></div>${pill(status)}</div>
<div class="source-path">${esc(source.path)}</div></article>`;
}).join("");
const breakdown = (rows, subject) => `<div class="table-scroll"><table><thead><tr><th>${esc(subject)}</th><th class="right">Runs</th><th class="right">Failures</th><th class="right">Avg latency</th><th class="right">Tokens</th><th class="right">Cost</th></tr></thead>
<tbody>${rows.length ? rows.map((row) => `<tr><td><strong>${esc(row.key)}</strong></td><td class="right mono">${row.runs}</td><td class="right mono">${row.failures}</td><td class="right mono">${number(row.latency_avg_ms)} ms</td><td class="right mono">${number(row.tokens)}</td><td class="right mono">${money(row.cost_usd)}</td></tr>`).join("") : `<tr><td colspan="6">${empty("No matching records", `No ${subject.toLowerCase()} evidence is available.`)}</td></tr>`}</tbody></table></div>`;
const outcomes = distribution("Governed run outcome distribution", data.details.by_status.map((row) => ({
label: String(row.status).replaceAll("_", " "),
value: row.count,
detail: `${data.summary.runs ? Math.round((row.count / data.summary.runs) * 100) : 0}% of selected runs`,
tone: row.status === "failed" ? "danger" : row.status === "degraded" ? "warning" : "success",
})));
$("#h6-view").innerHTML =
hero({
eyebrow: "H6 · AgentOps assurance",
title: "Operational evidence, ready for a decision.",
description: "Runtime health, reliability, cost provenance and telemetry integrity—designed to answer what happened, why it matters and where the evidence lives.",
verdict: data.verdict,
meta: [data.report_id, `Generated ${time(data.generated_at)}`, `Freshness ${data.freshness.status}`],
actions,
}) +
disclosure("Filter the evidence boundary", "Project, date and trace filters stay out of the executive view until needed.", h6Filters(data)) +
`<section class="metric-grid">
${metric("Governed runs", number(data.summary.runs), `${data.summary.success} successful · ${data.summary.degraded} degraded`)}
${metric("Failure rate", `${data.summary.failure_rate_pct}%`, `${data.summary.failed} failed · ${data.summary.alerts} alerts`)}
${metric("P95 latency", `${number(data.summary.latency_ms.p95)} ms`, `P50 ${number(data.summary.latency_ms.p50)} ms`)}
${metric("Telemetry integrity", data.data_quality.status.replaceAll("_", " "), `${data.summary.coverage.token_pct}% token · ${data.summary.coverage.cost_pct}% cost`)}
</section>` +
`<div class="two-col">
${panel("Decision brief", "What requires attention", "Threshold findings are review signals, not maturity claims.", findings, pill(data.verdict))}
${panel("Outcome mix", "Run distribution", "Canonical runtime status values, normalized against the largest observed group.", outcomes, `<span class="mono metric-detail">${number(data.summary.runs)} total</span>`)}
</div>` +
panel("Evidence quality", "Coverage and disclosure", "Unavailable telemetry remains null. Coverage gaps stay visible.", `<div class="two-col">
<div class="coverage">${coverage("Token attribution", data.summary.coverage.token_pct, `${data.summary.coverage.token_records}/${data.summary.coverage.runtime_records} records`)}
${coverage("Cost attribution", data.summary.coverage.cost_pct, `${data.summary.coverage.cost_records}/${data.summary.coverage.runtime_records} records`)}</div>
<div class="inset"><span class="inset-label">Disclosure notes</span>${data.data_quality.warnings.length ? `<ul>${data.data_quality.warnings.map((warning) => `<li class="metric-detail">${esc(warning)}</li>`).join("")}</ul>` : `<p class="success-copy">All required sources are present with no estimation warning.</p>`}</div>
</div>`, pill(data.data_quality.status)) +
disclosure(
"Technical evidence and breakdowns",
"Provider counters, canonical paths and raw tables are preserved for audit without crowding the decision view.",
`<section class="metric-grid compact">
${metric("Provider tokens", data.summary.coverage.token_records > 0 ? number(data.summary.tokens.provider_total ?? data.summary.tokens.total) : "Unavailable", `${data.summary.coverage.token_pct}% record coverage`)}
${metric("Actual cost", money(data.summary.cost_usd.provider_actual), `${data.summary.coverage.cost_pct}% record coverage`)}
${metric("Provider calls", number(data.summary.provider_calls), "Provider-attributed requests")}
${metric("Retries", number(data.summary.retries), "Observed reroute attempts")}
</section>
<div class="technical-section"><p class="eyebrow">Source integrity</p><h3>Evidence provenance</h3><div class="source-grid">${sources}</div></div>
<div class="breakdown-grid technical-section">
${panel("Runtime", "Execution profile by step", "Latency, failure and attribution by governed lifecycle step.", breakdown(data.details.by_step, "Step"))}
${panel("Provider", "Model usage and provenance", "Only provider-reported token and actual cost values are shown.", breakdown(data.details.by_provider, "Provider · model"))}
</div>`,
pill(data.freshness.status),
);
bindH6Filters();
}
function coverage(label, value, detail) {
const width = Math.round(Math.max(0, Math.min(100, Number(value) || 0)) / 5) * 5;
return `<div><div class="coverage-head"><strong>${esc(label)}</strong><span>${esc(value)}%</span></div>
<div class="coverage-track"><div class="coverage-fill width-${width}"></div></div>
<div class="metric-detail">${esc(detail)}</div></div>`;
}
function h6Filters(data) {
const scope = data.scope || {};
const options = data.available_filters || { projects: [], runs: [] };
const date = (value) => value ? value.slice(0, 10) : "";
return `<form id="h6-filters" class="filters">
<label class="field"><span>Project</span><select name="project"><option value="">All projects</option>${options.projects.map((value) => `<option value="${esc(value)}" ${scope.project === value ? "selected" : ""}>${esc(value)}</option>`).join("")}</select></label>
<label class="field"><span>From</span><input type="date" name="from" value="${esc(date(scope.from))}"></label>
<label class="field"><span>To</span><input type="date" name="to" value="${esc(date(scope.to))}"></label>
<label class="field"><span>Run / trace</span><input name="run" list="run-options" value="${esc(scope.run || "")}" placeholder="All governed runs"><datalist id="run-options">${options.runs.map((value) => `<option value="${esc(value)}"></option>`).join("")}</datalist></label>
<div class="filter-actions"><button class="button primary" type="submit">Apply</button><button class="button light" type="button" id="clear-h6">Clear</button></div>
</form>`;
}
async function loadTrace(traceId) {
if (!traceId) return;
state.trace = traceId;
state.selectedGate = "H1-context";
$("#loading").classList.remove("hidden");
try {
state.run = await api(`/api/v1/reports/run/${encodeURIComponent(traceId)}`);
setView("runs");
const dossier = $("#run-dossier");
if (dossier) {
requestAnimationFrame(() => {
dossier.focus({ preventScroll: true });
dossier.scrollIntoView({
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
block: "start",
});
});
}
} catch (error) {
showError(`Run report could not be reconstructed: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
async function loadH6(query = {}) {
const next = new URLSearchParams(Object.fromEntries(Object.entries(query).filter(([, value]) => value)));
$("#loading").classList.remove("hidden");
try {
state.h6 = await api(`/api/v1/reports/h6${next.size ? `?${next}` : ""}`);
renderH6();
} catch (error) {
showError(`H6 report could not be generated: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
function bindActions() {
document.querySelectorAll("[data-view-target]").forEach((button) => button.addEventListener("click", () => setView(button.dataset.viewTarget)));
document.querySelectorAll("[data-open-trace]").forEach((button) => button.addEventListener("click", () => loadTrace(button.dataset.openTrace)));
document.querySelectorAll("[data-open-h6]").forEach((button) => button.addEventListener("click", async () => {
await loadH6({ run: button.dataset.openH6 });
setView("h6");
}));
}
function bindGates() {
document.querySelectorAll("[data-gate]").forEach((button) => button.addEventListener("click", () => {
state.selectedGate = button.dataset.gate;
renderRuns();
}));
}
function bindH6Filters() {
$("#h6-filters")?.addEventListener("submit", async (event) => {
event.preventDefault();
const values = Object.fromEntries(new FormData(event.currentTarget));
await loadH6(values);
});
$("#clear-h6")?.addEventListener("click", () => loadH6());
}
function showError(message) {
$("#error").textContent = message;
$("#error").classList.remove("hidden");
}
function closeNav() {
$(".sidebar").classList.remove("open");
$("#nav-scrim").classList.add("hidden");
$("#menu-button").setAttribute("aria-expanded", "false");
}
async function loadAll() {
$("#error").classList.add("hidden");
$("#loading").classList.remove("hidden");
try {
[state.project, state.registry, state.h6] = await Promise.all([
api("/api/v1/project"),
api("/api/v1/runs?limit=100"),
api("/api/v1/reports/h6?limit=50"),
]);
$("#freshness-pill").textContent = `${state.h6.freshness.status} evidence`;
$("#freshness-pill").className = `pill ${slug(state.h6.freshness.status)}`;
renderOverview();
renderH6();
if (state.trace) {
await loadTrace(state.trace);
} else {
setView(state.view, false);
}
} catch (error) {
showError(`Local evidence could not be loaded: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
document.querySelectorAll(".nav-item").forEach((button) => button.addEventListener("click", () => setView(button.dataset.view)));
$("#refresh-button").addEventListener("click", loadAll);
$("#menu-button").addEventListener("click", () => {
const open = !$(".sidebar").classList.contains("open");
$(".sidebar").classList.toggle("open", open);
$("#nav-scrim").classList.toggle("hidden", !open);
$("#menu-button").setAttribute("aria-expanded", String(open));
});
$("#nav-scrim").addEventListener("click", closeNav);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeNav();
});
window.addEventListener("popstate", () => setView(new URLSearchParams(location.search).get("view") || "overview", false));
loadAll();
})();
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="16" fill="#101828"/>
<rect x="10" y="10" width="44" height="44" rx="12" fill="#172033" stroke="#344054"/>
<path d="M39.5 22.5a14 14 0 1 0 0 19" fill="none" stroke="#67e8f9" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#101828" />
<title>CASAN · Local Assurance Viewer</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/app.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to report</a>
<div id="app">
<aside class="sidebar" aria-label="Primary navigation">
<a class="brand" href="#overview" aria-label="CASAN overview">
<span class="brand-mark" aria-hidden="true">C</span>
<span><strong>CASAN</strong><small>Local Assurance</small></span>
</a>
<nav>
<button class="nav-item active" data-view="overview" type="button"><span aria-hidden="true">◫</span>Overview</button>
<button class="nav-item" data-view="runs" type="button"><span aria-hidden="true">⌁</span>Run registry</button>
<button class="nav-item" data-view="h6" type="button"><span aria-hidden="true">H6</span>AgentOps</button>
</nav>
<div class="local-seal">
<span class="live-dot"></span>
<div><strong>Local Core</strong><small>Loopback · read-only</small></div>
</div>
</aside>
<div class="shell">
<header class="topbar">
<button id="menu-button" class="icon-button menu-button" type="button" aria-label="Open navigation" aria-expanded="false">☰</button>
<div>
<p class="top-eyebrow">Independent evidence workspace</p>
<h1 id="page-title">Assurance overview</h1>
</div>
<div class="top-actions">
<span id="freshness-pill" class="pill neutral">Evidence loading</span>
<button id="refresh-button" class="icon-button" type="button" aria-label="Refresh evidence">↻</button>
</div>
</header>
<main id="main" tabindex="-1">
<div id="loading" class="loading-state" aria-live="polite">
<div class="skeleton hero-skeleton"></div>
<div class="metric-grid">
<div class="skeleton metric-skeleton"></div><div class="skeleton metric-skeleton"></div>
<div class="skeleton metric-skeleton"></div><div class="skeleton metric-skeleton"></div>
</div>
</div>
<div id="error" class="notice error hidden" role="alert"></div>
<section id="overview-view" class="view hidden" aria-labelledby="page-title"></section>
<section id="runs-view" class="view hidden" aria-labelledby="page-title"></section>
<section id="h6-view" class="view hidden" aria-labelledby="page-title"></section>
</main>
</div>
<div id="nav-scrim" class="nav-scrim hidden"></div>
</div>
<script src="/app.js" defer></script>
</body>
</html>
@@ -0,0 +1,78 @@
{
"schema_version": "1.0.0",
"description": "Deterministic action classes and minimum risk floors shared by CASAN runtimes.",
"risk_order": ["low", "medium", "high", "critical"],
"classes": {
"read_only": {"risk_floor": "low", "side_effect_level": "none", "requires_approval": false, "actor_required": false, "evidence_required": false},
"write": {"risk_floor": "medium", "side_effect_level": "write", "requires_approval": false, "actor_required": false, "evidence_required": true},
"delete": {"risk_floor": "high", "side_effect_level": "destructive", "requires_approval": true, "actor_required": true, "evidence_required": true},
"database_mutation": {"risk_floor": "high", "side_effect_level": "write", "requires_approval": true, "actor_required": true, "evidence_required": true},
"migration": {"risk_floor": "high", "side_effect_level": "write", "requires_approval": true, "actor_required": true, "evidence_required": true},
"deployment": {"risk_floor": "high", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
"release": {"risk_floor": "high", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
"credential_access": {"risk_floor": "high", "side_effect_level": "sensitive_read", "requires_approval": true, "actor_required": true, "evidence_required": true},
"identity_permission_modification": {"risk_floor": "high", "side_effect_level": "write", "requires_approval": true, "actor_required": true, "evidence_required": true},
"external_network_side_effect": {"risk_floor": "medium", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
"infrastructure_modification": {"risk_floor": "high", "side_effect_level": "external", "requires_approval": true, "actor_required": true, "evidence_required": true},
"unknown": {"risk_floor": "high", "side_effect_level": "unknown", "requires_approval": true, "actor_required": true, "evidence_required": true}
},
"action_aliases": {
"agent_step": "read_only",
"read": "read_only",
"search": "read_only",
"write": "write",
"write_code": "write",
"write_file": "write",
"delete": "delete",
"delete_file": "delete",
"db_write": "database_mutation",
"database_mutation": "database_mutation",
"migration": "migration",
"migrate": "migration",
"deploy": "deployment",
"deployment": "deployment",
"launch": "deployment",
"release": "release",
"credential_access": "credential_access",
"secret_access": "credential_access",
"permission_change": "identity_permission_modification",
"identity_change": "identity_permission_modification",
"external_api": "external_network_side_effect",
"infrastructure_change": "infrastructure_modification"
},
"tool_aliases": {
"read": "read_only",
"grep": "read_only",
"glob": "read_only",
"search": "read_only",
"view": "read_only",
"codegraph_search": "read_only",
"codegraph_node": "read_only",
"codegraph_context": "read_only",
"edit": "write",
"write": "write",
"multiedit": "write",
"notebookedit": "write",
"apply_patch": "write",
"str_replace_editor": "write",
"create_file": "write",
"update_file": "write",
"delete_file": "delete",
"webfetch": "external_network_side_effect",
"web_fetch": "external_network_side_effect",
"browser": "external_network_side_effect",
"sendmessage": "external_network_side_effect"
},
"command_patterns": [
{"pattern": "(^|\\s)(kubectl|helm)(\\s|$).*(apply|delete|upgrade|install)|(^|\\s)terraform\\s+(apply|destroy)", "class": "infrastructure_modification"},
{"pattern": "(^|\\s)(deploy|deployment|release)(\\s|$)|git\\s+push", "class": "deployment"},
{"pattern": "(^|\\s)(prisma|alembic|flyway|liquibase|rails)\\s+.*(migrat|upgrade)|\\bmigration\\b", "class": "migration"},
{"pattern": "\\b(delete|insert|update|alter|drop|truncate)\\s+(from|into|table|database)|\\b(db_write|database_mutation)\\b", "class": "database_mutation"},
{"pattern": "(^|\\s)(rm|rmdir|unlink)\\s|delete_file", "class": "delete"},
{"pattern": "(\\.ssh/|id_rsa|id_ed25519|\\.aws/credentials|\\.env($|\\s)|secret|credential|api[_-]?key)", "class": "credential_access"},
{"pattern": "\\b(iam|chmod|chown|setfacl|role|permission)\\b.*\\b(add|create|delete|grant|modify|remove|set|update)\\b", "class": "identity_permission_modification"},
{"pattern": "(^|\\s)(curl|wget|scp|sftp|ssh|nc|ncat)\\s", "class": "external_network_side_effect"},
{"pattern": "(^|\\s)(cp|mv|mkdir|touch|tee|sed\\s+-i)\\s|(^|\\s)(npm|pnpm|yarn|pip|cargo|go)\\s+(install|add|get)\\b|(^|\\s)(cat|printf|echo).*(>|>>)", "class": "write"},
{"pattern": "^\\s*(ls|pwd|cat|head|tail|wc|rg|grep|find|stat|git\\s+(status|diff|log|show))\\b", "class": "read_only"}
]
}
+25
View File
@@ -0,0 +1,25 @@
"""Framework-independent CASAN Assurance Kernel contracts and policy primitives."""
from .adapters import AgenticBridgeAdapter, NativeHarnessAdapter
from .contracts import CONTRACT_VERSION, validate_bundle
from .policy import (
classify_action,
evaluate_failure_policy,
evaluate_h2_gate,
evaluate_registry_configuration,
evaluate_risk,
evaluate_trust_capabilities,
)
__all__ = [
"AgenticBridgeAdapter",
"CONTRACT_VERSION",
"NativeHarnessAdapter",
"classify_action",
"evaluate_failure_policy",
"evaluate_h2_gate",
"evaluate_registry_configuration",
"evaluate_risk",
"evaluate_trust_capabilities",
"validate_bundle",
]
+194
View File
@@ -0,0 +1,194 @@
"""Compatibility adapters from existing runtimes into the canonical contract."""
from __future__ import annotations
from typing import Any, Mapping
from .contracts import CONTRACT_VERSION, actor, evidence_digest, outcome, utc_now
from .policy import evaluate_risk
class NativeHarnessAdapter:
runtime_type = "casan-native-harness"
def map_execution(self, event: Mapping[str, Any]) -> dict[str, Any]:
run_id = str(event.get("run_id") or event.get("trace_id") or "")
actor_id = str(event.get("actor") or "unidentified")
action_name = str(event.get("action") or "agent_step")
mode = str(event.get("mode") or "enforce")
risk = evaluate_risk(
action=action_name,
tool=str(event.get("tool") or action_name),
resource=str(event.get("resource") or ""),
command=str(event.get("command") or ""),
actor=actor_id if actor_id != "unidentified" else "",
environment=str(event.get("environment") or "development"),
)
execution_status = str(event.get("execution_status") or "unknown")
assurance_status = str(event.get("assurance_status") or "unknown")
certification = str(event.get("certification_status") or "non_certified")
return _bundle(
run_id=run_id,
runtime_type=self.runtime_type,
runtime_version=str(event.get("runtime_version") or "legacy-compatible"),
mode=mode,
actor_id=actor_id,
action_name=action_name,
risk=risk,
execution_status=execution_status,
assurance_status=assurance_status,
certification=certification,
event=event,
)
class AgenticBridgeAdapter:
runtime_type = "agentic-bridge"
def map_execution(self, event: Mapping[str, Any]) -> dict[str, Any]:
run_id = str(event.get("trace_id") or event.get("run_id") or "")
actor_id = str(event.get("actor") or "unidentified")
action_name = str(event.get("action") or event.get("last_action") or "agent_step")
mode = str(event.get("mode") or event.get("hook_trust_mode") or "observe")
risk = evaluate_risk(
action=action_name,
tool=str(event.get("tool") or event.get("last_tool") or action_name),
resource=str(event.get("resource") or ""),
command=str(event.get("command") or ""),
actor=actor_id if actor_id != "unidentified" else "",
environment=str(event.get("environment") or "development"),
)
execution_status = str(event.get("execution_status") or "unknown")
assurance_status = str(event.get("assurance_status") or "unknown")
certification = "certified" if event.get("certified") else "non_certified"
return _bundle(
run_id=run_id,
runtime_type=self.runtime_type,
runtime_version=str(event.get("adapter_version") or "legacy-compatible"),
mode=mode,
actor_id=actor_id,
action_name=action_name,
risk=risk,
execution_status=execution_status,
assurance_status=assurance_status,
certification=certification,
event=event,
)
def _bundle(
*,
run_id: str,
runtime_type: str,
runtime_version: str,
mode: str,
actor_id: str,
action_name: str,
risk: Mapping[str, Any],
execution_status: str,
assurance_status: str,
certification: str,
event: Mapping[str, Any],
) -> dict[str, Any]:
step_id = str(event.get("step_id") or f"{run_id}:1")
correlation_id = str(event.get("correlation_id") or run_id)
raw_evidence = list(event.get("evidence") or [])
claim_id = f"{run_id}:assurance-claim"
evidence = []
for index, item in enumerate(raw_evidence, 1):
raw = item if isinstance(item, Mapping) else {"detail": str(item)}
canonical_evidence = {
"evidence_id": str(raw.get("evidence_id") or f"{run_id}:evidence:{index}"),
"claim_refs": [claim_id],
"producer_identity": runtime_type,
"timestamp": raw.get("at") or raw.get("timestamp") or utc_now(),
"artifact_ref": str(raw.get("artifact_ref") or f"inline:{run_id}:{index}"),
"validation_status": "valid" if raw.get("decision") in {"allow", "pass", "passed", "certified"} else "unverified",
"retention_class": str(raw.get("retention_class") or "runtime_assurance"),
"category": str(raw.get("category") or "runtime_control"),
}
canonical_evidence["integrity"] = {
"algorithm": "sha256",
"digest": evidence_digest(canonical_evidence),
"source_content_hash": raw.get("content_hash"),
}
evidence.append(canonical_evidence)
policy_decisions = []
for item in list(event.get("policy_decisions") or []):
if not isinstance(item, Mapping):
continue
decision = str(item.get("decision") or "observe_only")
if decision not in {"allow", "deny", "quarantine", "require_approval", "transform", "observe_only"}:
decision = "observe_only"
policy_decisions.append({
"policy_id": str(item.get("policy_id") or "casan.compatibility.policy"),
"policy_version": str(item.get("policy_version") or item.get("schema_version") or CONTRACT_VERSION),
"decision": decision,
"reason_codes": list(item.get("reason_codes") or [item.get("reason_code") or "compatibility_mapping"]),
"effective_risk": str(item.get("effective_risk") or "high"),
"input_facts": dict(item),
"enforcement_point": str(item.get("enforcement_path") or runtime_type),
"timestamp": str(item.get("timestamp") or utc_now()),
"decision_engine_identity": "casan-assurance-kernel",
"evidence_ref": evidence[0]["evidence_id"] if evidence else None,
})
result = outcome(execution_status, assurance_status, certification)
return {
"schema_version": CONTRACT_VERSION,
"extension_namespace": f"casan.runtime.{runtime_type}",
"run": {
"run_id": run_id,
"parent_run_id": event.get("parent_run_id"),
"tenant": str(event.get("tenant") or "default"),
"project": str(event.get("project") or event.get("project_id") or "default"),
"environment": str(event.get("environment") or "development"),
"runtime": {"type": runtime_type, "version": runtime_version},
"mode": mode,
"requested_goal": str(event.get("requested_goal") or ""),
"started_at": event.get("started_at") or event.get("timestamp") or utc_now(),
"completed_at": event.get("completed_at") or utc_now(),
"status": execution_status,
"risk_summary": dict(risk),
"evidence_manifest_ref": event.get("evidence_manifest_ref"),
"correlation_id": correlation_id,
"outcome": result,
},
"actors": [actor(actor_id, "agent" if runtime_type == "casan-native-harness" else "runtime", runtime_type, "verified" if actor_id != "unidentified" else "unverified", event.get("authentication_evidence_ref"))],
"steps": [{
"step_id": step_id,
"run_id": run_id,
"parent_step_id": event.get("parent_step_id"),
"sequence": int(event.get("sequence") or 1),
"actor_ref": actor_id,
"action": {"class": risk["action_class"], "name": action_name, "tool": event.get("tool"), "side_effect_level": risk["side_effect_level"]},
"resource": event.get("resource"),
"input_context_refs": list(event.get("input_context_refs") or []),
"policy_decisions": policy_decisions,
"tool_invocation": event.get("tool_invocation"),
"verification_results": list(event.get("verification_results") or []),
"evidence_refs": [item["evidence_id"] for item in evidence],
"outcome": result,
"started_at": event.get("started_at") or event.get("timestamp") or utc_now(),
"completed_at": event.get("completed_at") or utc_now(),
}],
"runtime_capabilities": {
"canCancel": False, "canPause": False, "canResume": False,
"canRollback": False, "canReduceAuthority": False,
"canQuarantine": runtime_type == "agentic-bridge",
},
"claims": [{
"claim_id": claim_id,
"statement": "CASAN evaluated the runtime assurance outcome",
"evidence_refs": [item["evidence_id"] for item in evidence],
"validation_status": "validated" if assurance_status == "passed" else "insufficient",
}],
"evidence": evidence,
"trace_links": [
{"type": "parent_to_child_execution", "from": run_id, "to": step_id},
*[
{"type": "evidence_to_outcome", "from": item["evidence_id"], "to": claim_id}
for item in evidence
],
],
"extensions": {f"casan.runtime.{runtime_type}": dict(event.get("extensions") or {})},
}
+172
View File
@@ -0,0 +1,172 @@
"""Canonical CASAN wire-contract builders and lightweight validation."""
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from typing import Any, Mapping
CONTRACT_VERSION = "1.0.0"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def actor(actor_id: str, actor_type: str, issuer: str, trust_level: str, auth_evidence_ref: str | None = None) -> dict[str, Any]:
return {
"actor_id": actor_id,
"actor_type": actor_type,
"issuer": issuer,
"trust_level": trust_level,
"authentication_evidence_ref": auth_evidence_ref,
}
def outcome(execution: str, assurance: str, certification: str, business: str = "not_evaluated") -> dict[str, str]:
return {
"execution_result": execution,
"assurance_result": assurance,
"certification_result": certification,
"business_result": business,
}
def evidence_digest(evidence: Mapping[str, Any]) -> str:
"""Digest the claim-bearing evidence metadata, excluding its digest field."""
material = {
key: evidence.get(key)
for key in (
"evidence_id", "claim_refs", "producer_identity", "timestamp",
"artifact_ref", "validation_status", "retention_class", "category",
)
}
payload = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def verify_evidence_integrity(evidence: Mapping[str, Any]) -> bool:
integrity = evidence.get("integrity")
if not isinstance(integrity, Mapping) or integrity.get("algorithm") != "sha256":
return False
digest = integrity.get("digest")
return isinstance(digest, str) and digest == evidence_digest(evidence)
def validate_bundle(bundle: Mapping[str, Any]) -> list[str]:
"""Return deterministic contract errors; an empty list means valid.
JSON Schema is canonical. This small validator keeps runtime adapters stdlib-
only and catches the invariants needed at enforcement boundaries.
"""
errors: list[str] = []
if bundle.get("schema_version") != CONTRACT_VERSION:
errors.append("unsupported_schema_version")
run = bundle.get("run")
if not isinstance(run, Mapping):
errors.append("missing_execution_run")
return errors
for key in ("run_id", "runtime", "mode", "status", "correlation_id", "outcome"):
if not run.get(key):
errors.append(f"run.{key}_required")
actors = bundle.get("actors")
actor_ids: set[object] = set()
if not isinstance(actors, list) or not actors:
errors.append("actors_required")
else:
actor_ids = {item.get("actor_id") for item in actors if isinstance(item, Mapping)}
if "" in actor_ids or None in actor_ids:
errors.append("actor_identity_required")
steps = bundle.get("steps")
if not isinstance(steps, list):
errors.append("steps_required")
else:
for index, step in enumerate(steps):
if not isinstance(step, Mapping):
errors.append(f"steps.{index}_invalid")
continue
for key in ("step_id", "run_id", "sequence", "actor_ref", "action", "outcome"):
if step.get(key) is None:
errors.append(f"steps.{index}.{key}_required")
if step.get("actor_ref") not in actor_ids:
errors.append(f"steps.{index}.actor_ref_unknown")
evidence = bundle.get("evidence")
evidence_ids: set[object] = set()
if not isinstance(evidence, list):
errors.append("evidence_required")
else:
for index, item in enumerate(evidence):
if not isinstance(item, Mapping):
errors.append(f"evidence.{index}_invalid")
continue
evidence_ids.add(item.get("evidence_id"))
if not verify_evidence_integrity(item):
errors.append(f"evidence.{index}.integrity_invalid")
claims = bundle.get("claims")
claim_ids = {
item.get("claim_id") for item in claims or [] if isinstance(item, Mapping)
} if isinstance(claims, list) else set()
if isinstance(claims, list):
for index, claim in enumerate(claims):
if not isinstance(claim, Mapping):
errors.append(f"claims.{index}_invalid")
continue
for evidence_ref in claim.get("evidence_refs") or []:
if evidence_ref not in evidence_ids:
errors.append(f"claims.{index}.evidence_ref_unknown")
if isinstance(evidence, list):
for index, item in enumerate(evidence):
if isinstance(item, Mapping):
for claim_ref in item.get("claim_refs") or []:
if claim_ref not in claim_ids:
errors.append(f"evidence.{index}.claim_ref_unknown")
delegations = bundle.get("delegations")
if delegations is not None:
errors.extend(_validate_delegations(delegations, actor_ids))
if run.get("mode") == "observe" and isinstance(run.get("outcome"), Mapping):
if run["outcome"].get("certification_result") == "certified":
errors.append("observed_only_cannot_be_certified")
return errors
def _validate_delegations(delegations: object, actor_ids: set[object]) -> list[str]:
if not isinstance(delegations, list):
return ["delegations_invalid"]
errors: list[str] = []
records = {
item.get("delegation_id"): item
for item in delegations if isinstance(item, Mapping) and item.get("delegation_id")
}
for index, item in enumerate(delegations):
if not isinstance(item, Mapping):
errors.append(f"delegations.{index}_invalid")
continue
for actor_key in ("delegator_ref", "delegate_ref"):
if item.get(actor_key) not in actor_ids:
errors.append(f"delegations.{index}.{actor_key}_unknown")
current: Mapping[str, Any] = item
visited: set[object] = set()
depth = 0
while current.get("parent_delegation_ref"):
parent_ref = current.get("parent_delegation_ref")
if parent_ref in visited:
errors.append(f"delegations.{index}.cycle")
break
visited.add(parent_ref)
parent = records.get(parent_ref)
if not isinstance(parent, Mapping):
errors.append(f"delegations.{index}.parent_unknown")
break
depth += 1
current = parent
maximum_depth = item.get("maximum_depth")
if isinstance(maximum_depth, int) and depth > maximum_depth:
errors.append(f"delegations.{index}.maximum_depth_exceeded")
return errors
+400
View File
@@ -0,0 +1,400 @@
"""Deterministic security policy primitives shared by every CASAN adapter.
This module is deliberately stdlib-only. It never calls a model and never treats
model text as an enforcement decision. The JSON action registry is the canonical
machine-readable source; callers receive structured decisions rather than booleans.
"""
from __future__ import annotations
import json
import os
import re
import shutil
import stat
import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping, MutableMapping, Optional
POLICY_VERSION = "1.0.0"
RISK_ORDER = {"low": 0, "medium": 1, "high": 2, "critical": 3}
PRODUCTION_PROFILES = {"prod", "production", "strict"}
DEVELOPMENT_PROFILES = {"", "dev", "development", "local", "test", "testing"}
FAILURE_POLICIES = {"halt", "quarantine", "require_approval", "record_only"}
def _harness_root() -> Path:
override = os.environ.get("CASAN_HARNESS_ROOT")
return Path(override).resolve() if override else Path(__file__).resolve().parents[1]
def _action_registry_path() -> Path:
override = os.environ.get("CASAN_ACTION_CLASS_REGISTRY")
return Path(override).resolve() if override else _harness_root() / "config" / "action-classes.json"
def _load_action_registry() -> dict[str, Any]:
with _action_registry_path().open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict) or not isinstance(payload.get("classes"), dict):
raise ValueError("action class registry is malformed")
return payload
def _normal(value: object) -> str:
return re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
def _higher(left: str, right: str) -> str:
return right if RISK_ORDER.get(right, 3) > RISK_ORDER.get(left, 3) else left
def _content_risk(text: str) -> str:
value = (text or "").lower()
if re.search(r"\b(drop\s+table|truncate|password|api[_-]?key|secret|credential|shutdown|rm\s+-rf)\b", value):
return "high"
if re.search(r"\b(deploy|release|migration|permission|policy|external\s+api|database)\b", value):
return "medium"
return "low"
def _resource_risk(resource: str) -> str:
value = (resource or "").lower()
if re.search(r"(^|/)(\.env|\.ssh|\.aws)(/|$)|\.(pem|key|p12|pfx)$|credential|secret", value):
return "high"
if re.search(r"\b(prod|production|customer|tenant)\b", value):
return "medium"
return "low"
def _environment_risk(environment: str) -> str:
return "medium" if _normal(environment) in PRODUCTION_PROFILES else "low"
def classify_action(
action: str = "",
tool: str = "",
resource: str = "",
command: str = "",
) -> dict[str, Any]:
"""Classify an action using deterministic aliases and command patterns.
When several signals match, the class with the highest risk floor wins. An
unknown side-effect-shaped tool is therefore never downgraded by benign text.
"""
registry = _load_action_registry()
classes = registry["classes"]
candidates: list[tuple[str, str]] = []
action_key = _normal(action)
tool_key = _normal(tool)
action_class = registry.get("action_aliases", {}).get(action_key)
if action_class:
candidates.append(("action", action_class))
tool_class = registry.get("tool_aliases", {}).get(tool_key)
if tool_class:
candidates.append(("tool", tool_class))
combined = " ".join(part for part in (command, resource) if part)
for rule in registry.get("command_patterns", []):
try:
if re.search(str(rule["pattern"]), combined, re.IGNORECASE):
candidates.append(("command_or_resource", str(rule["class"])))
except (KeyError, re.error, TypeError) as exc:
raise ValueError("invalid action class pattern") from exc
shell_like = tool_key in {"bash", "shell", "exec", "run", "run_command", "terminal"}
if not candidates and shell_like:
candidates.append(("unknown_shell_command", "unknown"))
if not candidates:
candidates.append(("unclassified", "unknown"))
selected_source, selected_class = candidates[0]
selected_risk = str(classes[selected_class]["risk_floor"])
for source, candidate in candidates[1:]:
risk = str(classes[candidate]["risk_floor"])
if RISK_ORDER.get(risk, 3) > RISK_ORDER.get(selected_risk, 3):
selected_source, selected_class, selected_risk = source, candidate, risk
metadata = classes[selected_class]
return {
"schema_version": registry.get("schema_version", POLICY_VERSION),
"action_class": selected_class,
"classification_source": selected_source,
"risk_floor": selected_risk,
"side_effect_level": metadata["side_effect_level"],
"side_effecting": metadata["side_effect_level"] != "none",
"requires_approval": bool(metadata["requires_approval"]),
"actor_required": bool(metadata["actor_required"]),
"evidence_required": bool(metadata["evidence_required"]),
"matched_classes": [candidate for _source, candidate in candidates],
}
def evaluate_risk(
action: str = "",
tool: str = "",
resource: str = "",
command: str = "",
content: str = "",
actor: str = "",
environment: str = "",
) -> dict[str, Any]:
classification = classify_action(action, tool, resource, command)
actor_present = bool(str(actor or "").strip())
identity_risk = "high" if classification["actor_required"] and not actor_present else "low"
factors = {
"content_risk": _content_risk(content),
"action_risk": classification["risk_floor"],
"resource_risk": _resource_risk(resource),
"identity_risk": identity_risk,
"environment_risk": _environment_risk(environment),
}
effective = "low"
for factor in factors.values():
effective = _higher(effective, factor)
reason_codes: list[str] = ["action_risk_floor_applied"]
if classification["actor_required"] and not actor_present:
decision = "deny"
reason_codes.append("actor_identity_required")
elif classification["requires_approval"] or effective in {"high", "critical"}:
decision = "require_approval"
reason_codes.append("explicit_approval_required")
else:
decision = "allow"
reason_codes.append("risk_within_auto_approval_policy")
return {
"schema_version": POLICY_VERSION,
"policy_id": "casan.action-risk-floor",
"policy_version": POLICY_VERSION,
"decision": decision,
"reason_codes": reason_codes,
"effective_risk": effective,
"risk_factors": factors,
"actor_identity_present": actor_present,
"evidence_requirement": "required" if classification["evidence_required"] else "standard",
**classification,
}
def evaluate_registry_configuration(
mode: str,
profile: str,
explicit_value: Optional[str],
) -> dict[str, Any]:
normalized_mode = _normal(mode)
normalized_profile = _normal(profile)
explicit = None if explicit_value is None else _normal(explicit_value) in {"1", "true", "yes", "on", "enabled"}
production = normalized_profile in PRODUCTION_PROFILES
if explicit is False and production:
return {
"enabled": True,
"valid": False,
"unsafe_bypass": False,
"severity": "critical",
"reason_code": "h2_registry_bypass_forbidden",
}
if explicit is False:
return {
"enabled": False,
"valid": normalized_profile in DEVELOPMENT_PROFILES,
"unsafe_bypass": True,
"severity": "high",
"reason_code": "h2_registry_unsafe_development_bypass",
}
return {
"enabled": True if explicit is None else explicit,
"valid": True,
"unsafe_bypass": False,
"severity": "info",
"reason_code": "h2_registry_default_enabled" if explicit is None and normalized_mode == "enforce" else "h2_registry_enabled",
}
def evaluate_failure_policy(
*,
side_effecting: bool,
mode: str,
profile: str,
configured: Optional[str] = None,
) -> dict[str, Any]:
"""Select single-step failure handling without conflating telemetry success.
Invalid configuration fails closed. Production side effects may be made
stricter, but they may never be configured as record-only.
"""
normalized = _normal(configured or "")
production = _normal(profile) in PRODUCTION_PROFILES
enforce = _normal(mode) == "enforce"
if normalized and normalized not in FAILURE_POLICIES:
policy, source, valid = "halt", "invalid_policy_fail_closed", False
elif normalized == "record_only" and side_effecting and production:
policy, source, valid = "halt", "production_record_only_forbidden", False
elif normalized:
policy, source, valid = normalized, "configured", True
elif side_effecting and (enforce or production):
policy, source, valid = "halt", "safe_enforce_default", True
else:
policy, source, valid = "record_only", "observe_or_read_only_default", True
return {
"schema_version": POLICY_VERSION,
"policy_id": "casan.single-step-failure",
"policy_version": POLICY_VERSION,
"failure_policy": policy,
"source": source,
"valid": valid,
"side_effecting": side_effecting,
"mode": _normal(mode) or "observe",
"profile": _normal(profile) or "development",
"execution_result": "failed",
"telemetry_result": "recorded",
"assurance_may_continue": policy == "record_only",
"reason_codes": [source],
}
@dataclass(frozen=True)
class H2GateContext:
mode: str
actor: str
action: str
tool: str
execution_id: str
enforcement_path: str
idempotency_key: str
timeout_seconds: float = 8.0
def _h2_result(context: H2GateContext, underlying: str, reason_code: str, detail: str) -> dict[str, Any]:
enforce = _normal(context.mode) == "enforce"
actual_deny = underlying == "deny"
dependency_failure = underlying == "error"
execution_allowed = not enforce or (not actual_deny and not dependency_failure)
if enforce:
decision = "allow" if execution_allowed else "deny"
else:
decision = "observe_only"
return {
"schema_version": POLICY_VERSION,
"policy_id": "casan.h2.tool-registry",
"policy_version": POLICY_VERSION,
"decision": decision,
"underlying_decision": underlying,
"reason_codes": [reason_code],
"reason_code": reason_code,
"mode": _normal(context.mode) or "observe",
"actor": context.actor or "unidentified",
"action": context.action,
"tool": context.tool,
"execution_id": context.execution_id,
"enforcement_path": context.enforcement_path,
"execution_allowed": execution_allowed,
"certifiable": enforce and execution_allowed,
"assurance_status": "enforced" if enforce and execution_allowed else "denied" if enforce else "degraded",
"severity": "high" if reason_code != "h2_ok" else "info",
"detail": detail[:400],
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
def evaluate_h2_gate(
gate_path: str,
context: H2GateContext,
env: Optional[Mapping[str, str]] = None,
bash_path: str = "bash",
) -> dict[str, Any]:
"""Execute the H2 dependency and convert every outcome into one policy decision."""
path = Path(gate_path)
if not path.is_file():
return _h2_result(context, "error", "h2_gate_unavailable", "gate file is missing")
try:
mode_bits = stat.S_IMODE(path.stat().st_mode)
except OSError as exc:
return _h2_result(context, "error", "h2_gate_unavailable", f"gate stat failed: {type(exc).__name__}")
if mode_bits & 0o444 == 0:
return _h2_result(context, "error", "h2_gate_permission_denied", "gate file is not readable")
resolved_bash = shutil.which(bash_path) if not os.path.isabs(bash_path) else bash_path
if not resolved_bash or not Path(resolved_bash).is_file():
return _h2_result(context, "error", "h2_gate_unavailable", "bash runtime is unavailable")
child_env: MutableMapping[str, str] = dict(os.environ)
if env:
child_env.update({str(key): str(value) for key, value in env.items()})
child_env["CASAN_IDEMPOTENCY_KEY"] = context.idempotency_key
if context.actor and not child_env.get("CASAN_AGENT"):
child_env["CASAN_AGENT"] = context.actor
try:
proc = subprocess.run(
[resolved_bash, str(path), context.action],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=max(0.001, context.timeout_seconds),
env=child_env,
check=False,
text=True,
)
except subprocess.TimeoutExpired:
return _h2_result(context, "error", "h2_gate_timeout", "gate exceeded internal timeout")
except (OSError, ValueError, subprocess.SubprocessError) as exc:
return _h2_result(context, "error", "h2_gate_internal_error", type(exc).__name__)
stdout = proc.stdout.strip()[:4096]
stderr = proc.stderr.strip()[:4096]
if proc.returncode == 0 and re.search(r"\bTOOL_APPROVED\b", stdout):
return _h2_result(context, "allow", "h2_ok", stdout)
if re.search(r"\bTOOL_DENIED\b", stdout + "\n" + stderr):
reason_match = re.search(r"reason=([^\s]+)", stdout + "\n" + stderr)
reason = "h2_denied:%s" % (reason_match.group(1) if reason_match else "policy")
return _h2_result(context, "deny", reason, stderr or stdout)
if proc.returncode in {126, 127}:
return _h2_result(context, "error", "h2_gate_unavailable", stderr or "gate runtime unavailable")
if proc.returncode != 0:
return _h2_result(context, "error", "h2_gate_internal_error", stderr or stdout or f"exit={proc.returncode}")
return _h2_result(context, "error", "h2_gate_malformed_response", stdout or "empty response")
def evaluate_trust_capabilities(env: Optional[Mapping[str, str]] = None) -> dict[str, Any]:
values = dict(os.environ if env is None else env)
profile = _normal(values.get("CASAN_PROFILE", ""))
production = profile in PRODUCTION_PROFILES
signing_provider = _normal(values.get("CASAN_SIGNING_PROVIDER", "local_openssl"))
anchor_provider = _normal(values.get("CASAN_IMMUTABLE_ANCHOR_PROVIDER", "local_hash_chain"))
emergency = _normal(values.get("CASAN_TRUST_EMERGENCY_OVERRIDE", "")) in {"1", "true", "yes", "on"}
signing_external = signing_provider in {"vault", "vault_kms", "kms", "hsm"}
anchor_external = anchor_provider in {"s3_object_lock", "qldb", "external_worm"}
vault_addr = str(values.get("VAULT_ADDR") or "")
signing_configured = signing_external and vault_addr.startswith("https://") and bool(values.get("VAULT_TOKEN"))
anchor_configured = anchor_external and bool(values.get("CASAN_S3_BUCKET")) and bool(values.get("CASAN_S3_KMS_KEY_ID"))
ready = (not production) or (signing_configured and anchor_configured)
certifiable = ready and not emergency
reasons: list[str] = []
if production and not signing_configured:
reasons.append("external_signing_trust_root_required")
if production and not anchor_configured:
reasons.append("external_immutable_anchor_required")
if emergency:
reasons.append("emergency_local_trust_override_active")
return {
"schema_version": POLICY_VERSION,
"profile": profile or "development",
"production": production,
"signing_provider": signing_provider or "local_openssl",
"signing_capability": "external" if signing_configured else "local_or_unavailable",
"immutable_anchor_provider": anchor_provider or "local_hash_chain",
"immutable_anchor_capability": "external" if anchor_configured else "local_or_unavailable",
"ready": ready,
"certifiable": certifiable,
"emergency_override": emergency,
"severity": "critical" if emergency or not ready else "info",
"reason_codes": reasons or ["trust_capabilities_satisfied"],
}
@@ -0,0 +1,42 @@
"""Runtime supervision capability negotiation without pretending support."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from typing import Any
@dataclass(frozen=True)
class RuntimeCapabilities:
canCancel: bool = False
canPause: bool = False
canResume: bool = False
canRollback: bool = False
canReduceAuthority: bool = False
canQuarantine: bool = False
CAPABILITY_FOR_INTERVENTION = {
"cancel": "canCancel",
"pause": "canPause",
"resume": "canResume",
"roll_back": "canRollback",
"reduce_authority": "canReduceAuthority",
"quarantine": "canQuarantine",
}
def negotiate_intervention(intervention_id: str, intervention: str, requested_by: str, capabilities: RuntimeCapabilities) -> dict[str, Any]:
capability = CAPABILITY_FOR_INTERVENTION.get(intervention)
supported = bool(capability and getattr(capabilities, capability))
return {
"intervention_id": intervention_id,
"type": intervention,
"requested_by": requested_by,
"status": "pending" if supported else "unsupported",
"reason_code": "runtime_intervention_supported_pending_adapter" if supported else "runtime_intervention_unsupported",
"required_capability": capability,
"runtime_capabilities": asdict(capabilities),
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
+43
View File
@@ -0,0 +1,43 @@
"""Typed H1-H7 namespaces. Legacy `Hn` labels remain display aliases only."""
from __future__ import annotations
from enum import Enum
class AssuranceCategory(str, Enum):
RUNTIME_CONTROL = "runtime_control"
READINESS_CHECK = "readiness_check"
REPORT_DIMENSION = "report_dimension"
CERTIFICATION_CLAIM = "certification_claim"
class _HNamespace(str, Enum):
@property
def legacy_id(self) -> str:
return self.value.rsplit(".", 1)[-1]
class RuntimeControl(_HNamespace):
H1 = "RuntimeControl.H1"; H2 = "RuntimeControl.H2"; H3 = "RuntimeControl.H3"
H4 = "RuntimeControl.H4"; H5 = "RuntimeControl.H5"; H6 = "RuntimeControl.H6"; H7 = "RuntimeControl.H7"
class ReadinessCheck(_HNamespace):
H1 = "ReadinessCheck.H1"; H2 = "ReadinessCheck.H2"; H3 = "ReadinessCheck.H3"
H4 = "ReadinessCheck.H4"; H5 = "ReadinessCheck.H5"; H6 = "ReadinessCheck.H6"; H7 = "ReadinessCheck.H7"
class ReportDimension(_HNamespace):
H1 = "ReportDimension.H1"; H2 = "ReportDimension.H2"; H3 = "ReportDimension.H3"
H4 = "ReportDimension.H4"; H5 = "ReportDimension.H5"; H6 = "ReportDimension.H6"; H7 = "ReportDimension.H7"
class CertificationClaim(_HNamespace):
H1 = "CertificationClaim.H1"; H2 = "CertificationClaim.H2"; H3 = "CertificationClaim.H3"
H4 = "CertificationClaim.H4"; H5 = "CertificationClaim.H5"; H6 = "CertificationClaim.H6"; H7 = "CertificationClaim.H7"
def same_legacy_label(left: _HNamespace, right: _HNamespace) -> bool:
"""Compatibility display helper; never authorizes cross-category interpretation."""
return left.legacy_id == right.legacy_id
@@ -58,6 +58,10 @@
"type": ["string", "null"],
"description": "Client-native session id. Hashed by the bridge, never stored raw."
},
"actor": {
"type": ["string", "null"],
"description": "Stable actor identity asserted by the authenticated runtime boundary."
},
"integration_mode": {
"type": ["string", "null"],
"enum": ["casan_owned", "managed_hook", "project_hook", "observed_only", null],
@@ -167,7 +171,7 @@
"schema_version": { "type": "string" },
"decision": {
"type": "string",
"enum": ["allow", "block", "deny", "recorded", "certified", "non_certified", "error"]
"enum": ["allow", "block", "deny", "require_approval", "halt", "quarantine", "recorded", "certified", "non_certified", "error"]
},
"admission_id": { "type": ["string", "null"] },
"trace_id": { "type": ["string", "null"] },
@@ -0,0 +1,153 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://casan.dev/schemas/assurance-kernel/1.0.0",
"title": "CASAN Assurance Kernel Execution Envelope",
"description": "Framework-independent canonical wire contract. Runtime-specific fields belong under a namespaced extensions object.",
"type": "object",
"required": ["schema_version", "extension_namespace", "run", "actors", "steps", "evidence", "trace_links", "extensions"],
"properties": {
"schema_version": {"const": "1.0.0"},
"extension_namespace": {"type": "string", "pattern": "^[a-z][a-z0-9.-]+$"},
"run": {"$ref": "#/$defs/ExecutionRun"},
"actors": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/Actor"}},
"steps": {"type": "array", "items": {"$ref": "#/$defs/ExecutionStep"}},
"delegations": {"type": "array", "items": {"$ref": "#/$defs/Delegation"}},
"context_items": {"type": "array", "items": {"$ref": "#/$defs/ContextItem"}},
"runtime_constraints": {"type": "array", "items": {"$ref": "#/$defs/RuntimeConstraint"}},
"runtime_capabilities": {"$ref": "#/$defs/RuntimeCapabilities"},
"approvals": {"type": "array", "items": {"$ref": "#/$defs/Approval"}},
"interventions": {"type": "array", "items": {"$ref": "#/$defs/Intervention"}},
"claims": {"type": "array", "items": {"$ref": "#/$defs/Claim"}},
"evidence": {"type": "array", "items": {"$ref": "#/$defs/EvidenceItem"}},
"trace_links": {"type": "array", "items": {"$ref": "#/$defs/TraceLink"}},
"extensions": {"type": "object", "additionalProperties": {"type": "object"}}
},
"additionalProperties": false,
"$defs": {
"Timestamp": {"type": "string", "format": "date-time"},
"Outcome": {
"type": "object",
"required": ["execution_result", "assurance_result", "certification_result", "business_result"],
"properties": {
"execution_result": {"enum": ["pending", "success", "failed", "cancelled", "quarantined", "unknown"]},
"assurance_result": {"enum": ["pending", "passed", "failed", "degraded", "not_evaluated", "unknown"]},
"certification_result": {"enum": ["pending", "certified", "non_certified", "ineligible"]},
"business_result": {"enum": ["achieved", "not_achieved", "partial", "not_evaluated", "unknown"]}
},
"additionalProperties": false
},
"ExecutionRun": {
"type": "object",
"required": ["run_id", "tenant", "project", "environment", "runtime", "mode", "requested_goal", "started_at", "status", "risk_summary", "correlation_id", "outcome"],
"properties": {
"run_id": {"type": "string", "minLength": 1},
"parent_run_id": {"type": ["string", "null"]},
"tenant": {"type": "string"},
"project": {"type": "string"},
"environment": {"type": "string"},
"runtime": {"type": "object", "required": ["type", "version"], "properties": {"type": {"type": "string"}, "version": {"type": "string"}}, "additionalProperties": false},
"mode": {"enum": ["observe", "enforce"]},
"requested_goal": {"type": "string"},
"started_at": {"$ref": "#/$defs/Timestamp"},
"completed_at": {"anyOf": [{"$ref": "#/$defs/Timestamp"}, {"type": "null"}]},
"status": {"enum": ["pending", "running", "success", "failed", "cancelled", "quarantined", "unknown"]},
"risk_summary": {"type": "object"},
"evidence_manifest_ref": {"type": ["string", "null"]},
"correlation_id": {"type": "string", "minLength": 1},
"outcome": {"$ref": "#/$defs/Outcome"}
},
"additionalProperties": false
},
"ExecutionStep": {
"type": "object",
"required": ["step_id", "run_id", "sequence", "actor_ref", "action", "input_context_refs", "policy_decisions", "verification_results", "evidence_refs", "outcome", "started_at", "completed_at"],
"properties": {
"step_id": {"type": "string"}, "run_id": {"type": "string"}, "parent_step_id": {"type": ["string", "null"]}, "sequence": {"type": "integer", "minimum": 1},
"actor_ref": {"type": "string"}, "action": {"$ref": "#/$defs/Action"}, "resource": {"type": ["object", "string", "null"]},
"input_context_refs": {"type": "array", "items": {"type": "string"}}, "policy_decisions": {"type": "array", "items": {"$ref": "#/$defs/PolicyDecision"}},
"tool_invocation": {"type": ["object", "null"]}, "verification_results": {"type": "array", "items": {"$ref": "#/$defs/VerificationResult"}},
"evidence_refs": {"type": "array", "items": {"type": "string"}}, "outcome": {"$ref": "#/$defs/Outcome"},
"started_at": {"$ref": "#/$defs/Timestamp"}, "completed_at": {"anyOf": [{"$ref": "#/$defs/Timestamp"}, {"type": "null"}]}
},
"additionalProperties": false
},
"Actor": {
"type": "object",
"required": ["actor_id", "actor_type", "issuer", "trust_level"],
"properties": {
"actor_id": {"type": "string"}, "actor_type": {"enum": ["human", "agent", "child_agent", "model", "tool", "service_account", "runtime", "approver", "policy_engine"]},
"issuer": {"type": "string"}, "trust_level": {"enum": ["unverified", "asserted", "verified", "hardware_backed"]}, "authentication_evidence_ref": {"type": ["string", "null"]}
},
"additionalProperties": false
},
"Delegation": {
"type": "object",
"required": ["delegation_id", "delegator_ref", "delegate_ref", "allowed_authority", "resource_scope", "expiry", "maximum_depth", "revoked"],
"properties": {
"delegation_id": {"type": "string"}, "delegator_ref": {"type": "string"}, "delegate_ref": {"type": "string"}, "allowed_authority": {"type": "array", "items": {"type": "string"}},
"resource_scope": {"type": "array", "items": {"type": "string"}}, "expiry": {"$ref": "#/$defs/Timestamp"}, "maximum_depth": {"type": "integer", "minimum": 0},
"parent_delegation_ref": {"type": ["string", "null"]}, "revoked": {"type": "boolean"}, "approval_ref": {"type": ["string", "null"]}
},
"additionalProperties": false
},
"ContextItem": {
"type": "object", "required": ["context_id", "source", "trust_classification", "content_hash", "transform_history", "compression_lineage", "classification", "injection_scan_result"],
"properties": {
"context_id": {"type": "string"}, "source": {"type": "string"}, "trust_classification": {"enum": ["trusted", "untrusted", "mixed", "unknown"]}, "content_hash": {"type": "string"},
"transform_history": {"type": "array", "items": {"type": "object"}}, "compression_lineage": {"type": "array", "items": {"type": "string"}},
"origin_ref": {"type": ["string", "null"]}, "classification": {"enum": ["instruction", "data", "mixed", "unknown"]}, "injection_scan_result": {"type": "object"}
}, "additionalProperties": false
},
"Action": {
"type": "object", "required": ["class", "name", "side_effect_level"],
"properties": {"class": {"enum": ["read_only", "write", "delete", "database_mutation", "migration", "deployment", "release", "credential_access", "identity_permission_modification", "external_network_side_effect", "infrastructure_modification", "unknown"]}, "name": {"type": "string"}, "tool": {"type": ["string", "null"]}, "side_effect_level": {"enum": ["none", "sensitive_read", "write", "destructive", "external", "unknown"]}, "required_authority": {"type": ["string", "null"]}, "requested_operation": {"type": ["string", "null"]}, "environment": {"type": ["string", "null"]}},
"additionalProperties": false
},
"ResourceAccess": {
"type": "object", "required": ["resource", "operation", "side_effect_level", "environment"],
"properties": {"resource": {"type": "string"}, "operation": {"type": "string"}, "side_effect_level": {"type": "string"}, "required_authority": {"type": ["string", "null"]}, "environment": {"type": "string"}}, "additionalProperties": false
},
"PolicyDecision": {
"type": "object", "required": ["policy_id", "policy_version", "decision", "reason_codes", "effective_risk", "input_facts", "enforcement_point", "timestamp", "decision_engine_identity"],
"properties": {"policy_id": {"type": "string"}, "policy_version": {"type": "string"}, "decision": {"enum": ["allow", "deny", "quarantine", "require_approval", "transform", "observe_only"]}, "reason_codes": {"type": "array", "items": {"type": "string"}}, "effective_risk": {"enum": ["low", "medium", "high", "critical"]}, "input_facts": {"type": "object"}, "enforcement_point": {"type": "string"}, "timestamp": {"$ref": "#/$defs/Timestamp"}, "decision_engine_identity": {"type": "string"}, "evidence_ref": {"type": ["string", "null"]}}, "additionalProperties": false
},
"RuntimeConstraint": {
"type": "object", "required": ["constraint_id", "kind", "limit", "enforcement"],
"properties": {"constraint_id": {"type": "string"}, "kind": {"enum": ["iteration", "time", "cost", "token", "child_agent_count", "graph_depth", "retry", "repetition", "network", "filesystem", "tool", "environment"]}, "limit": {}, "enforcement": {"enum": ["hard", "soft", "observe"]}}, "additionalProperties": false
},
"RuntimeCapabilities": {
"type": "object",
"required": ["canCancel", "canPause", "canResume", "canRollback", "canReduceAuthority", "canQuarantine"],
"properties": {"canCancel": {"type": "boolean"}, "canPause": {"type": "boolean"}, "canResume": {"type": "boolean"}, "canRollback": {"type": "boolean"}, "canReduceAuthority": {"type": "boolean"}, "canQuarantine": {"type": "boolean"}},
"additionalProperties": false
},
"VerificationRequirement": {
"type": "object", "required": ["requirement_id", "validator", "expected_result", "independence_level", "failure_severity"],
"properties": {"requirement_id": {"type": "string"}, "validator": {"type": "string"}, "expected_result": {}, "independence_level": {"enum": ["same_runtime", "independent_process", "independent_service", "external_authority"]}, "failure_severity": {"enum": ["info", "warning", "error", "critical"]}}, "additionalProperties": false
},
"VerificationResult": {
"type": "object", "required": ["requirement_id", "validator", "expected_result", "actual_result", "status", "evidence_refs", "independence_level", "failure_severity"],
"properties": {"requirement_id": {"type": "string"}, "validator": {"type": "string"}, "expected_result": {}, "actual_result": {}, "status": {"enum": ["passed", "failed", "degraded", "not_run"]}, "evidence_refs": {"type": "array", "items": {"type": "string"}}, "independence_level": {"type": "string"}, "failure_severity": {"type": "string"}}, "additionalProperties": false
},
"Claim": {
"type": "object", "required": ["claim_id", "statement", "evidence_refs", "validation_status"],
"properties": {"claim_id": {"type": "string"}, "statement": {"type": "string"}, "evidence_refs": {"type": "array", "items": {"type": "string"}}, "validation_status": {"enum": ["validated", "rejected", "insufficient", "not_validated"]}}, "additionalProperties": false
},
"EvidenceItem": {
"type": "object", "required": ["evidence_id", "claim_refs", "integrity", "producer_identity", "timestamp", "artifact_ref", "validation_status", "retention_class"],
"properties": {"evidence_id": {"type": "string"}, "claim_refs": {"type": "array", "items": {"type": "string"}}, "integrity": {"type": "object", "required": ["algorithm", "digest"], "properties": {"algorithm": {"const": "sha256"}, "digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, "source_content_hash": {"type": ["string", "null"]}}, "additionalProperties": false}, "producer_identity": {"type": "string"}, "timestamp": {"$ref": "#/$defs/Timestamp"}, "artifact_ref": {"type": "string"}, "validation_status": {"enum": ["valid", "invalid", "unverified", "unavailable"]}, "retention_class": {"type": "string"}, "category": {"enum": ["runtime_control", "readiness_check", "report_dimension", "certification_claim"]}}, "additionalProperties": false
},
"TraceLink": {
"type": "object", "required": ["type", "from", "to"],
"properties": {"type": {"enum": ["requirement_to_policy", "policy_to_decision", "decision_to_action", "action_to_artifact", "artifact_to_verification", "verification_to_evidence", "evidence_to_outcome", "parent_to_child_execution"]}, "from": {"type": "string"}, "to": {"type": "string"}}, "additionalProperties": false
},
"Approval": {
"type": "object", "required": ["approval_id", "decision", "approver_ref", "timestamp"],
"properties": {"approval_id": {"type": "string"}, "decision": {"enum": ["approve", "reject"]}, "approver_ref": {"type": "string"}, "reason": {"type": ["string", "null"]}, "timestamp": {"$ref": "#/$defs/Timestamp"}}, "additionalProperties": false
},
"Intervention": {
"type": "object", "required": ["intervention_id", "type", "requested_by", "status", "timestamp"],
"properties": {"intervention_id": {"type": "string"}, "type": {"enum": ["pause", "resume", "cancel", "quarantine", "reduce_authority", "redirect", "roll_back"]}, "requested_by": {"type": "string"}, "status": {"enum": ["applied", "rejected", "unsupported", "pending"]}, "timestamp": {"$ref": "#/$defs/Timestamp"}}, "additionalProperties": false
}
}
}
@@ -110,6 +110,10 @@ LATENCY_MS=$((END_MS - START_MS))
if [[ ! -f "$OUTPUT_FILE" ]]; then
STATUS="failed"
ERROR_MSG="${ERROR_MSG:-output file not produced}"
# A zero command exit does not make the step successful when the runtime
# contract requires an output artifact and none was produced. Telemetry can
# record this failure successfully, but must propagate a failed step outcome.
[[ "$EXIT_CODE" -eq 0 ]] && EXIT_CODE=1
: > "$OUTPUT_FILE"
fi
@@ -24,6 +24,7 @@ fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
PROJECT_ROOT="$CASAN_APP_ROOT"
KERNEL_CLI="$CASAN_HARNESS_ROOT/scripts/python/kernel_cli.py"
# SEC-23 (MT-01): make state (control-plane settings, telemetry, audit) tenant-scoped
# when CASAN_TENANT_ID is set, so a run for tenant A never touches tenant B's state.
# No-op when no tenant is set (baseline unchanged); invalid tenant fails closed.
@@ -56,12 +57,52 @@ write_phase_report() {
"$ACTION_NAME" "$CACHE_STATUS" "$PHASE_LOG" > "$PHASE_REPORT" 2>/dev/null || true
}
emit_failed_kernel() { # phase rc — best effort, never masks the original failure
local failed_phase="$1" failed_rc="$2"
[[ -f "$KERNEL_CLI" ]] || return 0
local run_id="${EXECUTION_ID:-native-failed-${TRACE_SUFFIX:-$$}}"
local mode="${NATIVE_MODE:-observe}"
local event bundle path
event="$(CASAN_NATIVE_RISK="${ACTION_RISK_JSON:-}" python3 - "$run_id" "$ACTION_NAME" "$mode" "$failed_phase" "$failed_rc" "${CASAN_ACTOR:-${CASAN_AGENT:-}}" <<'PY'
import json, os, sys
run_id, action, mode, phase, rc, actor = sys.argv[1:]
try: risk = json.loads(os.environ.get("CASAN_NATIVE_RISK") or "{}")
except ValueError: risk = {}
print(json.dumps({
"run_id": run_id, "correlation_id": run_id, "action": action,
"tool": action, "actor": actor, "mode": mode,
"environment": os.environ.get("CASAN_PROFILE", "development"),
"execution_status": "failed", "assurance_status": "failed",
"certification_status": "non_certified", "policy_decisions": [risk] if risk else [],
"extensions": {"failed_phase": phase, "exit_code": int(rc)},
}, separators=(",", ":")))
PY
)" || return 0
bundle="$(printf '%s' "$event" | python3 "$KERNEL_CLI" adapt-native - 2>/dev/null)" || return 0
path="$CASAN_STATE_ROOT/logs/kernel/$run_id.json"
CASAN_KERNEL_BUNDLE="$bundle" python3 - "$path" <<'PY' || return 0
import json, os, sys, tempfile
path = sys.argv[1]; payload = json.loads(os.environ["CASAN_KERNEL_BUNDLE"])
os.makedirs(os.path.dirname(path), exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix=".kernel-", dir=os.path.dirname(path))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, sort_keys=True, separators=(",", ":")); handle.write("\n")
handle.flush(); os.fsync(handle.fileno())
os.replace(tmp, path)
finally:
try: os.unlink(tmp)
except OSError: pass
PY
}
run_phase() { # <phase-name> <command...> — preserves the failing rc exactly
local phase="$1"; shift
local rc=0
"$@" || rc=$?
record_phase "$phase" "$rc"
if [[ "$rc" -ne 0 ]]; then
emit_failed_kernel "$phase" "$rc"
write_phase_report
exit "$rc"
fi
@@ -76,9 +117,16 @@ hash_text() {
}
CMD_STR="${*:-no_cmd}"
NATIVE_MODE="${CASAN_ENFORCEMENT_MODE:-}"
if [[ -z "$NATIVE_MODE" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then NATIVE_MODE="enforce"; else NATIVE_MODE="observe"; fi
fi
case "$NATIVE_MODE" in observe|enforce) : ;; *) NATIVE_MODE="enforce"; casan_log error harness "INVALID_ENFORCEMENT_MODE fail_closed=enforce" ;; esac
INPUT_HASH="$(cat "$INPUT_FILE" | hash_text)"
CMD_HASH="$(printf '%s' "$CMD_STR" | hash_text)"
IDEMPOTENCY_KEY="$(printf '%s|%s|%s' "$INPUT_HASH" "$CMD_HASH" "$ACTION_NAME" | hash_text)"
EXECUTION_ID="${CASAN_EXECUTION_ID:-native-${IDEMPOTENCY_KEY:0:24}}"
export CASAN_EXECUTION_ID="$EXECUTION_ID"
CACHE_META="$CACHE_DIR/$IDEMPOTENCY_KEY.json"
CACHE_OUT="$CACHE_DIR/$IDEMPOTENCY_KEY.output"
@@ -92,7 +140,7 @@ casan_log debug harness "action=$ACTION_NAME input=$INPUT_FILE output=$FINAL_OUT
# C7: honor an engaged kill-switch before doing any work (incident containment).
# Opt-in (default off) so the baseline is unchanged. SEC-17 (ARCH-03): under
# CASAN_PROFILE=prod it defaults ON (secure-by-default); an explicit =0 still wins.
if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" || ( -z "${CASAN_KILLSWITCH_ENFORCE+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then
if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" || ( -z "${CASAN_KILLSWITCH_ENFORCE+x}" && ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ) ) ]]; then
KS_SCOPE="${CASAN_KILLSWITCH_SCOPE:-project}"
KS_ID="${CASAN_KILLSWITCH_ID:-${CASAN_PROJECT:-current}}"
if ! bash "$SCRIPT_DIR/kill-switch.sh" check "$KS_SCOPE" "$KS_ID" >/dev/null 2>&1; then
@@ -116,7 +164,7 @@ fi
# signed manifest and REFUSE to run on any drift — editing a gate/policy is a bypass
# that leaves no input trace. Only active when a manifest is provisioned (so dev and
# prod-without-a-manifest are unaffected); a present-but-drifted bundle fails closed.
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
&& -f "$SCRIPT_DIR/bundle-integrity.py" ]]; then
BUNDLE_MANIFEST="${CASAN_BUNDLE_MANIFEST:-$CASAN_GOVERNANCE_ROOT/harness-bundle-manifest.json}"
if [[ -f "$BUNDLE_MANIFEST" ]]; then
@@ -132,15 +180,62 @@ fi
run_phase "H4-in" "$SCRIPT_DIR/security-check.sh" "$INPUT_FILE" "$SAFE_INPUT" input
run_phase "H5" "$SCRIPT_DIR/governance-check.sh" "$SAFE_INPUT" "$APPROVED_INPUT" "$ACTION_NAME"
# H2 tool registry gate is in the line of fire for side-effecting actions:
# it enforces idempotency key, per-agent permission, and rollback strategy
# before the command is allowed to execute. The wrapper already derived a
# content-addressed idempotency key above.
case "$ACTION_NAME" in
write_code|migration|deploy|db_write|external_api|write_file)
run_phase "H2-gate" env CASAN_IDEMPOTENCY_KEY="$IDEMPOTENCY_KEY" "$SCRIPT_DIR/tool-registry-gate.sh" "$ACTION_NAME"
;;
esac
# Canonical action classification decides whether the H2 registry and isolated
# executor are required. A classifier failure is treated as unknown/high-risk.
ACTION_RISK_JSON=""
ACTION_RISK_RC=0
ACTION_RISK_JSON="$(python3 "$KERNEL_CLI" risk --action "$ACTION_NAME" --tool "$ACTION_NAME" \
--command "$CMD_STR" --content-file "$SAFE_INPUT" --actor "${CASAN_ACTOR:-${CASAN_AGENT:-}}" \
--environment "${CASAN_PROFILE:-development}" 2>/dev/null)" || ACTION_RISK_RC=$?
if [[ "$ACTION_RISK_RC" -eq 0 && -n "$ACTION_RISK_JSON" ]]; then
ACTION_FIELDS="$(python3 - "$ACTION_RISK_JSON" <<'PY'
import json, sys
p=json.loads(sys.argv[1])
print("%s\t%s" % ("1" if p["side_effecting"] else "0", p["action_class"]))
PY
)" || ACTION_RISK_RC=$?
fi
if [[ "$ACTION_RISK_RC" -eq 0 && -n "${ACTION_FIELDS:-}" ]]; then
IFS=$'\t' read -r SIDE_EFFECTING ACTION_CLASS <<< "$ACTION_FIELDS"
else
SIDE_EFFECTING=1
ACTION_CLASS="unknown"
casan_log error harness "ACTION_CLASSIFIER_FAILED_CLOSED action=$ACTION_NAME"
fi
if [[ "$SIDE_EFFECTING" == "1" ]]; then
REGISTRY_ARGS=(registry-config --mode "$NATIVE_MODE" --profile "${CASAN_PROFILE:-development}" \
--evidence-log "$CASAN_STATE_ROOT/logs/policy/h2-registry-config.jsonl")
[[ -n "${CASAN_H2_REGISTRY+x}" ]] && REGISTRY_ARGS+=(--explicit "$CASAN_H2_REGISTRY")
REGISTRY_RC=0
REGISTRY_JSON="$(python3 "$KERNEL_CLI" "${REGISTRY_ARGS[@]}")" || REGISTRY_RC=$?
if [[ "$REGISTRY_RC" -ne 0 ]]; then
record_phase "H2-config" "$REGISTRY_RC"
echo "H2_REGISTRY_CONFIGURATION_DENIED $REGISTRY_JSON" >&2
write_phase_report
exit "$REGISTRY_RC"
fi
REGISTRY_ENABLED="$(python3 -c 'import json,sys; print("1" if json.load(sys.stdin)["enabled"] else "0")' <<< "$REGISTRY_JSON")"
if [[ "$REGISTRY_ENABLED" == "1" ]]; then
H2_ACTION="$ACTION_NAME"
case "$ACTION_CLASS" in
write) H2_ACTION="write_file" ;;
delete) H2_ACTION="delete_file" ;;
database_mutation) H2_ACTION="db_write" ;;
deployment|release|infrastructure_modification) H2_ACTION="deploy" ;;
external_network_side_effect) H2_ACTION="external_api" ;;
unknown) H2_ACTION="unknown_tool" ;;
esac
run_phase "H2-gate" python3 "$KERNEL_CLI" h2-gate \
--gate "${CASAN_H2_GATE_PATH:-$SCRIPT_DIR/tool-registry-gate.sh}" --mode "$NATIVE_MODE" \
--actor "${CASAN_ACTOR:-${CASAN_AGENT:-}}" --action "$H2_ACTION" --tool "$ACTION_NAME" \
--execution-id "$EXECUTION_ID" --enforcement-path "native_harness.pre_execution.h2_registry" \
--idempotency-key "$IDEMPOTENCY_KEY" --timeout "${CASAN_H2_GATE_TIMEOUT_SECONDS:-8}" \
--evidence-log "$CASAN_STATE_ROOT/logs/policy/h2-decisions.jsonl"
else
casan_log warn harness "HIGH H2 registry unsafe development/test bypass active; run is non-certifiable"
fi
fi
# T4: propagate step name so any nested model calls (model-call.py) log against the same step
# name, enabling provider-cost-lookup.py to match real Ollama token counts in agent-metrics.sh.
@@ -155,13 +250,34 @@ if [[ -f "$CACHE_META" && -f "$CACHE_OUT" ]]; then
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- bash -c 'cp "$1" "$CASAN_OUTPUT"' _ "$CACHE_OUT"
elif [[ "$#" -gt 0 ]]; then
CACHE_STATUS="stored"
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
if [[ "$SIDE_EFFECTING" == "1" && "$NATIVE_MODE" == "enforce" ]]; then
run_phase "H6-exec" env CASAN_ENFORCEMENT_MODE="$NATIVE_MODE" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
"$SCRIPT_DIR/sandbox-run.sh" --workspace "$PROJECT_ROOT" --timeout "$TOOL_TIMEOUT" -- "$@"
else
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
fi
else
CACHE_STATUS="stored"
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT"
fi
TOOL_OUTPUT_MAX_BYTES="${CASAN_TOOL_OUTPUT_MAX_BYTES:-1048576}"
if [[ ! "$TOOL_OUTPUT_MAX_BYTES" =~ ^[1-9][0-9]*$ ]]; then
casan_log error harness "TOOL_OUTPUT_LIMIT_INVALID value=$TOOL_OUTPUT_MAX_BYTES"
: > "$FINAL_OUTPUT"
exit 2
fi
RAW_OUTPUT_BYTES="$(wc -c < "$RAW_OUTPUT" | tr -d ' ')"
if [[ "$RAW_OUTPUT_BYTES" -gt "$TOOL_OUTPUT_MAX_BYTES" ]]; then
record_phase "H4-output-size" 2
casan_log error harness "TOOL_OUTPUT_QUARANTINED bytes=$RAW_OUTPUT_BYTES limit=$TOOL_OUTPUT_MAX_BYTES"
: > "$FINAL_OUTPUT"
write_phase_report
echo "TOOL_OUTPUT_QUARANTINED reason=output_size_limit bytes=$RAW_OUTPUT_BYTES limit=$TOOL_OUTPUT_MAX_BYTES" >&2
exit 2
fi
# V7: tool output can carry indirect injection that would re-enter a downstream
# model's context. Scan RAW_OUTPUT for injection/secret patterns before it is
# reused. Mode: off | warn (default) | block. Strict mode upgrades to block.
@@ -170,7 +286,7 @@ fi
TOOL_OUTPUT_SCAN_MODE="${CASAN_TOOL_OUTPUT_SCAN:-}"
if [[ -z "$TOOL_OUTPUT_SCAN_MODE" ]]; then
# SEC-17/M-02: prod profile defaults tool-output scanning to block (fail-closed).
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ) ) ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
fi
if [[ "$TOOL_OUTPUT_SCAN_MODE" != "off" ]]; then
TOS_RC=0
@@ -204,6 +320,65 @@ EOF
cp "$FINAL_OUTPUT" "$CACHE_OUT"
fi
# Dual-emit the framework-independent kernel envelope. Legacy phase reports and
# outputs remain unchanged; the canonical contract is an additive artifact.
TRUST_JSON="$(python3 "$KERNEL_CLI" trust-capabilities 2>/dev/null || true)"
RUN_CERTIFICATION="non_certified"
RUN_ASSURANCE="passed"
[[ "$NATIVE_MODE" == "observe" ]] && RUN_ASSURANCE="degraded"
if [[ "$NATIVE_MODE" == "enforce" && -n "$TRUST_JSON" ]]; then
TRUST_CERTIFIABLE="$(python3 -c 'import json,sys; print("1" if json.load(sys.stdin).get("certifiable") else "0")' <<< "$TRUST_JSON" 2>/dev/null || echo 0)"
REGISTRY_BYPASS="$(printf '%s' "${REGISTRY_JSON:-{}}" | python3 -c 'import json,sys; print("1" if json.load(sys.stdin).get("unsafe_bypass") else "0")' 2>/dev/null || echo 0)"
[[ "$REGISTRY_BYPASS" == "1" ]] && RUN_ASSURANCE="degraded"
[[ "$TRUST_CERTIFIABLE" == "1" && "$REGISTRY_BYPASS" == "0" ]] && RUN_CERTIFICATION="certified"
fi
NATIVE_EVENT="$(CASAN_NATIVE_RISK="$ACTION_RISK_JSON" python3 - "$EXECUTION_ID" "$ACTION_NAME" "$ACTION_CLASS" "$NATIVE_MODE" "$RUN_ASSURANCE" "$RUN_CERTIFICATION" "${CASAN_ACTOR:-${CASAN_AGENT:-}}" "$INPUT_HASH" "$CMD_HASH" <<'PY'
import json, os, sys
run_id, action, action_class, mode, assurance, certification, actor, input_hash, command_hash = sys.argv[1:]
try:
risk = json.loads(os.environ.get("CASAN_NATIVE_RISK") or "{}")
except ValueError:
risk = {}
print(json.dumps({
"run_id": run_id, "correlation_id": run_id, "action": action,
"tool": action, "actor": actor, "mode": mode,
"environment": os.environ.get("CASAN_PROFILE", "development"),
"execution_status": "success", "assurance_status": assurance,
"certification_status": certification,
"policy_decisions": [risk] if risk else [],
"extensions": {"input_hash": input_hash, "command_hash": command_hash, "action_class": action_class},
}, separators=(",", ":")))
PY
)"
KERNEL_RC=0
KERNEL_BUNDLE="$(printf '%s' "$NATIVE_EVENT" | python3 "$KERNEL_CLI" adapt-native -)" || KERNEL_RC=$?
if [[ "$KERNEL_RC" -ne 0 ]]; then
casan_log error harness "KERNEL_CONTRACT_EMISSION_FAILED rc=$KERNEL_RC"
if [[ "$NATIVE_MODE" == "enforce" ]]; then
: > "$FINAL_OUTPUT"
exit 2
fi
RUN_CERTIFICATION="non_certified"
else
KERNEL_PATH="$CASAN_STATE_ROOT/logs/kernel/$EXECUTION_ID.json"
python3 - "$KERNEL_PATH" "$KERNEL_BUNDLE" <<'PY'
import json, os, sys, tempfile
path, raw = sys.argv[1:]
os.makedirs(os.path.dirname(path), exist_ok=True)
payload = json.loads(raw)
fd, tmp = tempfile.mkstemp(prefix=".kernel-", dir=os.path.dirname(path))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
handle.write("\n")
handle.flush(); os.fsync(handle.fileno())
os.replace(tmp, path)
finally:
try: os.unlink(tmp)
except OSError: pass
PY
fi
write_phase_report
casan_log debug harness "action=$ACTION_NAME complete cache=$CACHE_STATUS"
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY output=$FINAL_OUTPUT"
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY execution=success assurance=$RUN_ASSURANCE certification=$RUN_CERTIFICATION output=$FINAL_OUTPUT"
@@ -703,7 +703,13 @@ def submit_escalation(args, router, binding):
def ask(args) -> int:
os.environ["CASAN_PROJECT_ID"] = args.project or "default"
# "default" is the Control Panel's logical fallback, not a manifest project.
# Export only explicit project bindings so generic chat turns can use the
# repository domain fallback without weakening manifest validation.
if args.project and args.project != "default":
os.environ["CASAN_PROJECT_ID"] = args.project
else:
os.environ.pop("CASAN_PROJECT_ID", None)
guard_rc = tenant_runtime_guard(args)
if guard_rc != 0:
return guard_rc
@@ -1,45 +1,62 @@
#!/usr/bin/env python3
"""CASAN-native token-killer (Plan-08 Track 3).
"""CASAN-native deterministic context compressor (Plan-08 Track 3).
A deterministic tool-output compressor written for CASAN — NOT a wrapper around
RTK. It reduces token count of long command/tool output before it enters model
context, while (a) always preserving must-keep lines, (b) never compressing on
failure (raw passthrough for debugging, RTK-style tee), and (c) reporting the
token savings for H6 telemetry.
The compressor reduces long tool output before it enters model context while
preserving operationally significant lines. It is deliberately not a tokenizer
or a billing source: token figures are whitespace-based estimates and are
labelled as such.
Modes:
dedup collapse consecutive duplicate lines with an (xN) counter
extractive keep only important lines (errors/failures/warnings) + must-keep
structural dedup + keep summary/important/must-keep lines (for test/log output)
Safety contract:
* preserve severity-tagged errors/warnings, failure/skip signals, summaries,
operational result lines, and project-supplied must-keep patterns;
* verify preservation after compression, including match multiplicity;
* on a preservation failure, return non-zero and emit raw input by default so
a caller that ignores the exit code still cannot consume a lossy view;
* support halt-with-no-output for callers whose policy forbids raw fallback;
* optionally emit a hash-bound JSON evidence report;
* never compress a failed command when ``--failed`` is supplied.
Governance note: this runs AFTER `H4 scan raw` + `H5 hash raw` and BEFORE
`H4 scan compressed` in the Plan-08 pipeline; it is deterministic and needs no
model, so it cannot be used as a path to evade H4.
Governance ordering remains the caller's responsibility:
H4 scan raw -> H5 hash raw -> compress -> H3 preservation/faithfulness
-> H4 scan compressed -> H5 bind raw/compressed hashes -> model.
"""
import argparse
from collections import Counter
import hashlib
import json
import os
def _casan_app_root():
# Plan-01: walk UP for the `.specify` state marker (harness code lives in
# packages/casan-harness/, so a fixed __file__ depth would mis-root).
_d = os.path.abspath(os.path.dirname(__file__))
_p = _d
while _p != os.path.dirname(_p):
if os.path.isdir(os.path.join(_p, ".specify")) or os.path.isdir(os.path.join(_p, "packages/casan-harness")):
return _p
_p = os.path.dirname(_p)
return os.path.abspath(os.path.join(_d, "..", "..", ".."))
import re
import sys
import tempfile
from typing import Dict, Iterable, List, Pattern, Sequence, Tuple
DEFAULT_MAX_BYTES = 2 * 1024 * 1024
PatternRule = Tuple[str, Pattern[str]]
def _casan_app_root() -> str:
"""Find the project/source root without relying on a fixed file depth."""
directory = os.path.abspath(os.path.dirname(__file__))
current = directory
while current != os.path.dirname(current):
if (
os.path.isdir(os.path.join(current, ".specify"))
or os.path.isdir(os.path.join(current, "packages/casan-harness"))
):
return current
current = os.path.dirname(current)
return os.path.abspath(os.path.join(directory, "..", "..", ".."))
def compression_enabled() -> bool:
"""Read the effective `compression.enabled` from the control-plane settings
store (the harness-owned governed settings). Absent/invalid ⇒ enabled (default).
This is how a Control Plane setting change actually governs the harness."""
"""Return the governed ``compression.enabled`` setting.
An absent or invalid store retains the historical enabled-by-default
behavior. A malformed project-supplied must-keep policy is handled
separately and fails closed.
"""
store_file = os.environ.get(
"CASAN_CP_STORE_FILE",
os.path.join(
@@ -50,145 +67,544 @@ def compression_enabled() -> bool:
if not os.path.isfile(store_file):
return True
try:
data = json.load(open(store_file, encoding="utf-8"))
with open(store_file, encoding="utf-8") as handle:
data = json.load(handle)
setting = data.get("settings", {}).get("compression.enabled")
return True if setting is None else bool(setting["value"])
except (OSError, ValueError, KeyError, TypeError):
return True
IMPORTANT_RE = re.compile(
r"\b(error|errors|fail|failed|failure|failing|exception|panic|denied|blocked|warn|warning)\b",
# Serilog/log4net-style severity tags need explicit recognition. Word-boundary
# matching alone does not classify abbreviations such as ``[WRN]``.
SEVERITY_TAG_RE = re.compile(
r"\[(ERR(?:OR)?|FTL|FATAL|CRIT(?:ICAL)?|WRN|WARN(?:ING)?)\]",
re.IGNORECASE,
)
SUMMARY_RE = re.compile(r"\b(\d+)\s+(pass|passed|fail|failed|tests?|errors?|warnings?)\b", re.IGNORECASE)
IMPORTANT_RE = re.compile(
r"\b("
r"error|errors|fail|failed|failure|failing|exception|panic|"
r"denied|blocked|warn|warning|timeout|timed out|"
r"abort|aborted|cancel|cancelled|skip|skipped|incomplete|"
r"partially completed|partial (?:result|output|analysis|run|failure|success)"
r")\b",
re.IGNORECASE,
)
SUMMARY_RE = re.compile(
r"\b(\d+)\s+(pass|passed|fail|failed|tests?|errors?|warnings?|files?|artifacts?)\b",
re.IGNORECASE,
)
OPERATIONAL_RE = re.compile(
r"\b("
r"exit\s*code|return\s*code|"
r"output\s*(?:path|directory)|"
r"generated\s+\d+\s+(?:files?|artifacts?)"
r")\b",
re.IGNORECASE,
)
DEDUP_SUFFIX_RE = re.compile(r"^(.*) \(x([1-9][0-9]*)\)$")
def estimate_tokens(text: str) -> int:
return len(text.split())
def configured_max_bytes() -> int:
raw = os.environ.get("CASAN_MAX_INPUT_BYTES", str(DEFAULT_MAX_BYTES))
try:
value = int(raw)
except ValueError:
print(
"COMPRESS_FAIL invalid_CASAN_MAX_INPUT_BYTES fail-closed",
file=sys.stderr,
)
raise SystemExit(1)
if value <= 0:
print(
"COMPRESS_FAIL non_positive_CASAN_MAX_INPUT_BYTES fail-closed",
file=sys.stderr,
)
raise SystemExit(1)
return value
# SEC-09 (M-10): bound input size (DoS) and read fail-closed. Non-UTF8 degrades via
# errors="replace" instead of crashing; oversize/unreadable input exits non-zero and
# emits nothing (never a crash traceback, never silent truncation).
MAX_BYTES = int(os.environ.get("CASAN_MAX_INPUT_BYTES", str(2 * 1024 * 1024)))
def read_capped(src: str) -> str:
def read_capped(src: str, max_bytes: int) -> Tuple[str, int]:
"""Read at most ``max_bytes`` and return text plus replacement count."""
try:
if src == "-":
data = sys.stdin.buffer.read(MAX_BYTES + 1)
data = sys.stdin.buffer.read(max_bytes + 1)
else:
with open(src, "rb") as fh:
data = fh.read(MAX_BYTES + 1)
with open(src, "rb") as handle:
data = handle.read(max_bytes + 1)
except OSError as exc:
print(f"COMPRESS_FAIL unreadable_input: {exc}", file=sys.stderr)
raise SystemExit(1)
if len(data) > MAX_BYTES:
print(f"COMPRESS_FAIL input_exceeds_cap({MAX_BYTES}B) fail-closed", file=sys.stderr)
if len(data) > max_bytes:
print(
f"COMPRESS_FAIL input_exceeds_cap({max_bytes}B) fail-closed",
file=sys.stderr,
)
raise SystemExit(1)
return data.decode("utf-8", errors="replace")
text = data.decode("utf-8", errors="replace")
return text, text.count("\ufffd")
def load_patterns(path: str):
def load_patterns(path: str) -> List[PatternRule]:
"""Load and compile one regex per line, ignoring blank/comment lines."""
if not path:
return []
try:
with open(path, encoding="utf-8", errors="replace") as fh:
return [line.strip() for line in fh if line.strip()]
with open(path, encoding="utf-8", errors="replace") as handle:
raw_rules = [
(line_number, line.strip())
for line_number, line in enumerate(handle, start=1)
if line.strip() and not line.lstrip().startswith("#")
]
except OSError as exc:
print(f"COMPRESS_FAIL must_keep_file_unreadable: {exc}", file=sys.stderr)
raise SystemExit(1)
def is_must_keep(line: str, patterns) -> bool:
return any(re.search(p, line) for p in patterns)
rules: List[PatternRule] = []
for line_number, expression in raw_rules:
try:
rules.append((expression, re.compile(expression)))
except re.error as exc:
print(
"COMPRESS_FAIL invalid_must_keep_regex "
f"file={path} line={line_number}: {exc}",
file=sys.stderr,
)
raise SystemExit(1)
return rules
def dedup(lines):
out = []
i = 0
n = len(lines)
while i < n:
j = i
while j + 1 < n and lines[j + 1] == lines[i]:
j += 1
count = j - i + 1
out.append(lines[i] if count == 1 else f"{lines[i]} (x{count})")
i = j + 1
return out
def is_must_keep(line: str, patterns: Sequence[PatternRule]) -> bool:
return any(pattern.search(line) for _, pattern in patterns)
def compress(text: str, mode: str, must):
lines = text.split("\n")
if mode == "dedup":
def is_protected(line: str, patterns: Sequence[PatternRule]) -> bool:
"""Return whether a line is forbidden from disappearing."""
return bool(
SEVERITY_TAG_RE.search(line)
or IMPORTANT_RE.search(line)
or SUMMARY_RE.search(line)
or OPERATIONAL_RE.search(line)
or is_must_keep(line, patterns)
)
def estimate_tokens(text: str) -> int:
"""Return a whitespace estimate, never provider/billed token usage."""
return len(text.split())
def dedup(lines: Sequence[str]) -> List[str]:
"""Collapse consecutive duplicate lines while retaining multiplicity."""
output: List[str] = []
index = 0
count_lines = len(lines)
while index < count_lines:
end = index
while end + 1 < count_lines and lines[end + 1] == lines[index]:
end += 1
occurrences = end - index + 1
output.append(
lines[index]
if occurrences == 1
else f"{lines[index]} (x{occurrences})"
)
index = end + 1
return output
def compress(
text: str,
mode: str,
must_keep: Sequence[PatternRule],
) -> List[str]:
"""Create a candidate compressed view.
Preservation is verified independently after this function returns.
"""
lines = compression_source_lines(text.split("\n"), mode, must_keep)
if mode in {"dedup", "structural"}:
return dedup(lines)
if mode == "extractive":
return [ln for ln in lines if IMPORTANT_RE.search(ln) or is_must_keep(ln, must)]
if mode == "structural":
kept = [
ln
for ln in lines
if IMPORTANT_RE.search(ln) or SUMMARY_RE.search(ln) or is_must_keep(ln, must)
]
return dedup(kept)
return lines
raise ValueError(f"unknown mode: {mode}")
def compression_source_lines(
raw_lines: Sequence[str],
mode: str,
must_keep: Sequence[PatternRule],
) -> List[str]:
"""Return the exact raw lines from which a candidate may be built."""
if mode == "dedup":
return list(raw_lines)
return [line for line in raw_lines if is_protected(line, must_keep)]
def expanded_line_counts(
lines: Iterable[str],
dedup_encoded: bool,
source_lines: Sequence[str],
) -> Counter:
"""Decode ``(xN)`` markers produced by ``dedup`` into weighted counts."""
candidate_lines = list(lines)
# This is the normal path and resolves the otherwise ambiguous case where
# a real log line itself ends in ``(xN)``.
if dedup_encoded and candidate_lines == dedup(source_lines):
return Counter(source_lines)
counts: Counter = Counter()
for line in candidate_lines:
match = DEDUP_SUFFIX_RE.match(line) if dedup_encoded else None
if match:
counts[match.group(1)] += int(match.group(2))
else:
counts[line] += 1
return counts
def protected_line_deficits(
source_lines: Sequence[str],
candidate_lines: Sequence[str],
dedup_encoded: bool,
) -> Dict[str, int]:
"""Return exact protected-line multiplicity missing from the candidate."""
expected = Counter(source_lines)
actual = expanded_line_counts(
candidate_lines,
dedup_encoded=dedup_encoded,
source_lines=source_lines,
)
return {
line: expected_count - actual.get(line, 0)
for line, expected_count in expected.items()
if actual.get(line, 0) < expected_count
}
def required_pattern_deficits(
raw_lines: Sequence[str],
source_lines: Sequence[str],
candidate_lines: Sequence[str],
required: Sequence[PatternRule],
dedup_encoded: bool,
) -> List[Dict[str, object]]:
"""Verify every required match present in raw remains in the candidate.
A pattern absent from raw is not a failure: the invariant is preservation,
not fabrication. Match multiplicity prevents one surviving warning from
hiding the loss of a second warning matched by the same rule.
"""
deficits: List[Dict[str, object]] = []
actual_lines = expanded_line_counts(
candidate_lines,
dedup_encoded=dedup_encoded,
source_lines=source_lines,
)
for expression, pattern in required:
expected = sum(1 for line in raw_lines if pattern.search(line))
actual = sum(
count
for line, count in actual_lines.items()
if pattern.search(line)
)
if actual < expected:
deficits.append(
{
"pattern": expression,
"expected": expected,
"actual": actual,
"missing": expected - actual,
}
)
return deficits
def severity_counts(text: str) -> Dict[str, int]:
counts = {"error": 0, "warning": 0, "critical": 0}
for line in text.splitlines():
match = SEVERITY_TAG_RE.search(line)
if not match:
continue
level = match.group(1).upper()
if level.startswith(("ERR",)):
counts["error"] += 1
elif level.startswith(("WRN", "WARN")):
counts["warning"] += 1
else:
counts["critical"] += 1
return counts
def sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def deficit_fingerprints(deficits: Dict[str, int]) -> List[Dict[str, object]]:
"""Report hashes, not potentially sensitive raw lines."""
return [
{"line_sha256": sha256_text(line), "missing": missing}
for line, missing in sorted(deficits.items())
]
def write_json_report(path: str, report: Dict[str, object]) -> None:
"""Atomically write a private evidence report."""
if path == "-":
print(
"COMPRESS_FAIL report_json_stdout_conflicts_with_compressed_output",
file=sys.stderr,
)
raise SystemExit(1)
absolute = os.path.abspath(path)
directory = os.path.dirname(absolute)
try:
os.makedirs(directory, exist_ok=True)
descriptor, temporary_path = tempfile.mkstemp(
prefix=".context-compress-",
suffix=".json.tmp",
dir=directory,
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
json.dump(
report,
handle,
ensure_ascii=False,
indent=2,
sort_keys=True,
)
handle.write("\n")
os.chmod(temporary_path, 0o600)
os.replace(temporary_path, absolute)
except Exception:
try:
os.unlink(temporary_path)
except OSError:
pass
raise
except OSError as exc:
print(f"COMPRESS_FAIL report_write_failed: {exc}", file=sys.stderr)
raise SystemExit(1)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--mode", choices=["dedup", "extractive", "structural"], default="structural")
ap.add_argument("--input", default="-", help="input file or - for stdin")
ap.add_argument("--must-keep-file", default="", help="file with one must-keep regex per line")
ap.add_argument("--failed", action="store_true", help="raw passthrough (tee) when the command failed")
ap.add_argument(
parser = argparse.ArgumentParser()
parser.add_argument(
"--mode",
choices=["dedup", "extractive", "structural"],
default="structural",
)
parser.add_argument("--input", default="-", help="input file or - for stdin")
parser.add_argument(
"--must-keep-file",
default="",
help="file with one must-keep regex per line",
)
parser.add_argument(
"--failed",
action="store_true",
help="raw passthrough when the producing command failed",
)
parser.add_argument(
"--respect-policy",
action="store_true",
help="honor control-plane `compression.enabled`; if disabled, pass raw through",
help="honor control-plane compression.enabled",
)
ap.add_argument(
parser.add_argument(
"--require-must-keep-file",
default="",
help="verify every pattern in this file still appears; exit 1 (gate) if any is missing",
help=(
"verify every match present in raw remains in the candidate; "
"match multiplicity is enforced"
),
)
args = ap.parse_args()
parser.add_argument(
"--on-preservation-failure",
choices=["raw", "halt"],
default="raw",
help=(
"raw: emit raw input and return 1; "
"halt: emit nothing and return 1"
),
)
parser.add_argument(
"--report-json",
default="",
help="optional atomic JSON evidence report path",
)
args = parser.parse_args()
raw = read_capped(args.input)
must = load_patterns(args.must_keep_file)
raw, decode_replacements = read_capped(
args.input,
configured_max_bytes(),
)
must_keep = load_patterns(args.must_keep_file)
required = load_patterns(args.require_must_keep_file)
raw_lines = raw.split("\n")
exit_code = 0
preservation_status = "not_applicable"
pattern_deficits: List[Dict[str, object]] = []
line_deficits: Dict[str, int] = {}
if args.failed:
# RTK-style tee: never compress failing output; keep raw for debugging.
out_text = raw
candidate_text = raw
output_text = raw
mode_used = "passthrough"
decision = "failed-command-raw-passthrough"
saving_status = "not_compressed"
elif args.respect_policy and not compression_enabled():
# Control-plane setting governs the harness: compression disabled ⇒ raw.
out_text = raw
candidate_text = raw
output_text = raw
mode_used = "policy-disabled"
decision = "policy-disabled-raw-passthrough"
saving_status = "not_compressed"
else:
out_lines = compress(raw, args.mode, must)
out_text = "\n".join(out_lines)
mode_used = args.mode
source_lines = compression_source_lines(
raw_lines,
args.mode,
must_keep,
)
candidate_lines = compress(raw, args.mode, must_keep)
candidate_text = "\n".join(candidate_lines)
line_deficits = protected_line_deficits(
source_lines,
candidate_lines,
dedup_encoded=args.mode in {"dedup", "structural"},
)
pattern_deficits = required_pattern_deficits(
raw_lines,
source_lines,
candidate_lines,
required,
dedup_encoded=args.mode in {"dedup", "structural"},
)
empty_loss = bool(raw.strip()) and not candidate_text.strip()
preservation_failed = bool(
line_deficits or pattern_deficits or empty_loss
)
in_tokens = estimate_tokens(raw)
out_tokens = estimate_tokens(out_text)
saved = in_tokens - out_tokens
ratio = round(out_tokens / in_tokens, 4) if in_tokens else 1.0
if preservation_failed:
preservation_status = "failed"
exit_code = 1
mode_used = (
"fallback-raw"
if args.on_preservation_failure == "raw"
else "halt"
)
decision = "compression-rejected"
saving_status = "rejected"
output_text = (
raw if args.on_preservation_failure == "raw" else ""
)
else:
preservation_status = "passed"
mode_used = args.mode
decision = "compressed"
saving_status = "estimate_only_quality_gate_required"
output_text = candidate_text
verify_patterns = load_patterns(args.require_must_keep_file)
missing = [p for p in verify_patterns if not re.search(p, out_text)]
input_estimate = estimate_tokens(raw)
candidate_estimate = estimate_tokens(candidate_text)
output_estimate = estimate_tokens(output_text)
if saving_status == "rejected":
# A rejected candidate has no claimable saving even when halt policy
# intentionally emits zero bytes.
estimated_saved = 0
estimated_ratio = 1.0
else:
estimated_saved = input_estimate - output_estimate
estimated_ratio = (
round(output_estimate / input_estimate, 4)
if input_estimate
else 1.0
)
emitted_text = output_text
if emitted_text and not emitted_text.endswith("\n"):
emitted_text += "\n"
sys.stdout.write(out_text)
if not out_text.endswith("\n"):
sys.stdout.write("\n")
report: Dict[str, object] = {
"schema_version": 1,
"measurement_source": "whitespace_estimate",
"provider_telemetry": None,
"mode_requested": args.mode,
"mode_used": mode_used,
"decision": decision,
"exit_code": exit_code,
"saving_status": saving_status,
"preservation": {
"status": preservation_status,
"protected_line_missing": sum(line_deficits.values()),
"protected_line_deficits": deficit_fingerprints(line_deficits),
"required_pattern_deficits": pattern_deficits,
},
"estimated_tokens": {
"input": input_estimate,
"candidate": candidate_estimate,
"output": output_estimate,
"saved": estimated_saved,
"ratio": estimated_ratio,
},
"severity": {
"raw": severity_counts(raw),
"candidate": severity_counts(candidate_text),
"output": severity_counts(output_text),
},
"hashes": {
"raw_sha256": sha256_text(raw),
"candidate_sha256": sha256_text(candidate_text),
"output_sha256": sha256_text(emitted_text),
},
"bytes": {
"raw": len(raw.encode("utf-8")),
"candidate": len(candidate_text.encode("utf-8")),
"output": len(emitted_text.encode("utf-8")),
},
"decode_replacement_count": decode_replacements,
"requires_downstream_h3_quality_gate": decision == "compressed",
"requires_downstream_h4_compressed_scan": decision == "compressed",
"requires_downstream_h5_audit_binding": decision == "compressed",
}
if args.report_json:
write_json_report(args.report_json, report)
sys.stdout.write(emitted_text)
print(
f"COMPRESS mode={mode_used} in_tokens={in_tokens} out_tokens={out_tokens} "
f"saved={saved} ratio={ratio} must_keep_missing={len(missing)}",
"COMPRESS "
f"mode={mode_used} "
f"measurement_source=whitespace_estimate "
f"in_tokens={input_estimate} "
f"candidate_tokens={candidate_estimate} "
f"out_tokens={output_estimate} "
f"saved={estimated_saved} "
f"ratio={estimated_ratio} "
f"preservation={preservation_status} "
f"saving_status={saving_status} "
f"must_keep_missing={sum(int(item['missing']) for item in pattern_deficits)} "
f"protected_missing={sum(line_deficits.values())}",
file=sys.stderr,
)
if missing:
print(f"COMPRESS_MUST_KEEP_DROPPED {','.join(missing)}", file=sys.stderr)
return 1
return 0
if pattern_deficits:
details = ",".join(
f"{item['pattern']}({item['actual']}/{item['expected']})"
for item in pattern_deficits
)
print(f"COMPRESS_MUST_KEEP_DROPPED {details}", file=sys.stderr)
if line_deficits:
print(
"COMPRESS_PROTECTED_CONTENT_DROPPED "
f"occurrences={sum(line_deficits.values())}",
file=sys.stderr,
)
if preservation_status == "failed":
print(
"COMPRESS_REJECTED "
f"fallback={args.on_preservation_failure}",
file=sys.stderr,
)
return exit_code
if __name__ == "__main__":
@@ -58,27 +58,48 @@ TRACE_ID="$(new_trace_id)"
TIMESTAMP="$(timestamp)"
INPUT="$(cat "$INPUT_FILE")"
LOWER_INPUT="$(printf '%s' "$INPUT" | tr '[:upper:]' '[:lower:]')"
ACTOR="${CASAN_ACTOR:-developer}"
ACTOR="${CASAN_ACTOR:-${CASAN_AGENT:-}}"
APPROVER="${CASAN_APPROVER:-}"
APPROVAL_DECISION="${CASAN_APPROVAL_DECISION:-auto}"
AUDIT_LOG="$AUDIT_DIR/audit.jsonl"
RISK_LEVEL="low"
REASONS=()
case "$ACTION_NAME" in
deploy|launch|write_code|write_file|migration|db_write|external_api|tool_call)
RISK_LEVEL="medium"
REASONS+=("sensitive-action:$ACTION_NAME")
;;
esac
if printf '%s' "$LOWER_INPUT" | grep -Eq "(delete|drop table|password|api[_-]?key|secret|token|credential|migration|deploy|external api|shutdown|dump database)"; then
ACTION_CLASS="unknown"
RISK_FACTORS_JSON='{"action_risk":"high","content_risk":"high","environment_risk":"low","identity_risk":"low","resource_risk":"low"}'
EVIDENCE_REQUIREMENT="required"
RISK_POLICY_DECISION="require_approval"
KERNEL_CLI="$CASAN_HARNESS_ROOT/scripts/python/kernel_cli.py"
RISK_JSON=""
RISK_RC=0
if [[ -f "$KERNEL_CLI" ]]; then
RISK_JSON="$(python3 "$KERNEL_CLI" risk --action "$ACTION_NAME" --tool "$ACTION_NAME" \
--content-file "$INPUT_FILE" --actor "$ACTOR" --environment "${CASAN_PROFILE:-development}" 2>/dev/null)" || RISK_RC=$?
else
RISK_RC=127
fi
if [[ "$RISK_RC" -eq 0 && -n "$RISK_JSON" ]]; then
RISK_FIELDS="$(python3 - "$RISK_JSON" <<'PY'
import json, sys
payload = json.loads(sys.argv[1])
print("\t".join([
str(payload["action_class"]),
str(payload["effective_risk"]),
json.dumps(payload["risk_factors"], sort_keys=True, separators=(",", ":")),
str(payload["evidence_requirement"]),
str(payload["decision"]),
]))
PY
)" || RISK_RC=$?
fi
if [[ "$RISK_RC" -eq 0 && -n "${RISK_FIELDS:-}" ]]; then
IFS=$'\t' read -r ACTION_CLASS RISK_LEVEL RISK_FACTORS_JSON EVIDENCE_REQUIREMENT RISK_POLICY_DECISION <<< "$RISK_FIELDS"
REASONS+=("action-risk-floor:$ACTION_CLASS")
else
RISK_LEVEL="high"
REASONS+=("high-risk-content")
elif printf '%s' "$LOWER_INPUT" | grep -Eq "(internal|config|system|policy|permission)"; then
[[ "$RISK_LEVEL" == "low" ]] && RISK_LEVEL="medium"
REASONS+=("medium-risk-content")
ACTION_CLASS="unknown"
RISK_POLICY_DECISION="require_approval"
REASONS+=("action-risk-classifier-failed-closed")
fi
APPROVAL_STATUS="auto_approved"
@@ -88,8 +109,14 @@ if [[ "$RISK_LEVEL" == "medium" ]]; then
APPROVAL_STATUS="policy_auto_approved_with_audit"
fi
if [[ "$RISK_LEVEL" == "high" ]]; then
if [[ "${CASAN_APPROVAL_STRICT:-0}" == "1" ]]; then
if [[ "$RISK_POLICY_DECISION" == "deny" ]]; then
APPROVAL_STATUS="actor_identity_required"
DECISION="denied"
REASONS+=("actor-identity-required")
elif [[ "$RISK_LEVEL" == "high" || "$RISK_LEVEL" == "critical" || "$RISK_POLICY_DECISION" == "require_approval" ]]; then
APPROVAL_STRICT_EFFECTIVE="${CASAN_APPROVAL_STRICT:-0}"
[[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]] && APPROVAL_STRICT_EFFECTIVE="1"
if [[ "$APPROVAL_STRICT_EFFECTIVE" == "1" ]]; then
# Approval-identity mode (V20): an env-var approver is NOT enough — the
# reviewer must cryptographically SIGN this exact request and their role must
# be authorized for the action. SoD (actor != approver) still enforced.
@@ -147,7 +174,7 @@ fi
REASONS_JSON="$(printf '%s\n' "${REASONS[@]:-}" | python -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
# approver and output_hash are part of the hashed core so they cannot be
# silently mutated after the fact.
RECORD_CORE="$(printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s' "$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" "$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH")"
RECORD_CORE="$(printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s' "$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTION_CLASS" "$ACTOR" "$RISK_LEVEL" "$RISK_FACTORS_JSON" "$EVIDENCE_REQUIREMENT" "$DECISION" "$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH")"
RECORD_HASH="$(printf '%s' "$RECORD_CORE" | hash_text)"
TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
@@ -160,18 +187,24 @@ TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
# written (disk full, read-only, quota), there must be NO governed action without
# its accountability record — deny and empty the output rather than proceed.
if ! CASAN_GC_REASONS="$REASONS_JSON" python - "$TRACE_FILE" "$AUDIT_LOG" \
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" \
"$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTION_CLASS" "$ACTOR" "$RISK_LEVEL" \
"$RISK_FACTORS_JSON" "$EVIDENCE_REQUIREMENT" "$DECISION" "$APPROVAL_STATUS" \
"$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
import json, os, sys
(trace_file, audit_log, ts, trace_id, action, actor, risk, decision,
approval_status, approver, input_hash, output_hash, prev_hash, record_hash) = sys.argv[1:]
(trace_file, audit_log, ts, trace_id, action, action_class, actor, risk,
risk_factors_json, evidence_requirement, decision, approval_status, approver,
input_hash, output_hash, prev_hash, record_hash) = sys.argv[1:]
try:
reasons = json.loads(os.environ.get("CASAN_GC_REASONS") or "[]")
except ValueError:
reasons = []
rec = {
"schema_version": 2, "category": "runtime_control",
"timestamp": ts, "trace_id": trace_id, "harness": "H5-governance",
"action": action, "actor": actor, "risk_level": risk, "decision": decision,
"action": action, "action_class": action_class, "actor": actor,
"risk_level": risk, "effective_risk": risk,
"risk_factors": json.loads(risk_factors_json),
"evidence_requirement": evidence_requirement, "decision": decision,
"approval_status": approval_status, "approver": approver,
"input_hash": input_hash, "output_hash": output_hash,
"previous_record_hash": prev_hash, "record_hash": record_hash,
@@ -196,8 +229,43 @@ fi
# --- External anchor: cryptographically sign the new chain head ---
# A re-forged chain (recomputed hashes) changes the head; without the private
# key the attacker cannot produce a matching signature, so verification fails.
# Production note: the private key must live off-repo (KMS/HSM). It is local
# here only for self-contained demonstration.
# Development may use a local key for self-contained demonstration. Production
# refuses that path unless an explicit emergency override is visible in evidence.
PRODUCTION_PROFILE=0
[[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]] && PRODUCTION_PROFILE=1
EMERGENCY_TRUST_OVERRIDE="${CASAN_TRUST_EMERGENCY_OVERRIDE:-0}"
TRUST_LOG="$CASAN_STATE_ROOT/logs/readiness/trust-capabilities.jsonl"
mkdir -p "$(dirname "$TRUST_LOG")"
if [[ "$PRODUCTION_PROFILE" == "1" && "$EMERGENCY_TRUST_OVERRIDE" != "1" ]]; then
TRUST_RC=0
TRUST_JSON="$(python3 "$KERNEL_CLI" trust-capabilities 2>/dev/null)" || TRUST_RC=$?
if [[ "$TRUST_RC" -ne 0 ]]; then
if [[ -n "$TRUST_JSON" ]]; then
printf '%s\n' "$TRUST_JSON" >> "$TRUST_LOG"
else
printf '{"ready":false,"severity":"critical","reason_codes":["production_trust_configuration_invalid"]}\n' >> "$TRUST_LOG"
fi
: > "$OUTPUT_FILE"
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=production_trust_root_unavailable" >&2
exit 2
fi
printf '%s\n' "$TRUST_JSON" >> "$TRUST_LOG"
if ! bash "$SCRIPT_DIR/sign-audit-head.sh" "$AUDIT_LOG" >/dev/null 2>&1; then
: > "$OUTPUT_FILE"
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=external_signing_failed" >&2
exit 2
fi
if ! bash "$SCRIPT_DIR/audit-ship-s3.sh" "$AUDIT_DIR/audit-head.txt" >/dev/null 2>&1; then
: > "$OUTPUT_FILE"
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=external_immutable_anchor_failed" >&2
exit 2
fi
else
if [[ "$PRODUCTION_PROFILE" == "1" ]]; then
printf '{"schema_version":"1.0.0","profile":"production","ready":false,"certifiable":false,"emergency_override":true,"severity":"critical","reason_codes":["emergency_local_trust_override_active"]}\n' >> "$TRUST_LOG"
echo "CRITICAL: emergency local trust override active; execution cannot be production-certified" >&2
fi
if command -v openssl >/dev/null 2>&1; then
# Private signing key lives OFF-REPO (default ~/.casan/audit-keys); only the
# public key is committed. Production: replace with KMS/HSM.
@@ -207,7 +275,7 @@ if command -v openssl >/dev/null 2>&1; then
AUDIT_PUB="$PUB_DIR/audit-public.pem"
mkdir -p "$PUB_DIR" "$PRIV_DIR"
if [[ ! -f "$AUDIT_PRIV" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then
# SEC-02 (H-02): in enforced mode NEVER auto-generate a local signing key.
# A freshly-minted key next to the data lets any file-writer re-sign a forged
# head. Prod must provision the key out-of-band (KMS/HSM — see sign-audit-head.sh
@@ -229,6 +297,7 @@ if command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 -sign "$AUDIT_PRIV" -out "$AUDIT_DIR/audit-head.sig" "$AUDIT_DIR/audit-head.txt" 2>/dev/null || true
fi
fi
fi
if [[ "$DECISION" != "approved" ]]; then
: > "$OUTPUT_FILE"
@@ -21,22 +21,22 @@ while IFS= read -r raw || [[ -n "$raw" ]]; do
[[ "$line" =~ ^([A-Z0-9_]+)=(.*)$ ]] || fail "invalid_env_syntax"
key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}"
case "$key" in
CASAN_PUBLIC_FQDN|CASAN_CP_HTTPS_PORT|CASAN_CP_TLS_DIR|CASAN_CP_OAUTH_ENV|CASAN_CP_RUNTIME_ENV|CASAN_CP_VAULT_ENV|CASAN_CP_STATE_DIR|CASAN_CP_OUTPUT_DIR|CASAN_CP_API_IMAGE|CASAN_CP_UI_IMAGE|CASAN_S3_BUCKET|CASAN_S3_PREFIX|CASAN_S3_REGION|CASAN_S3_RETENTION_DAYS|CASAN_S3_KMS_KEY_ID) export "$key=$value" ;;
CASAN_PUBLIC_FQDN|CASAN_CP_HTTPS_PORT|CASAN_CP_TLS_DIR|CASAN_CP_OAUTH_ENV|CASAN_CP_RUNTIME_ENV|CASAN_CP_VAULT_ENV|CASAN_CP_STATE_DIR|CASAN_CP_OUTPUT_DIR|CASAN_CP_IDP_PUBLIC_KEY|CASAN_CP_API_IMAGE|CASAN_CP_UI_IMAGE|CASAN_CP_OAUTH2_PROXY_IMAGE|CASAN_S3_BUCKET|CASAN_S3_PREFIX|CASAN_S3_REGION|CASAN_S3_RETENTION_DAYS|CASAN_S3_KMS_KEY_ID) export "$key=$value" ;;
*) fail "unexpected_env_key key=$key" ;;
esac
done < "$ENV_FILE"
required=(CASAN_PUBLIC_FQDN CASAN_CP_TLS_DIR CASAN_CP_OAUTH_ENV CASAN_CP_RUNTIME_ENV CASAN_CP_VAULT_ENV CASAN_CP_STATE_DIR CASAN_CP_OUTPUT_DIR CASAN_CP_API_IMAGE CASAN_CP_UI_IMAGE CASAN_S3_BUCKET CASAN_S3_REGION CASAN_S3_KMS_KEY_ID)
required=(CASAN_PUBLIC_FQDN CASAN_CP_TLS_DIR CASAN_CP_OAUTH_ENV CASAN_CP_RUNTIME_ENV CASAN_CP_VAULT_ENV CASAN_CP_STATE_DIR CASAN_CP_OUTPUT_DIR CASAN_CP_IDP_PUBLIC_KEY CASAN_CP_API_IMAGE CASAN_CP_UI_IMAGE CASAN_CP_OAUTH2_PROXY_IMAGE CASAN_S3_BUCKET CASAN_S3_REGION CASAN_S3_KMS_KEY_ID)
for key in "${required[@]}"; do [[ -n "${!key:-}" ]] || fail "missing_env key=$key"; done
case "$CASAN_PUBLIC_FQDN" in *localhost*|*127.0.0.1*|*example.com*|*replace-with*|*/*|[0-9]* ) fail "invalid_fqdn";; esac
[[ "$CASAN_PUBLIC_FQDN" == *.* ]] || fail "fqdn_required"
for image in "$CASAN_CP_API_IMAGE" "$CASAN_CP_UI_IMAGE"; do
for image in "$CASAN_CP_API_IMAGE" "$CASAN_CP_UI_IMAGE" "$CASAN_CP_OAUTH2_PROXY_IMAGE"; do
[[ "$image" =~ @sha256:[a-f0-9]{64}$ ]] || fail "image_must_be_digest_pinned image=$image"
done
pass "public FQDN and images are production-safe"
for file in "$CASAN_CP_TLS_DIR/tls.crt" "$CASAN_CP_TLS_DIR/tls.key" "$CASAN_CP_OAUTH_ENV" "$CASAN_CP_RUNTIME_ENV" "$CASAN_CP_VAULT_ENV"; do
for file in "$CASAN_CP_TLS_DIR/tls.crt" "$CASAN_CP_TLS_DIR/tls.key" "$CASAN_CP_IDP_PUBLIC_KEY" "$CASAN_CP_OAUTH_ENV" "$CASAN_CP_RUNTIME_ENV" "$CASAN_CP_VAULT_ENV"; do
[[ -s "$file" ]] || fail "missing_or_empty path=$file"
done
openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -noout >/dev/null || fail "invalid_tls_certificate"
@@ -45,6 +45,7 @@ openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -noout -checkhost "$CASAN_PUBLIC_FQ
cert_pub="$(openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 | awk '{print $NF}')"
key_pub="$(openssl pkey -in "$CASAN_CP_TLS_DIR/tls.key" -pubout -outform DER | openssl dgst -sha256 | awk '{print $NF}')"
[[ "$cert_pub" == "$key_pub" ]] || fail "tls_key_does_not_match_certificate"
openssl rsa -pubin -in "$CASAN_CP_IDP_PUBLIC_KEY" -noout -modulus >/dev/null 2>&1 || fail "invalid_idp_rsa_public_key"
pass "TLS certificate is valid for at least 30 days"
value_of() { sed -n -E "s/^${1}=//p" "$CASAN_CP_OAUTH_ENV" | tail -1; }
@@ -56,8 +57,24 @@ done
[[ "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" == https://* ]] || fail "oidc_issuer_https_required"
[[ "$(value_of OAUTH2_PROXY_REDIRECT_URL)" == "https://$CASAN_PUBLIC_FQDN/oauth2/callback" ]] || fail "oidc_redirect_mismatch"
[[ "$(value_of OAUTH2_PROXY_COOKIE_SECURE)" == true ]] || fail "oidc_secure_cookie_required"
[[ "$(value_of OAUTH2_PROXY_SET_XAUTHREQUEST)" == true ]] || fail "oidc_xauthrequest_required"
[[ "$(value_of OAUTH2_PROXY_PASS_ACCESS_TOKEN)" == true ]] || fail "oidc_access_token_forwarding_required"
[[ "$(value_of OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER)" == true ]] || fail "oidc_authorization_header_forwarding_required"
pass "enterprise OIDC configuration"
runtime_value_of() { sed -n -E "s/^${1}=//p" "$CASAN_CP_RUNTIME_ENV" | tail -1; }
[[ "$(runtime_value_of CASAN_PROFILE)" == prod ]] || fail "runtime_profile_must_be_prod"
[[ "$(runtime_value_of CASAN_CP_AUTH_MODE)" == jwt ]] || fail "runtime_jwt_auth_required"
[[ "$(runtime_value_of CASAN_CP_JWT_ISSUER)" == "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" ]] || fail "runtime_oidc_issuer_mismatch"
[[ "$(runtime_value_of CASAN_CP_JWT_AUDIENCE)" == "$(value_of OAUTH2_PROXY_CLIENT_ID)" ]] || fail "runtime_oidc_audience_mismatch"
[[ "$(runtime_value_of CASAN_CP_JWT_PUBLIC_KEY_FILE)" == /run/casan-idp/idp-public.pem ]] || fail "runtime_idp_public_key_path_invalid"
[[ "$(runtime_value_of CASAN_SIGNING_PROVIDER)" == vault_kms ]] || fail "runtime_external_signing_required"
[[ "$(runtime_value_of CASAN_IMMUTABLE_ANCHOR_PROVIDER)" == s3_object_lock ]] || fail "runtime_immutable_anchor_required"
[[ -z "$(runtime_value_of CASAN_CP_TRUST_AUTH_PROXY)" ]] || fail "legacy_trusted_header_auth_forbidden"
clock_skew="$(runtime_value_of CASAN_CP_JWT_CLOCK_SKEW_SECONDS)"
[[ "$clock_skew" =~ ^[0-9]+$ && "$clock_skew" -le 300 ]] || fail "runtime_jwt_clock_skew_invalid"
pass "Control Plane verifies OIDC token identity cryptographically"
vault_addr="$(sed -n -E 's/^VAULT_ADDR=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
vault_token="$(sed -n -E 's/^VAULT_TOKEN=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
vault_cacert="$(sed -n -E 's/^VAULT_CACERT=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
@@ -88,7 +105,8 @@ COMPOSE="$ROOT/docker-compose.control-panel.yml"
CASAN_CP_TLS_DIR="$CASAN_CP_TLS_DIR" CASAN_CP_OAUTH_ENV="$CASAN_CP_OAUTH_ENV" \
CASAN_CP_RUNTIME_ENV="$CASAN_CP_RUNTIME_ENV" CASAN_CP_VAULT_ENV="$CASAN_CP_VAULT_ENV" \
CASAN_CP_STATE_DIR="$CASAN_CP_STATE_DIR" CASAN_CP_OUTPUT_DIR="$CASAN_CP_OUTPUT_DIR" \
CASAN_CP_API_IMAGE="$CASAN_CP_API_IMAGE" CASAN_CP_UI_IMAGE="$CASAN_CP_UI_IMAGE" \
CASAN_CP_IDP_PUBLIC_KEY="$CASAN_CP_IDP_PUBLIC_KEY" CASAN_CP_API_IMAGE="$CASAN_CP_API_IMAGE" \
CASAN_CP_UI_IMAGE="$CASAN_CP_UI_IMAGE" CASAN_CP_OAUTH2_PROXY_IMAGE="$CASAN_CP_OAUTH2_PROXY_IMAGE" \
docker compose -f "$COMPOSE" config >/dev/null || fail "compose_config_invalid"
pass "production compose config"
@@ -47,10 +47,15 @@ docker info >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_DOCKER_DOWN" >&2; exit
# rootful Docker daemon because a compromised daemon socket defeats container
# isolation. Local developer/test profiles may use a rootful daemon, but cannot
# claim that configuration as a hardened production runner.
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \
|| "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'rootless' \
|| { echo "SANDBOX_CONTAINER_ROOTLESS_REQUIRED" >&2; exit 2; }
fi
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
[[ "$IMAGE" =~ @sha256:[a-f0-9]{64}$ ]] \
|| { echo "SANDBOX_CONTAINER_IMAGE_DIGEST_REQUIRED image=$IMAGE" >&2; exit 2; }
fi
WS_ABS="$(cd "$WORKSPACE" 2>/dev/null && pwd)" || { echo "SANDBOX_CONTAINER_BAD_WORKSPACE" >&2; exit 2; }
@@ -36,6 +36,37 @@ CPU_SECONDS="${CASAN_SANDBOX_CPU_SECONDS:-30}"
# check is the real gate; a container --pids-limit is the production backstop.
MAX_PROCS="${CASAN_SANDBOX_MAX_PROCS:-}"
TIMEOUT="${CASAN_SANDBOX_TIMEOUT:-30}"
SANDBOX_MODE="${CASAN_SANDBOX_MODE:-}"
STRICT_SANDBOX=0
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \
|| "${CASAN_ENFORCEMENT_MODE:-}" == "enforce" || "${CASAN_SANDBOX_STRICT:-0}" == "1" ]]; then
STRICT_SANDBOX=1
fi
[[ -n "$SANDBOX_MODE" ]] || { if [[ "$STRICT_SANDBOX" == "1" ]]; then SANDBOX_MODE="container"; else SANDBOX_MODE="static"; fi; }
record_sandbox() { # decision reason backend capability-json
local decision="$1" reason="$2" backend="$3" capabilities="$4"
local log="$CASAN_STATE_ROOT/logs/sandbox/decisions.jsonl"
mkdir -p "$(dirname "$log")"
CASAN_SANDBOX_CAPABILITIES="$capabilities" python3 - "$log" "$decision" "$reason" "$backend" "${CASAN_EXECUTION_ID:-sandbox-$$}" <<'PY'
import json, os, sys
path, decision, reason, backend, execution_id = sys.argv[1:]
try:
capabilities = json.loads(os.environ.get("CASAN_SANDBOX_CAPABILITIES", "{}"))
except ValueError:
capabilities = {}
record = {
"schema_version": "1.0.0", "category": "runtime_control",
"policy_id": "casan.sandbox.backend", "decision": decision,
"reason_code": reason, "backend": backend, "execution_id": execution_id,
"capabilities": capabilities,
}
with open(path, "a", encoding="utf-8") as handle:
handle.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
handle.flush()
os.fsync(handle.fileno())
PY
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
@@ -54,12 +85,35 @@ if [[ "$#" -eq 0 ]]; then
exit 64
fi
# C6 production form: CASAN_SANDBOX_MODE=container runs under TRUE kernel
# C6 production form: CASAN_SANDBOX_MODE=container runs under kernel-backed
# isolation (sandbox-container.sh: --network=none --read-only --pids-limit …).
# Default stays the static-policy + ulimit scaffold so existing behaviour is
# unchanged. Falls back to the scaffold if Docker is unavailable.
if [[ "${CASAN_SANDBOX_MODE:-static}" == "container" ]] && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
exec "$SCRIPT_DIR/sandbox-container.sh" --workspace "$WORKSPACE" --timeout "$TIMEOUT" -- "$@"
# A requested/required container backend never silently falls back.
if [[ "$SANDBOX_MODE" == "container" ]]; then
if [[ "${CASAN_SANDBOX_TEST_FORCE_UNAVAILABLE:-0}" != "1" ]] \
&& command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
record_sandbox "allow" "sandbox_container_selected" "docker" '{"network_disabled":true,"read_only_root":true,"workspace_write_restricted":true,"environment_filtered":true,"non_root":true,"resource_limits":true}'
exec "$SCRIPT_DIR/sandbox-container.sh" --workspace "$WORKSPACE" --timeout "$TIMEOUT" -- "$@"
fi
if [[ "$STRICT_SANDBOX" == "1" ]]; then
record_sandbox "deny" "sandbox_isolation_backend_unavailable" "none" '{"timeout_only":false}'
echo "SANDBOX_ISOLATION_REQUIRED backend=container reason=unavailable" >&2
exit 2
fi
if [[ "${CASAN_SANDBOX_ALLOW_STATIC_FALLBACK:-0}" != "1" ]]; then
record_sandbox "deny" "sandbox_fallback_not_approved" "none" '{}'
echo "SANDBOX_FALLBACK_REQUIRES_EXPLICIT_DEVELOPMENT_APPROVAL" >&2
exit 2
fi
record_sandbox "observe_only" "sandbox_static_fallback_development_only" "static_rlimit" '{"network_disabled":false,"read_only_root":false,"workspace_write_restricted":false,"environment_filtered":false,"non_root":false,"resource_limits":true}'
echo "HIGH: container sandbox unavailable; explicit development static fallback is not production isolation" >&2
elif [[ "$SANDBOX_MODE" != "static" ]]; then
record_sandbox "deny" "sandbox_backend_unknown" "$SANDBOX_MODE" '{}'
echo "SANDBOX_BACKEND_UNKNOWN mode=$SANDBOX_MODE" >&2
exit 2
elif [[ "$STRICT_SANDBOX" == "1" ]]; then
record_sandbox "deny" "sandbox_static_forbidden_in_enforce_mode" "static_rlimit" '{"network_disabled":false,"read_only_root":false}'
echo "SANDBOX_STATIC_FORBIDDEN_IN_ENFORCE_MODE" >&2
exit 2
fi
CMD_STR="$*"
@@ -96,6 +150,7 @@ done < <(printf '%s\n' "$CMD_STR" | grep -oE '>>?[[:space:]]*[^[:space:];|&]+' |
# ── 2. Runtime rlimits + wall-clock timeout ─────────────────────────────────
casan_log debug sandbox "SANDBOX_RUN workspace=$WS_ABS file_kb=$MAX_FILE_KB cpu=$CPU_SECONDS procs=$MAX_PROCS timeout=$TIMEOUT"
record_sandbox "allow" "sandbox_static_policy_selected" "static_rlimit" '{"network_disabled":false,"read_only_root":false,"workspace_write_restricted":false,"environment_filtered":false,"non_root":false,"resource_limits":true}'
(
ulimit -f "$((MAX_FILE_KB * 2))" 2>/dev/null || true # ulimit -f is in 512-byte blocks
ulimit -t "$CPU_SECONDS" 2>/dev/null || true
@@ -45,14 +45,24 @@ with open(path, encoding="utf-8") as f:
if not line.strip():
continue
record = json.loads(line)
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("actor",""),
record.get("risk_level",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""),
previous,
])
if int(record.get("schema_version", 1)) >= 2:
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("action_class",""),
record.get("actor",""), record.get("risk_level",""),
json.dumps(record.get("risk_factors", {}), sort_keys=True, separators=(",", ":")),
record.get("evidence_requirement",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""), previous,
])
else:
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("actor",""),
record.get("risk_level",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""), previous,
])
previous = hashlib.sha256(core.encode()).hexdigest()
print(previous)
PY
@@ -68,6 +78,10 @@ printf '%s' "$HEAD_HASH" > "$HEAD_FILE"
# ── Sign the head file ────────────────────────────────────────────────────
VAULT_KMS="$SCRIPT_DIR/vault-kms.sh"
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
[[ "${VAULT_ADDR:-}" == https://* ]] || { echo "SIGN_AUDIT_HEAD_FAIL reason=vault_https_required_in_prod" >&2; exit 1; }
fi
if [[ -n "${VAULT_ADDR:-}" && -n "${VAULT_TOKEN:-}" ]] && \
curl -sf "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; then
# KMS path — sign via Vault Transit, export public key
@@ -103,7 +117,7 @@ PY
fi
else
# Fallback — local key (dev environment without Vault)
if [[ "${CASAN_PROFILE:-}" == "prod" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
echo "SIGN_AUDIT_HEAD_FAIL reason=vault_kms_required_in_prod" >&2
exit 1
fi
@@ -16,6 +16,9 @@ FR_RE = re.compile(r"\|\s*(FR-\d+)\s*\|\s*([^|]+?)\s*\|")
def project_root() -> str:
explicit = os.environ.get("CASAN_APP_ROOT")
if explicit:
return os.path.abspath(explicit)
# Plan-01: harness code lives in packages/casan-harness/; a fixed __file__ parent
# depth lands on the package, not the app. Walk UP for the `.specify` state marker
# so this resolves the app root whether invoked via packages/... or the .specify facade.
@@ -35,21 +35,28 @@ with open(path, encoding="utf-8") as f:
f"AUDIT_CHAIN_BROKEN line={line_no} expected_previous={previous} actual_previous={expected_previous}"
)
core = "|".join(
[
record.get("timestamp", ""),
record.get("trace_id", ""),
record.get("action", ""),
record.get("actor", ""),
record.get("risk_level", ""),
record.get("decision", ""),
record.get("approval_status", ""),
record.get("approver", ""),
record.get("input_hash", ""),
record.get("output_hash", ""),
if int(record.get("schema_version", 1)) >= 2:
core = "|".join([
record.get("timestamp", ""), record.get("trace_id", ""),
record.get("action", ""), record.get("action_class", ""),
record.get("actor", ""), record.get("risk_level", ""),
json.dumps(record.get("risk_factors", {}), sort_keys=True, separators=(",", ":")),
record.get("evidence_requirement", ""), record.get("decision", ""),
record.get("approval_status", ""), record.get("approver", ""),
record.get("input_hash", ""), record.get("output_hash", ""),
expected_previous,
]
)
])
else:
core = "|".join(
[
record.get("timestamp", ""), record.get("trace_id", ""),
record.get("action", ""), record.get("actor", ""),
record.get("risk_level", ""), record.get("decision", ""),
record.get("approval_status", ""), record.get("approver", ""),
record.get("input_hash", ""), record.get("output_hash", ""),
expected_previous,
]
)
expected_hash = hashlib.sha256(core.encode()).hexdigest()
actual_hash = record.get("record_hash", "")
if expected_hash != actual_hash:
@@ -40,8 +40,23 @@ import sys
import time
import uuid
SCHEMA_VERSION = "20.1"
ADAPTER_DEFAULT_VERSION = "20.1.0"
HARNESS_PACKAGE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if HARNESS_PACKAGE_ROOT not in sys.path:
sys.path.insert(0, HARNESS_PACKAGE_ROOT)
from kernel.adapters import AgenticBridgeAdapter
from kernel.contracts import validate_bundle
from kernel.policy import (
H2GateContext,
evaluate_failure_policy,
evaluate_h2_gate,
evaluate_registry_configuration,
evaluate_risk,
evaluate_trust_capabilities,
)
SCHEMA_VERSION = "20.2"
ADAPTER_DEFAULT_VERSION = "20.2.0"
# ── Certification strength ranking (higher == stronger) ──────────────────────
STRENGTH_RANK = {
@@ -77,6 +92,20 @@ TOOL_REGISTRY_ACTION = {
"str_replace_editor": "write_file",
}
REGISTRY_ACTION_BY_CLASS = {
"write": "write_file",
"delete": "delete_file",
"database_mutation": "db_write",
"migration": "migration",
"deployment": "deploy",
"release": "deploy",
"credential_access": "credential_access",
"identity_permission_modification": "identity_change",
"external_network_side_effect": "external_api",
"infrastructure_modification": "deploy",
"unknown": "unknown_tool",
}
# ─────────────────────────────────────────────────────────────────────────────
# Path resolution (mirrors scripts/bash/casan-paths.sh so state lands in the
@@ -145,6 +174,12 @@ def trace_event_dir():
return d
def kernel_trace_dir():
d = os.path.join(state_root(), "logs", "kernel")
os.makedirs(d, exist_ok=True)
return d
def metrics_log():
override = os.environ.get("CASAN_TELEMETRY_METRICS_LOG") or os.environ.get(
"CASAN_METRICS_LOG"
@@ -612,28 +647,52 @@ def h4_scan(text, mode="input"):
pass
def h2_registry_gate(action, idempotency_key):
"""H2 tool-registry gate for a mapped side-effect action."""
script = os.path.join(gates_dir(), "tool-registry-gate.sh")
if not os.path.exists(script) or not bash_available():
return True, "h2_gate_missing"
env = dict(os.environ)
env["CASAN_IDEMPOTENCY_KEY"] = idempotency_key
def h2_registry_gate(action, tool, actor, execution_id, idempotency_key):
"""Return a structured H2 policy decision for every dependency outcome."""
script = os.environ.get("CASAN_H2_GATE_PATH") or os.path.join(
gates_dir(), "tool-registry-gate.sh")
context = H2GateContext(
mode=enforcement_mode(),
actor=actor or "",
action=action,
tool=tool or action,
execution_id=execution_id,
enforcement_path="agentic_bridge.pre_tool.h2_registry",
idempotency_key=idempotency_key,
timeout_seconds=internal_timeout(),
)
return evaluate_h2_gate(
script,
context,
env=os.environ,
bash_path=bash_bin(),
)
def h5_governance_gate(action, content):
"""Invoke the existing H5 gate for action-floor approval decisions."""
script = os.path.join(gates_dir(), "governance-check.sh")
if not os.path.isfile(script) or not bash_available():
return {"approved": False, "reason_code": "h5_gate_unavailable", "detail": "governance gate unavailable"}
tmpin = os.path.join(sessions_dir(), ".h5-in-%s" % uuid.uuid4().hex[:8])
tmpout = os.path.join(sessions_dir(), ".h5-out-%s" % uuid.uuid4().hex[:8])
try:
proc = subprocess.run(
[bash_bin(), script, action],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=internal_timeout(),
env=env,
)
if proc.returncode == 0:
return True, "h2_ok"
return False, "h2_denied"
except subprocess.TimeoutExpired:
return False, "h2_internal_timeout"
except (OSError, ValueError) as exc:
return False, "h2_error:%s" % exc
with open(tmpin, "w", encoding="utf-8") as handle:
handle.write(content or "")
rc, stdout, stderr = _run_gate([bash_bin(), script, tmpin, tmpout, action])
if rc == 0 and "GOVERNANCE_APPROVED" in stdout:
return {"approved": True, "reason_code": "h5_approval_verified", "detail": stdout.strip()}
if rc == 124:
return {"approved": False, "reason_code": "h5_gate_timeout", "detail": "governance gate timed out"}
if rc == 125:
return {"approved": False, "reason_code": "h5_gate_internal_error", "detail": stderr}
return {"approved": False, "reason_code": "h5_approval_required", "detail": stderr or stdout}
finally:
for path in (tmpin, tmpout):
try:
os.unlink(path)
except OSError:
pass
# ─────────────────────────────────────────────────────────────────────────────
@@ -692,6 +751,8 @@ def write_trace_events(rec, evidence):
"kind": evidence.get("kind"),
"decision": evidence.get("decision"),
"detail": evidence.get("detail"),
"category": evidence.get("category"),
"facts": evidence.get("facts", {}),
"certification_strength": rec.get(
"certification_strength"),
},
@@ -700,13 +761,16 @@ def write_trace_events(rec, evidence):
event, ensure_ascii=False, separators=(",", ":")) + "\n")
def add_evidence(rec, h, kind, decision, detail):
def add_evidence(rec, h, kind, decision, detail, facts=None, category="runtime_control"):
evidence = {
"evidence_id": "%s:%s" % (rec.get("trace_id", "trace"), len(rec.get("evidence", [])) + 1),
"h": h,
"category": category,
"kind": kind,
"decision": decision,
"at": now_iso(),
"detail": redact(detail, 160),
"facts": facts if isinstance(facts, dict) else {},
}
rec.setdefault("evidence", []).append(evidence)
write_trace_events(rec, evidence)
@@ -828,6 +892,27 @@ def write_h6_record(rec, status, quality, warnings, missing):
return record
def write_kernel_bundle(rec):
"""Dual-emit the canonical contract without changing legacy trace readers."""
event = dict(rec)
event.update({
"mode": enforcement_mode(),
"execution_status": rec.get("execution_status", "unknown"),
"assurance_status": rec.get("assurance_status", "unknown"),
"environment": os.environ.get("CASAN_PROFILE", "development"),
"completed_at": rec.get("finalized_at") or now_iso(),
"policy_decisions": rec.get("policy_decisions", []),
})
bundle = AgenticBridgeAdapter().map_execution(event)
errors = validate_bundle(bundle)
if errors:
raise ValueError("kernel_contract_invalid:%s" % ",".join(errors))
path = os.path.join(kernel_trace_dir(), "%s.json" % rec.get("trace_id"))
atomic_write_json(path, bundle)
rec["kernel_contract_ref"] = path
return path
def write_trace_file(rec, certified, reasons):
path = os.path.join(trace_dir(), "agentic-%s.json" % rec.get("trace_id"))
payload = {
@@ -851,6 +936,9 @@ def write_trace_file(rec, certified, reasons):
"failures": rec.get("failures", 0),
"certified": certified,
"certification_reasons": reasons,
"execution_outcome": rec.get("execution_status"),
"assurance_outcome": rec.get("assurance_status"),
"kernel_contract_ref": rec.get("kernel_contract_ref"),
"evidence": rec.get("evidence", []),
}
atomic_write_json(path, payload)
@@ -886,6 +974,8 @@ def op_begin(req):
"client": client,
"client_version": req.get("client_version"),
"adapter_version": req.get("adapter_version") or ADAPTER_DEFAULT_VERSION,
"actor": req.get("actor") or os.environ.get("CASAN_ACTOR") or os.environ.get("CASAN_AGENT") or "",
"mode": enforcement_mode(),
"project_root": project,
"project_id": project_id(project),
"session_id_hash": salted_hash(req.get("session")),
@@ -900,8 +990,10 @@ def op_begin(req):
"state": "Admitted",
"tool_calls": 0,
"failures": 0,
"side_effect_failures": 0,
"retries": 0,
"evidence": [],
"policy_decisions": [],
"telemetry": {},
"bypass_signal": False,
"finalized": False,
@@ -1012,30 +1104,109 @@ def op_pre_tool(req):
# record the degradation but do not block the developer.
add_evidence(rec, "H4", "pre-tool", "degraded", "%s:gate_unavailable_no_bash" % tool)
# H2 registry gate for mapped side-effect actions. Opt-in via
# CASAN_AGENTIC_H2_REGISTRY=1: the tool-registry is keyed on NAMED CASAN
# agent identities, which the transparent developer flow does not carry, so
# enabling it unconditionally would deny every write. The always-on H2
# equivalent for this flow is the admission gate above (a side effect
# without a valid admission is denied). Managed deployments that define
# agent identities can turn the registry gate on for defence in depth.
registry_on = os.environ.get("CASAN_AGENTIC_H2_REGISTRY", "0") in ("1", "true", "yes")
if side_effect and registry_on and enforcement_mode() == "enforce":
action = TOOL_REGISTRY_ACTION.get((tool or "").strip().lower())
if action:
key = plain_hash("%s|%s|%s" % (rec["trace_id"], tool, redact(tool_input_text)))[:24]
ok, reason = h2_registry_gate(action, key)
if not ok:
add_evidence(rec, "H2", "pre-tool", "deny", "%s:%s" % (tool, reason))
save_admission(rec)
return _base_response("pre-tool", "deny", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason=reason)
tool_input_string = tool_input_text if isinstance(tool_input_text, str) else json.dumps(
tool_input_text, ensure_ascii=False, sort_keys=True) if tool_input_text is not None else ""
# Native hook payloads commonly wrap a shell command in {"command": ...}.
# Classify the actual command while retaining the complete serialized input
# for scanning, governance evidence and hashing.
risk_command = tool_input_string
if isinstance(tool_input_text, dict) and isinstance(tool_input_text.get("command"), str):
risk_command = tool_input_text["command"]
risk = evaluate_risk(
action=str(req.get("action") or ""),
tool=tool,
resource=str(req.get("resource") or ""),
command=risk_command,
content=tool_input_string,
actor=rec.get("actor", ""),
environment=os.environ.get("CASAN_PROFILE", "development"),
)
# The canonical classifier can prove a shell command read-only; unknown
# shell commands remain side-effecting/high-risk by construction.
side_effect = bool(risk["side_effecting"])
action = REGISTRY_ACTION_BY_CLASS.get(risk["action_class"], TOOL_REGISTRY_ACTION.get((tool or "").strip().lower(), "unknown_tool"))
rec["last_tool"] = tool
rec["last_action"] = action
rec["last_risk"] = risk
rec.setdefault("policy_decisions", []).append(risk)
add_evidence(
rec, "H5", "action-risk", risk["decision"],
"class=%s effective=%s" % (risk["action_class"], risk["effective_risk"]),
facts=risk,
)
if side_effect and risk["decision"] == "deny":
reason = risk["reason_codes"][-1]
add_evidence(rec, "H5", "pre-tool", "deny", reason, facts=risk)
save_admission(rec)
return _base_response("pre-tool", "deny", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason=reason,
policy_decision=risk)
if side_effect and risk["decision"] == "require_approval":
approval = h5_governance_gate(action, tool_input_string)
add_evidence(
rec, "H5", "approval", "allow" if approval["approved"] else "require_approval",
approval["reason_code"], facts={"risk": risk, "approval": approval},
)
if not approval["approved"]:
save_admission(rec)
return _base_response("pre-tool", "require_approval", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason=approval["reason_code"],
policy_decision=risk)
registry = evaluate_registry_configuration(
enforcement_mode(),
os.environ.get("CASAN_PROFILE", "development"),
os.environ.get("CASAN_AGENTIC_H2_REGISTRY") if "CASAN_AGENTIC_H2_REGISTRY" in os.environ else None,
)
if side_effect and not registry["valid"]:
add_evidence(rec, "H2", "registry-configuration", "deny", registry["reason_code"], facts=registry)
save_admission(rec)
return _base_response("pre-tool", "deny", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason=registry["reason_code"])
if side_effect and registry["unsafe_bypass"]:
rec["integration_mode"] = "observed_only"
rec["certification_strength"] = "observed_only"
add_evidence(rec, "H2", "registry-configuration", "degraded", registry["reason_code"], facts=registry)
if side_effect and registry["enabled"]:
key = plain_hash("%s|%s|%s" % (rec["trace_id"], tool, redact(tool_input_text)))[:24]
h2_decision = h2_registry_gate(action, tool, rec.get("actor", ""), rec["trace_id"], key)
rec.setdefault("policy_decisions", []).append(h2_decision)
add_evidence(
rec, "H2", "tool-registry", h2_decision["decision"],
h2_decision["reason_code"], facts=h2_decision,
)
if not h2_decision["execution_allowed"]:
save_admission(rec)
return _base_response("pre-tool", "deny", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason=h2_decision["reason_code"],
policy_decision=h2_decision)
if not h2_decision["certifiable"]:
rec["integration_mode"] = "observed_only"
rec["certification_strength"] = "observed_only"
add_evidence(rec, "H2/H4", "pre-tool", "allow",
"tool=%s side_effect=%s" % (tool, side_effect))
"tool=%s side_effect=%s action=%s" % (tool, side_effect, action),
facts={"action_risk": risk, "registry": registry})
save_admission(rec)
return _base_response("pre-tool", "allow", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason="allowed")
response = _base_response("pre-tool", "allow", admission_id=admission_id,
trace_id=rec.get("trace_id"), reason="allowed",
policy_decision=risk)
if registry["unsafe_bypass"]:
response["warnings"].append("HIGH: H2 registry bypass active in development/test; execution is not certifiable")
return response
def single_step_failure_policy(side_effect):
decision = evaluate_failure_policy(
side_effecting=side_effect,
mode=enforcement_mode(),
profile=os.environ.get("CASAN_PROFILE", "development"),
configured=os.environ.get("CASAN_SINGLE_STEP_FAILURE_POLICY"),
)
return decision["failure_policy"], decision["source"]
def op_post_tool(req):
@@ -1045,11 +1216,52 @@ def op_post_tool(req):
reason="no_admission")
rec["tool_calls"] = rec.get("tool_calls", 0) + 1
status = req.get("status") or "success"
if status in ("error", "timeout"):
risk = evaluate_risk(
action=str(rec.get("last_action") or ""),
tool=str(req.get("tool") or rec.get("last_tool") or ""),
command=str(req.get("command") or ""),
actor=rec.get("actor", ""),
environment=os.environ.get("CASAN_PROFILE", "development"),
)
side_effect = bool(risk["side_effecting"])
if status in ("error", "timeout", "denied", "failed"):
rec["failures"] = rec.get("failures", 0) + 1
if side_effect:
rec["side_effect_failures"] = rec.get("side_effect_failures", 0) + 1
policy, source = single_step_failure_policy(side_effect)
rec["failure_policy"] = policy
rec["execution_status"] = "failed"
rec["assurance_status"] = "failed"
add_evidence(
rec, "H6", "failure-policy", policy,
"tool=%s status=%s policy=%s" % (req.get("tool"), status, policy),
facts={
"command_executed": status not in ("denied",),
"command_outcome": status,
"telemetry_recorded": True,
"assurance_may_continue": policy == "record_only",
"failure_policy": policy,
"policy_source": source,
"side_effecting": side_effect,
},
)
add_evidence(rec, "H5", "post-tool", status,
"tool=%s dur_ms=%s result=%s" % (req.get("tool"), req.get("duration_ms"),
redact(req.get("result"), 80)),
facts={"risk": risk, "failure_policy": policy})
save_admission(rec)
decision = "recorded" if policy == "record_only" else policy
return _base_response(
"post-tool", decision, admission_id=rec["admission_id"],
trace_id=rec.get("trace_id"), reason="tool_failed:%s" % policy,
execution_outcome="failed", assurance_outcome="failed",
assurance_may_continue=policy == "record_only",
)
rec["execution_status"] = rec.get("execution_status") or "success"
add_evidence(rec, "H5", "post-tool", status,
"tool=%s dur_ms=%s result=%s" % (req.get("tool"), req.get("duration_ms"),
redact(req.get("result"), 80)))
redact(req.get("result"), 80)),
facts={"risk": risk, "command_outcome": status, "telemetry_recorded": True})
save_admission(rec)
return _base_response("post-tool", "recorded", admission_id=rec["admission_id"],
trace_id=rec.get("trace_id"), reason="evidence_appended")
@@ -1092,6 +1304,10 @@ def op_finalize(req):
rec["summary_hash"] = salted_hash(req.get("assistant_summary"))
stop_reason = req.get("stop_reason") or "completed"
status = "success" if stop_reason in ("completed", "max_turns") else "failed"
if rec.get("failures", 0) > 0:
# Telemetry success is not execution success. A failed tool remains a
# failed execution even if the client later emits Stop(completed).
status = "failed"
observed_harnesses = {
harness
@@ -1110,6 +1326,8 @@ def op_finalize(req):
# H3/H5/H7 finalize controls: run the H4 output filter over the assistant
# summary as the closing verification control.
reasons = []
if rec.get("failures", 0) > 0:
reasons.append("tool_execution_failed")
if req.get("assistant_summary"):
h4 = h4_scan(req.get("assistant_summary"), "output")
if h4 == "ok":
@@ -1158,7 +1376,17 @@ def op_finalize(req):
reasons.append("coverage_bypass")
if status != "success":
certified = False
reasons.append("stop_%s" % stop_reason)
failure_reason = "failed_tool_outcome" if rec.get("failures", 0) > 0 else "stop_%s" % stop_reason
if failure_reason not in reasons:
reasons.append(failure_reason)
trust = evaluate_trust_capabilities()
add_evidence(
rec, "H7", "trust-capability", "pass" if trust["certifiable"] else "degraded",
",".join(trust["reason_codes"]), facts=trust, category="readiness_check",
)
if trust["production"] and not trust["certifiable"]:
certified = False
reasons.extend(reason for reason in trust["reason_codes"] if reason not in reasons)
if not certified and not reasons:
reasons.append("unknown")
if certified:
@@ -1168,12 +1396,15 @@ def op_finalize(req):
rec["certified"] = certified
rec["finalized"] = True
rec["finalized_at"] = now_iso()
rec["execution_status"] = status
rec["assurance_status"] = "passed" if certified else "failed" if rec.get("failures", 0) else "degraded"
add_evidence(
rec,
"H7",
"certification",
"certified" if certified else "non_certified",
",".join(reasons),
category="certification_claim",
)
quality, warnings, missing = classify_telemetry(rec)
@@ -1185,13 +1416,26 @@ def op_finalize(req):
",".join(warnings) if warnings else "provider_usage_complete",
)
h6_record = write_h6_record(rec, status, quality, warnings, missing)
try:
write_kernel_bundle(rec)
except (OSError, ValueError, TypeError) as exc:
certified = False
rec["certified"] = False
rec["state"] = "NonCertified"
rec["assurance_status"] = "failed"
if "kernel_contract_emission_failed" not in reasons:
reasons.append("kernel_contract_emission_failed")
add_evidence(rec, "H7", "kernel-contract", "failed",
"kernel_contract_emission_failed:%s" % type(exc).__name__)
trace_path = write_trace_file(rec, certified, reasons)
save_admission(rec)
resp = _base_response("finalize", "certified" if certified else "non_certified",
admission_id=rec["admission_id"], trace_id=rec.get("trace_id"),
certification_strength=strength, telemetry_quality=quality,
reason=",".join(reasons))
reason=",".join(reasons), execution_outcome=status,
assurance_outcome=rec.get("assurance_status"),
certification_outcome="certified" if certified else "non_certified")
resp["warnings"].extend(warnings)
resp["context"] = "trace=%s certified=%s" % (os.path.basename(trace_path), certified)
report_url = dashboard_url(rec.get("trace_id"))
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""CLI boundary for shell/native runtimes to consume Assurance Kernel policy."""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
HARNESS_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(HARNESS_ROOT))
from kernel.adapters import NativeHarnessAdapter # noqa: E402
from kernel.contracts import validate_bundle # noqa: E402
from kernel.policy import ( # noqa: E402
H2GateContext,
evaluate_h2_gate,
evaluate_registry_configuration,
evaluate_risk,
evaluate_trust_capabilities,
)
def _print(payload: object) -> None:
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
def _read_content(path: str | None) -> str:
if not path:
return ""
with open(path, encoding="utf-8") as handle:
return handle.read()
def _append_jsonl(path: str, payload: object) -> None:
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
data = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
fd = os.open(str(target), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
try:
try:
import fcntl
fcntl.flock(fd, fcntl.LOCK_EX)
except (ImportError, OSError):
pass
os.write(fd, data)
os.fsync(fd)
finally:
os.close(fd)
def cmd_risk(args: argparse.Namespace) -> int:
payload = evaluate_risk(
action=args.action,
tool=args.tool,
resource=args.resource,
command=args.command,
content=_read_content(args.content_file),
actor=args.actor,
environment=args.environment,
)
_print(payload)
return 0
def cmd_registry(args: argparse.Namespace) -> int:
payload = evaluate_registry_configuration(args.mode, args.profile, args.explicit)
if args.evidence_log:
_append_jsonl(args.evidence_log, payload)
_print(payload)
return 0 if payload["valid"] else 2
def cmd_h2(args: argparse.Namespace) -> int:
context = H2GateContext(
mode=args.mode,
actor=args.actor,
action=args.action,
tool=args.tool,
execution_id=args.execution_id,
enforcement_path=args.enforcement_path,
idempotency_key=args.idempotency_key,
timeout_seconds=args.timeout,
)
payload = evaluate_h2_gate(args.gate, context, bash_path=args.bash)
if args.evidence_log:
_append_jsonl(args.evidence_log, payload)
_print(payload)
return 0 if payload["execution_allowed"] else 2
def cmd_trust(_args: argparse.Namespace) -> int:
payload = evaluate_trust_capabilities()
_print(payload)
return 0 if payload["ready"] else 2
def cmd_native(args: argparse.Namespace) -> int:
event = json.load(sys.stdin) if args.event_file == "-" else json.load(open(args.event_file, encoding="utf-8"))
payload = NativeHarnessAdapter().map_execution(event)
errors = validate_bundle(payload)
if errors:
_print({"valid": False, "errors": errors, "bundle": payload})
return 2
_print(payload)
return 0
def parser() -> argparse.ArgumentParser:
root = argparse.ArgumentParser()
sub = root.add_subparsers(dest="command", required=True)
risk = sub.add_parser("risk")
for name in ("action", "tool", "resource", "command", "actor", "environment"):
risk.add_argument(f"--{name}", default="")
risk.add_argument("--content-file")
risk.set_defaults(func=cmd_risk)
registry = sub.add_parser("registry-config")
registry.add_argument("--mode", default="observe")
registry.add_argument("--profile", default="development")
registry.add_argument("--explicit")
registry.add_argument("--evidence-log")
registry.set_defaults(func=cmd_registry)
h2 = sub.add_parser("h2-gate")
h2.add_argument("--gate", required=True)
h2.add_argument("--mode", choices=("observe", "enforce"), required=True)
h2.add_argument("--actor", default="")
h2.add_argument("--action", required=True)
h2.add_argument("--tool", required=True)
h2.add_argument("--execution-id", required=True)
h2.add_argument("--enforcement-path", required=True)
h2.add_argument("--idempotency-key", required=True)
h2.add_argument("--timeout", type=float, default=8.0)
h2.add_argument("--bash", default="bash")
h2.add_argument("--evidence-log")
h2.set_defaults(func=cmd_h2)
trust = sub.add_parser("trust-capabilities")
trust.set_defaults(func=cmd_trust)
native = sub.add_parser("adapt-native")
native.add_argument("event_file")
native.set_defaults(func=cmd_native)
return root
def main() -> int:
args = parser().parse_args()
return int(args.func(args))
if __name__ == "__main__":
raise SystemExit(main())
+750
View File
@@ -0,0 +1,750 @@
#!/usr/bin/env python3
"""Zero-dependency, read-only reporting engine for CASAN Core.
The evidence files under .specify remain the source of truth. This module only
builds bounded, sanitized projections for the Local Assurance Viewer and
on-demand JSON/HTML exports.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from datetime import datetime, timezone
from html import escape
import json
import math
import os
from pathlib import Path
import re
from typing import Any
SAFE_ID = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$")
STALE_AFTER_S = 900
MAX_SOURCE_BYTES = 20 * 1024 * 1024
MAX_RECORDS = 2000
GATES = (
("H1-context", "H1 · Context", "Prompt contract, mode and risk classification"),
("H2-tool", "H2 · Tool", "Allowlisted source and tool preparation"),
("H3-eval", "H3 · Eval", "Grounded synthesis and quality evaluation"),
("H4-security", "H4 · Security", "Input and output security boundary"),
("H5-governance", "H5 · Governance", "Decision policy and append-only audit"),
("H6-agentops", "H6 · AgentOps", "Runtime, token, cost and failure telemetry"),
("H7-orchestration", "H7 · Orchestration", "Final governed outcome and certification"),
)
GATE_MAP = {f"H{index}": gate[0] for index, gate in enumerate(GATES, 1)}
SENSITIVE_KEYS = {
"authorization", "api_key", "apikey", "access_token", "refresh_token",
"password", "secret", "prompt", "raw_prompt", "tool_input", "tool_output",
"request_body", "response_body",
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def state_root(root: str | Path) -> Path:
override = os.environ.get("CASAN_STATE_ROOT")
return Path(override).expanduser().resolve() if override else Path(root).resolve() / ".specify"
def _read_text(path: Path, maximum: int = MAX_SOURCE_BYTES) -> str:
try:
if not path.is_file() or path.stat().st_size > maximum:
return ""
return path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
def _read_tail_text(path: Path, maximum: int = MAX_SOURCE_BYTES) -> str:
try:
if not path.is_file():
return ""
size = path.stat().st_size
with path.open("rb") as handle:
if size > maximum:
handle.seek(-maximum, os.SEEK_END)
handle.readline() # discard a possibly partial first record
return handle.read(maximum).decode("utf-8", errors="replace")
except OSError:
return ""
def read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(_read_text(path))
return value if isinstance(value, dict) else {}
except (TypeError, ValueError):
return {}
def read_jsonl(path: Path, maximum_records: int = MAX_RECORDS) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for line in _read_tail_text(path).splitlines()[-maximum_records:]:
try:
value = json.loads(line)
except ValueError:
continue
if isinstance(value, dict):
rows.append(value)
return rows
def _safe_value(value: Any, depth: int = 0) -> Any:
if depth > 5:
return "[depth limited]"
if isinstance(value, dict):
result: dict[str, Any] = {}
for key, item in list(value.items())[:80]:
normalized = str(key).lower().replace("-", "_")
if normalized in SENSITIVE_KEYS:
result[str(key)] = "[redacted]"
else:
result[str(key)] = _safe_value(item, depth + 1)
return result
if isinstance(value, list):
return [_safe_value(item, depth + 1) for item in value[:80]]
if isinstance(value, str):
return value if len(value) <= 4096 else value[:4096] + "…"
if value is None or isinstance(value, (bool, int, float)):
return value
return str(value)[:4096]
def sanitize(row: dict[str, Any]) -> dict[str, Any]:
value = _safe_value(row)
return value if isinstance(value, dict) else {}
def _number(value: Any) -> float | None:
if isinstance(value, bool):
return None
if isinstance(value, (int, float)) and math.isfinite(float(value)):
return float(value)
return None
def _threshold(name: str, fallback: float) -> float:
try:
value = float(os.environ.get(name, str(fallback)))
return value if math.isfinite(value) and value >= 0 else fallback
except ValueError:
return fallback
def _text(value: Any, fallback: str = "") -> str:
return value.strip() if isinstance(value, str) and value.strip() else fallback
def _row_run(row: dict[str, Any]) -> str:
return _text(row.get("run_id") or row.get("trace_id"), "unattributed")
def _row_project(row: dict[str, Any]) -> str:
return _text(row.get("project") or row.get("project_id"), "default")
def _parse_time(value: Any) -> float | None:
if not isinstance(value, str) or not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
except ValueError:
return None
def _status(value: Any) -> str:
status = _text(value).lower()
if status in {"success", "pass", "passed", "allow", "allowed", "answered", "opened", "certified"}:
return "pass"
if status in {"warn", "warning", "degraded", "partial", "insufficient"}:
return "warning"
if status in {"block", "blocked", "deny", "denied", "flag", "non_certified"}:
return "blocked"
if status in {"fail", "failed", "error"}:
return "error"
if status in {"running", "skipped"}:
return status
return "queued"
def paths(root: str | Path) -> dict[str, Path]:
state = state_root(root)
return {
"config": Path(root).resolve() / ".casan" / "config.json",
"readiness": Path(root).resolve() / ".casan" / "readiness.json",
"receipt": state / "state" / "latest-run.json",
"metrics": state / "logs" / "cost" / "metrics.jsonl",
"provider": state / "logs" / "level5" / "provider-usage.jsonl",
"alerts": state / "agentops" / "alerts.log",
"trace_dir": state / "logs" / "trace",
"event_dir": state / "logs" / "trace-events",
}
def project_profile(root: str | Path) -> dict[str, Any]:
config = read_json(paths(root)["config"])
readiness = read_json(paths(root)["readiness"])
maturity = config.get("maturity") if isinstance(config.get("maturity"), dict) else {}
return {
"project_id": _text(config.get("project_id"), Path(root).resolve().name),
"project_name": _text(config.get("project_name"), Path(root).resolve().name),
"edition": _text(config.get("edition") or config.get("target_level_name"), "core"),
"edition_status": _text(config.get("edition_status"), "configured"),
"enforcement_mode": _text(config.get("enforcement_mode"), "unknown"),
"integration_mode": _text(config.get("integration_mode"), "unknown"),
"clients": [str(value) for value in config.get("clients", []) if isinstance(value, str)],
"maturity": {
"level": maturity.get("level") if _number(maturity.get("level")) is not None else None,
"status": _text(maturity.get("status"), "not_assessed"),
"evidence": _text(maturity.get("evidence")) or None,
},
"readiness": readiness if readiness else {
"schema_version": 1,
"overall_status": "not_assessed",
"dimensions": {
"core": {
"status": "not_assessed",
"summary": "Run `casan readiness --refresh` to assess this installation.",
"acceptance": [],
},
"domain_pipeline": {
"status": "not_assessed",
"summary": "Domain Pipeline readiness has not been assessed.",
},
"provider_telemetry": {
"status": "not_assessed",
"summary": "Provider Telemetry readiness has not been assessed.",
},
},
"next_actions": [],
},
"viewer": {
"mode": "local_core",
"scope": "single_project",
"read_only": True,
"network": "loopback_only",
},
}
def _trace(root: str | Path, trace_id: str) -> dict[str, Any]:
if not SAFE_ID.fullmatch(trace_id):
return {}
return read_json(paths(root)["trace_dir"] / f"agentic-{trace_id}.json")
def _metric_rows(root: str | Path) -> list[dict[str, Any]]:
return read_jsonl(paths(root)["metrics"])
def trace_graph(root: str | Path, trace_id: str) -> dict[str, Any]:
if not SAFE_ID.fullmatch(trace_id):
return _empty_graph(trace_id)
source = paths(root)
events: list[dict[str, Any]] = []
for row in read_jsonl(source["event_dir"] / f"{trace_id}.jsonl", 500):
if row.get("trace_id") != trace_id or row.get("gate_id") not in {gate[0] for gate in GATES}:
continue
events.append({
"timestamp": _text(row.get("timestamp")),
"trace_id": trace_id,
"gate_id": _text(row.get("gate_id")),
"status": _status(row.get("status")),
"reason": _text(row.get("reason"), "Evidence observed"),
"evidence": sanitize(row.get("evidence") if isinstance(row.get("evidence"), dict) else {}),
})
if not events:
trace = _trace(root, trace_id)
harness = _text(trace.get("harness"))
if harness in {gate[0] for gate in GATES}:
events.append({
"timestamp": _text(trace.get("timestamp")),
"trace_id": trace_id,
"gate_id": harness,
"status": _status(trace.get("status") or trace.get("action")),
"reason": f"Legacy {harness} trace",
"evidence": sanitize({
"mode": trace.get("mode"),
"action": trace.get("action"),
"risk_level": trace.get("risk_level"),
}),
})
evidence_rows = trace.get("evidence") if isinstance(trace.get("evidence"), list) else []
for evidence in evidence_rows[:200]:
if not isinstance(evidence, dict):
continue
for marker in _text(evidence.get("h")).split("/"):
gate_id = GATE_MAP.get(marker)
if gate_id:
events.append({
"timestamp": _text(evidence.get("at") or trace.get("finalized_at")),
"trace_id": trace_id,
"gate_id": gate_id,
"status": _status(evidence.get("decision")),
"reason": f"{_text(evidence.get('kind'), 'legacy-evidence')}: {_text(evidence.get('detail'))}",
"evidence": sanitize({
"kind": evidence.get("kind"),
"decision": evidence.get("decision"),
"detail": evidence.get("detail"),
"certification_strength": trace.get("certification_strength"),
}),
})
for metric in _metric_rows(root):
if metric.get("trace_id") != trace_id:
continue
quality = _text(metric.get("telemetry_quality"))
events.append({
"timestamp": _text(metric.get("timestamp")),
"trace_id": trace_id,
"gate_id": "H6-agentops",
"status": "warning" if quality in {"partial", "insufficient"} else _status(metric.get("status")),
"reason": _text(metric.get("step"), "Runtime metric"),
"evidence": sanitize({
"latency_ms": metric.get("latency_ms"),
"total_tokens": metric.get("total_tokens"),
"cost_estimate": metric.get("cost_estimate"),
"telemetry_quality": metric.get("telemetry_quality"),
}),
})
events.sort(key=lambda row: _text(row.get("timestamp")))
nodes = []
for gate_id, title, description in GATES:
gate_events = [row for row in events if row["gate_id"] == gate_id]
latest = gate_events[-1] if gate_events else {}
nodes.append({
"id": gate_id,
"title": title,
"description": description,
"status": latest.get("status", "queued"),
"reason": latest.get("reason", "Waiting for evidence"),
"updated_at": latest.get("timestamp") or None,
"evidence": latest.get("evidence", {}),
"events": gate_events,
})
outcome = nodes[-1]
return {
"found": bool(events),
"trace_id": trace_id,
"updated_at": events[-1]["timestamp"] if events else None,
"terminal": outcome["status"] in {"pass", "blocked", "error"},
"progress": sum(1 for node in nodes if node["status"] != "queued"),
"nodes": nodes,
"events": events,
}
def _empty_graph(trace_id: str) -> dict[str, Any]:
nodes = [{
"id": gate_id, "title": title, "description": description,
"status": "queued", "reason": "Waiting for evidence",
"updated_at": None, "evidence": {}, "events": [],
} for gate_id, title, description in GATES]
return {
"found": False, "trace_id": trace_id, "updated_at": None,
"terminal": False, "progress": 0, "nodes": nodes, "events": [],
}
def run_report(root: str | Path, trace_id: str) -> dict[str, Any]:
graph = trace_graph(root, trace_id)
trace = _trace(root, trace_id)
metric = next(
(row for row in reversed(_metric_rows(root)) if row.get("trace_id") == trace_id),
None,
)
profile = project_profile(root)
metric_row = metric or {}
duration = _number(metric_row.get("duration_ms"))
if duration is None:
duration = _number(metric_row.get("latency_ms"))
found = bool(trace or graph["found"] or metric)
finalized = _text(trace.get("finalized_at") or metric_row.get("finished_at")) or None
if not found:
verdict = "not_found"
elif not graph["terminal"] and not finalized:
verdict = "in_progress"
elif trace.get("certified") is True:
verdict = "certified"
else:
verdict = "non_certified"
cost = metric_row.get("cost") if isinstance(metric_row.get("cost"), dict) else {}
reasons = trace.get("certification_reasons") if isinstance(trace.get("certification_reasons"), list) else []
return {
"schema_version": 1,
"report_id": f"RUN-{trace_id}",
"generated_at": utc_now(),
"trace_id": trace_id,
"project": {
"id": profile["project_id"],
"name": profile["project_name"],
"edition": profile["edition"],
"maturity": profile["maturity"],
},
"verdict": verdict,
"certification": {
"strength": _text(trace.get("certification_strength") or metric_row.get("certification_strength")) or None,
"reasons": [str(value) for value in reasons if isinstance(value, str)],
"finalized_at": finalized,
},
"summary": {
"gates_observed": graph["progress"],
"gates_total": 7,
"tool_calls": int(_number(trace.get("tool_calls") or metric_row.get("tool_calls")) or 0),
"failures": int(_number(trace.get("failures") or metric_row.get("failures")) or 0),
"duration_ms": duration,
"telemetry_quality": _text(metric_row.get("telemetry_quality"), "unknown"),
"token_usage_available": any(_number(metric_row.get(key)) is not None for key in ("total_tokens", "input_tokens", "output_tokens")),
"cost_available": _number(metric_row.get("cost_estimate")) is not None or _number(cost.get("amount")) is not None,
},
"gates": graph["nodes"],
"h6": sanitize(metric_row) if metric else None,
"source": {
"trace_found": bool(trace),
"graph_found": graph["found"],
"metric_found": metric is not None,
},
}
def runs(root: str | Path, limit: int = 100) -> dict[str, Any]:
limit = max(1, min(int(limit), 200))
rows = _metric_rows(root)
receipts = read_json(paths(root)["receipt"])
safe_rows = [sanitize(row) for row in reversed(rows[-limit:])]
if not safe_rows and receipts.get("trace_id"):
safe_rows = [sanitize({
"trace_id": receipts.get("trace_id"),
"project_id": receipts.get("project_id"),
"status": "success" if receipts.get("certified") else "failed",
"timestamp": receipts.get("finalized_at"),
"latency_ms": receipts.get("duration_ms"),
"tool_calls": receipts.get("tool_calls"),
"failures": receipts.get("failures"),
"telemetry_quality": receipts.get("telemetry_quality"),
})]
return {"count": len(rows) if rows else len(safe_rows), "runs": safe_rows, "latest": sanitize(receipts)}
def _freshness(path: Path, source: str, required: bool) -> dict[str, Any]:
try:
stat = path.stat()
age = max(0, int(datetime.now(timezone.utc).timestamp() - stat.st_mtime))
present = path.is_file()
except OSError:
age, present = None, False
return {
"source": source,
"path": str(path),
"present": present,
"updated_at": datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat().replace("+00:00", "Z") if present else None,
"age_s": age,
"stale": bool(present and age is not None and age > STALE_AFTER_S),
"required": required,
}
def _display_path(root: str | Path, path: Path) -> str:
try:
return str(path.resolve().relative_to(Path(root).resolve()))
except (OSError, ValueError):
return f"[external]/{path.name}"
def _percentile(values: list[float], pct: int) -> float:
if not values:
return 0
ordered = sorted(values)
return ordered[max(0, math.ceil((pct / 100) * len(ordered)) - 1)]
def _grouped(rows: list[dict[str, Any]], key) -> list[dict[str, Any]]:
groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
groups[key(row)].append(row)
result = []
for name, records in groups.items():
latencies = [_number(row.get("latency_ms")) for row in records]
latencies = [value for value in latencies if value is not None and value > 0]
tokens = [_number(row.get("total_tokens")) for row in records]
cost_values = [
_number(row.get("cost_usd")) if _number(row.get("cost_usd")) is not None
else _number(row.get("cost_estimate"))
for row in records
]
result.append({
"key": name,
"runs": len(records),
"failures": sum(1 for row in records if _text(row.get("status"), "unknown") == "failed"),
"latency_avg_ms": round(sum(latencies) / len(latencies)) if latencies else 0,
"tokens": round(sum(value for value in tokens if value is not None)) if any(value is not None for value in tokens) else None,
"cost_usd": round(sum(value for value in cost_values if value is not None), 6) if any(value is not None for value in cost_values) else None,
})
return sorted(result, key=lambda row: (-row["runs"], row["key"]))
def _matches(row: dict[str, Any], project: str | None, run: str | None,
start: float | None, end: float | None) -> bool:
if project and _row_project(row) != project:
return False
if run and _row_run(row) != run:
return False
timestamp = _parse_time(row.get("timestamp"))
if start is not None and (timestamp is None or timestamp < start):
return False
if end is not None and (timestamp is None or timestamp > end):
return False
return True
def _date_boundary(value: str | None, end: bool = False) -> tuple[str | None, float | None]:
if not value:
return None, None
normalized = value
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
normalized += "T23:59:59.999Z" if end else "T00:00:00.000Z"
parsed = _parse_time(normalized)
if parsed is None:
raise ValueError("invalid report time boundary")
return datetime.fromtimestamp(parsed, timezone.utc).isoformat().replace("+00:00", "Z"), parsed
def h6_report(root: str | Path, project: str | None = None, run: str | None = None,
from_date: str | None = None, to_date: str | None = None,
limit: int = 50) -> dict[str, Any]:
for value in (project, run):
if value and not SAFE_ID.fullmatch(value):
raise ValueError("invalid project or run filter")
normalized_from, start = _date_boundary(from_date)
normalized_to, end = _date_boundary(to_date, True)
if start is not None and end is not None and start > end:
raise ValueError("invalid report time range")
limit = max(1, min(int(limit), 200))
source = paths(root)
all_metrics = _metric_rows(root)
all_provider = read_jsonl(source["provider"])
all_alerts = read_jsonl(source["alerts"])
metrics = [row for row in all_metrics if _matches(row, project, run, start, end)]
provider = [row for row in all_provider if _matches(row, project, run, start, end)]
alerts = [row for row in all_alerts if _matches(row, project, run, start, end)]
latencies = [value for value in (_number(row.get("latency_ms")) for row in metrics) if value is not None and value > 0]
failed = sum(1 for row in metrics if _text(row.get("status"), "unknown") == "failed")
degraded = sum(1 for row in metrics if _text(row.get("status"), "unknown") == "degraded")
success = sum(1 for row in metrics if _text(row.get("status"), "unknown") in {"success", "pass", "passed"})
provider_token_runs = {_row_run(row) for row in provider if _number(row.get("total_tokens")) is not None}
provider_cost_runs = {_row_run(row) for row in provider if _number(row.get("cost_usd")) is not None}
token_records = sum(1 for row in metrics if any(_number(row.get(key)) is not None for key in ("total_tokens", "input_tokens", "output_tokens")) or _row_run(row) in provider_token_runs)
cost_records = sum(1 for row in metrics if _number(row.get("cost_estimate")) is not None or _row_run(row) in provider_cost_runs)
token_pct = round((token_records / len(metrics)) * 100, 1) if metrics else 0
cost_pct = round((cost_records / len(metrics)) * 100, 1) if metrics else 0
quality = Counter(_text(row.get("telemetry_quality"), "unknown") for row in metrics)
evidence = [
{**_freshness(source["metrics"], "metrics", True), "records": len(all_metrics)},
{**_freshness(source["provider"], "provider", False), "records": len(all_provider)},
{**_freshness(source["alerts"], "alerts", False), "records": len(all_alerts)},
]
for item in evidence:
item["path"] = _display_path(root, Path(item["path"]))
primary = evidence[0]
failure_rate = round((failed / len(metrics)) * 100, 1) if metrics else 0
p95 = _percentile(latencies, 95)
failure_threshold = _threshold("CASAN_H6_FAILURE_RATE_THRESHOLD_PCT", 5)
p95_threshold = _threshold("CASAN_H6_P95_LATENCY_THRESHOLD_MS", 5000)
findings: list[dict[str, Any]] = []
warnings: list[str] = []
if not primary["present"]:
findings.append({"severity": "critical", "code": "METRICS_MISSING", "message": "The required runtime metrics source is missing."})
elif primary["stale"]:
findings.append({"severity": "warning", "code": "METRICS_STALE", "message": "The primary runtime metrics source is stale.", "metric": "age_s", "value": primary["age_s"], "threshold": STALE_AFTER_S})
if failure_rate > failure_threshold:
findings.append({"severity": "critical", "code": "FAILURE_RATE_BREACH", "message": "Failure rate exceeds the configured H6 threshold.", "metric": "failure_rate_pct", "value": failure_rate, "threshold": failure_threshold})
elif failed:
findings.append({"severity": "warning", "code": "FAILURES_PRESENT", "message": f"{failed} failed run(s) are present in the selected scope.", "metric": "failed", "value": failed})
if p95 > p95_threshold:
findings.append({"severity": "warning", "code": "P95_LATENCY_BREACH", "message": "P95 latency exceeds the configured H6 threshold.", "metric": "p95_latency_ms", "value": p95, "threshold": p95_threshold})
alert_counter: Counter[str] = Counter()
fingerprints: set[tuple[str, str]] = set()
for row in metrics + alerts:
alert_types: list[str] = []
if isinstance(row.get("alerts"), list):
alert_types.extend(str(value) for value in row["alerts"] if isinstance(value, str))
body = row.get("body") if isinstance(row.get("body"), dict) else {}
if isinstance(body.get("alert.type"), str):
alert_types.append(body["alert.type"])
for alert in alert_types:
row_run = _row_run(row)
fingerprint = (
_text(row.get("timestamp"), "unknown-time")
if row_run == "unattributed" else row_run,
alert,
)
if fingerprint not in fingerprints:
alert_counter[alert] += 1
fingerprints.add(fingerprint)
alert_count = sum(alert_counter.values())
if alert_count:
findings.append({"severity": "warning", "code": "ALERTS_PRESENT", "message": f"{alert_count} alert signal(s) require review.", "metric": "alerts", "value": alert_count})
if degraded:
findings.append({"severity": "warning", "code": "DEGRADED_RUNS_PRESENT", "message": f"{degraded} degraded run(s) are present in the selected scope.", "metric": "degraded", "value": degraded})
if metrics and (token_pct < 100 or cost_pct < 100):
findings.append({"severity": "warning", "code": "TELEMETRY_COVERAGE_GAP", "message": "Some runs do not have reliable provider token or cost attribution.", "metric": "token_cost_coverage_pct", "value": f"{token_pct}/{cost_pct}", "threshold": "100/100"})
if not provider:
warnings.append("No provider usage records matched the selected scope; token and actual-cost breakdown may be incomplete.")
if metrics and token_pct < 100:
warnings.append(f"{token_pct}% of runtime records have reliable token attribution; unavailable values remain null, never zero.")
if metrics and cost_pct < 100:
warnings.append(f"{cost_pct}% of runtime records have reliable cost attribution; unavailable values remain null, never zero.")
for item in evidence[1:]:
if not item["present"]:
warnings.append(f"Optional {item['source']} telemetry source is missing; its breakdown is unavailable.")
elif item["stale"]:
warnings.append(f"Optional {item['source']} telemetry source is stale; its breakdown may not reflect recent activity.")
critical = any(item["severity"] == "critical" for item in findings)
warning = any(item["severity"] == "warning" for item in findings)
verdict = "no_data" if not metrics and not provider else "fail" if critical else "attention" if warning else "pass"
status_counts = Counter(_text(row.get("status"), "unknown") for row in metrics)
generated_at = utc_now()
total = lambda rows, key: sum(value for value in (_number(row.get(key)) for row in rows) if value is not None)
return {
"schema_version": 1,
"report_id": "H6-" + re.sub(r"[-:.TZ+]", "", generated_at)[:14],
"harness": "H6",
"title": "H6 · AgentOps Report",
"description": "Evidence-backed runtime, token, cost, failure, retry and alert telemetry.",
"generated_at": generated_at,
"scope": {"project": project, "from": normalized_from, "to": normalized_to, "run": run},
"verdict": verdict,
"verdict_reasons": [item["code"] for item in findings],
"freshness": {"status": "missing" if not primary["present"] else "stale" if primary["stale"] else "live", "stale_after_s": STALE_AFTER_S, "primary_age_s": primary["age_s"], "sources": evidence},
"summary": {
"runs": len(metrics), "success": success, "failed": failed, "degraded": degraded,
"failure_rate_pct": failure_rate, "retries": int(total(metrics, "retry_count")),
"latency_ms": {
"average": round(sum(latencies) / len(latencies)) if latencies else 0,
"p50": _percentile(latencies, 50), "p95": p95,
"p99": _percentile(latencies, 99), "max": max(latencies) if latencies else 0,
},
"tokens": {
"input": total(metrics, "input_tokens") if any(_number(row.get("input_tokens")) is not None for row in metrics) else None,
"output": total(metrics, "output_tokens") if any(_number(row.get("output_tokens")) is not None for row in metrics) else None,
"total": total(metrics, "total_tokens") if any(_number(row.get("total_tokens")) is not None for row in metrics) else None,
"provider_total": total(provider, "total_tokens") if any(_number(row.get("total_tokens")) is not None for row in provider) else None,
},
"cost_usd": {
"provider_actual": round(total(provider, "cost_usd"), 6) if any(_number(row.get("cost_usd")) is not None for row in provider) else None,
"estimated": round(total(metrics, "cost_estimate"), 6) if any(_number(row.get("cost_estimate")) is not None for row in metrics) else None,
},
"provider_calls": len(provider), "alerts": alert_count,
"coverage": {
"runtime_records": len(metrics), "token_records": token_records, "cost_records": cost_records,
"token_pct": token_pct, "cost_pct": cost_pct,
"quality": {name: quality.get(name, 0) for name in ("complete", "partial", "insufficient", "unknown")},
},
},
"thresholds": {"failure_rate_pct": failure_threshold, "p95_latency_ms": p95_threshold, "freshness_age_s": STALE_AFTER_S},
"findings": findings,
"evidence_sources": evidence,
"data_quality": {"status": "insufficient" if not primary["present"] or (metrics and not token_records and not cost_records) else "partial" if warnings else "complete", "warnings": warnings},
"available_filters": {
"projects": sorted({_row_project(row) for row in all_metrics + all_provider + all_alerts}),
"runs": sorted({_row_run(row) for row in all_metrics + all_provider + all_alerts} - {"unattributed"}),
},
"details": {
"by_status": [{"status": key, "count": value} for key, value in status_counts.most_common()],
"by_step": _grouped(metrics, lambda row: _text(row.get("step"), "unknown-step")),
"by_provider": _grouped(provider, lambda row: f"{_text(row.get('provider'), 'unknown-provider')} · {_text(row.get('model'), 'unknown-model')}"),
"by_cost_source": [{"source": row["key"], "records": row["runs"], "cost_usd": row["cost_usd"] or 0} for row in _grouped(metrics, lambda row: _text(row.get("cost_source"), "unknown"))],
"by_alert": [{"alert": key, "count": value} for key, value in alert_counter.most_common()],
"recent_runs": [sanitize(row) for row in reversed(metrics[-limit:])],
"recent_alerts": [sanitize(row) for row in reversed(alerts[-limit:])],
},
}
def report_html(report: dict[str, Any], kind: str) -> str:
"""Render a self-contained, print-ready evidence dossier."""
title = report.get("title") or (
f"Run assurance · {report.get('trace_id', 'unknown')}"
)
verdict = _text(report.get("verdict"), "unknown").replace("_", " ")
if kind == "h6":
summary = report.get("summary", {})
metrics = [
("Governed runs", summary.get("runs")),
("Failure rate", f"{summary.get('failure_rate_pct', 0)}%"),
("P95 latency", f"{summary.get('latency_ms', {}).get('p95', 0)} ms"),
("Data quality", report.get("data_quality", {}).get("status")),
]
findings = report.get("findings", [])
statuses = report.get("details", {}).get("by_status", [])
maximum = max(
[float(item.get("count", 0) or 0) for item in statuses] or [1]
)
total_runs = float(summary.get("runs", 0) or 0)
outcome_rows = "".join(
'<li><div><span><strong>%s</strong><small>%s%% of selected runs</small></span><b>%s</b></div>'
'<i><em class="%s" style="width:%s%%"></em></i></li>' % (
escape(_text(item.get("status"), "unknown").replace("_", " ")),
round((float(item.get("count", 0) or 0) / total_runs) * 100)
if total_runs else 0,
escape(str(int(float(item.get("count", 0) or 0)))),
"fail" if item.get("status") == "failed" else
"attention" if item.get("status") == "degraded" else "pass",
max((float(item.get("count", 0) or 0) / maximum) * 100, 3)
if float(item.get("count", 0) or 0) else 0,
)
for item in statuses
)
outcomes = (
'<section class="panel"><h2>Run distribution</h2>'
'<p class="sub">Canonical runtime status values, normalized against the largest observed group.</p>'
f'<ol class="distribution">{outcome_rows}</ol></section>'
if outcome_rows else ""
)
else:
summary = report.get("summary", {})
metrics = [
("Evidence gates", f"{summary.get('gates_observed', 0)}/7"),
("Tool calls", summary.get("tool_calls")),
("Failures", summary.get("failures")),
("H6 quality", summary.get("telemetry_quality")),
]
findings = [
{"code": gate.get("title"), "severity": gate.get("status"), "message": gate.get("reason")}
for gate in report.get("gates", [])
]
outcomes = ""
cards = "".join(
f'<div class="metric"><span>{escape(str(label))}</span><strong>{escape(str(value if value is not None else "Unavailable"))}</strong></div>'
for label, value in metrics
)
rows = "".join(
'<article class="finding"><div><code>%s</code><em>%s</em></div><p>%s</p></article>' % (
escape(str(item.get("code", "EVIDENCE"))),
escape(str(item.get("severity", "info"))),
escape(str(item.get("message", "No explanation emitted."))),
) for item in findings
) or '<div class="clear">No threshold breach was detected.</div>'
contract = escape(json.dumps(report, ensure_ascii=False, indent=2))
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{escape(str(title))}</title>
<style>
:root{{--ink:#101828;--muted:#667085;--line:#e4e7ec;--paper:#f7f8fa;--navy:#101828;--cyan:#06b6d4;--green:#067647;--amber:#b54708}}
*{{box-sizing:border-box}}body{{margin:0;background:var(--paper);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}}
main{{max-width:1060px;margin:auto;padding:44px 24px 72px}}header{{border-radius:28px;background:var(--navy);color:white;padding:38px;box-shadow:0 24px 70px #10182820}}
.eyebrow,dt,.metric span{{font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}}.eyebrow{{color:#67e8f9}}h1{{max-width:760px;margin:12px 0 14px;font-size:42px;line-height:1.05;letter-spacing:-.04em}}
.meta{{display:flex;flex-wrap:wrap;gap:12px;color:#98a2b3;font:12px ui-monospace,monospace}}.badge{{display:inline-block;margin-top:20px;border:1px solid #344054;border-radius:999px;padding:8px 12px;font-size:11px;font-weight:800;text-transform:uppercase}}
.grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}}.metric,.panel{{border:1px solid var(--line);border-radius:18px;background:white;padding:20px;box-shadow:0 10px 30px #10182808}}
.metric span{{display:block;color:var(--muted)}}.metric strong{{display:block;margin-top:16px;font-size:24px;letter-spacing:-.03em}}.panel{{margin-top:18px;padding:26px}}h2{{margin:0 0 6px;font-size:20px}}.sub{{margin:0 0 20px;color:var(--muted);font-size:14px}}
.finding{{border-left:4px solid var(--cyan);border-radius:10px;background:#f8fafc;padding:14px 16px;margin-top:10px}}.finding div{{display:flex;justify-content:space-between;gap:16px}}code{{font-weight:800}}em{{color:var(--muted);font-size:11px;text-transform:uppercase}}.finding p{{margin:8px 0 0;color:#344054;font-size:14px;line-height:1.55}}.clear{{border-radius:12px;background:#ecfdf3;color:var(--green);padding:16px}}
.distribution{{display:grid;gap:16px;margin:20px 0 0;padding:0;list-style:none}}.distribution li>div{{display:flex;align-items:end;justify-content:space-between;gap:16px}}.distribution span strong,.distribution span small{{display:block}}.distribution span strong{{font-size:13px;text-transform:capitalize}}.distribution span small{{margin-top:4px;color:var(--muted);font-size:11px}}.distribution b{{font:700 12px ui-monospace,monospace}}.distribution i{{display:block;overflow:hidden;height:8px;margin-top:8px;border-radius:99px;background:#eaecf0}}.distribution em{{display:block;height:100%;border-radius:inherit;background:var(--cyan)}}.distribution em.pass{{background:#12b76a}}.distribution em.attention{{background:#f79009}}.distribution em.fail{{background:#f04438}}
details{{margin-top:18px}}summary{{cursor:pointer;font-weight:700}}pre{{overflow:auto;max-height:720px;border-radius:14px;background:#101828;color:#d0d5dd;padding:20px;font:11px/1.6 ui-monospace,monospace}}
footer{{margin-top:22px;color:var(--muted);font-size:11px}}@media(max-width:760px){{h1{{font-size:32px}}.grid{{grid-template-columns:1fr 1fr}}header{{padding:28px}}}}@media print{{body{{background:white}}main{{max-width:none;padding:0}}header,.panel,.metric{{box-shadow:none}}details{{break-before:page}}}}
</style></head><body><main><header><div class="eyebrow">CASAN · Independent assurance dossier</div><h1>{escape(str(title))}</h1>
<div class="meta"><span>{escape(str(report.get("report_id", "")))}</span><span>Generated {escape(str(report.get("generated_at", "")))}</span></div><div class="badge">Verdict · {escape(verdict)}</div></header>
<section class="grid">{cards}</section><section class="panel"><h2>Decision evidence</h2><p class="sub">Threshold findings and control outcomes from canonical local evidence.</p>{rows}</section>{outcomes}
<section class="panel"><h2>Machine-auditable contract</h2><p class="sub">The complete sanitized report payload used to render this dossier.</p><details><summary>Open JSON evidence</summary><pre>{contract}</pre></details></section>
<footer>Generated on demand by CASAN Core Local Assurance Viewer · read-only · offline-capable</footer></main></body></html>"""
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""Loopback-only HTTP server for the CASAN Core Local Assurance Viewer."""
from __future__ import annotations
import argparse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import mimetypes
import os
from pathlib import Path
import signal
import threading
from urllib.parse import parse_qs, unquote, urlparse
from local_report import (
SAFE_ID,
h6_report,
project_profile,
report_html,
run_report,
runs,
trace_graph,
)
ASSET_ROOT = Path(__file__).resolve().parents[2] / "assets" / "local-viewer"
MAX_RESPONSE_BYTES = 24 * 1024 * 1024
def atomic_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
temporary.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
try:
os.chmod(temporary, 0o600)
except OSError:
if os.name != "nt":
raise
os.replace(temporary, path)
class ViewerServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = False
def __init__(self, address, root: Path, token: str):
self.root = root.resolve()
self.token = token
super().__init__(address, ViewerHandler)
class ViewerHandler(BaseHTTPRequestHandler):
server_version = "CASAN-Core-Viewer/1"
sys_version = ""
@property
def viewer(self) -> ViewerServer:
return self.server # type: ignore[return-value]
def log_message(self, fmt: str, *args) -> None:
# Never persist the session token from the initial/query download URL.
print(
"%s - - [%s] %s %s" % (
self.client_address[0],
self.log_date_time_string(),
self.command,
urlparse(self.path).path,
),
flush=True,
)
def _host_allowed(self) -> bool:
host = self.headers.get("Host", "").split(":", 1)[0].strip("[]").lower()
return host in {"127.0.0.1", "localhost", "::1"}
def _authorized(self, query: dict[str, list[str]]) -> bool:
supplied = self.headers.get("X-CASAN-Viewer-Token", "")
if not supplied:
supplied = query.get("token", [""])[0]
return bool(supplied) and supplied == self.viewer.token
def _headers(self, status: int, content_type: str, length: int) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(length))
self.send_header("Cache-Control", "no-store")
self.send_header("Pragma", "no-cache")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Cross-Origin-Resource-Policy", "same-origin")
self.send_header(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; "
"img-src 'self' data:; connect-src 'self'; object-src 'none'; "
"base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
)
self.end_headers()
def _send(self, status: int, body: bytes, content_type: str) -> None:
if len(body) > MAX_RESPONSE_BYTES:
self._json(413, {"error": "CASAN_VIEW_RESPONSE_TOO_LARGE"})
return
self._headers(status, content_type, len(body))
if self.command != "HEAD":
self.wfile.write(body)
def _json(self, status: int, payload) -> None:
self._send(
status,
(json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8"),
"application/json; charset=utf-8",
)
def _asset(self, name: str) -> None:
safe_name = "index.html" if name in {"", "/"} else name.lstrip("/")
if safe_name not in {"index.html", "app.css", "app.js", "favicon.svg"}:
self._json(404, {"error": "CASAN_VIEW_NOT_FOUND"})
return
path = ASSET_ROOT / safe_name
try:
body = path.read_bytes()
except OSError:
self._json(503, {"error": "CASAN_VIEW_ASSET_MISSING"})
return
content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
if content_type.startswith("text/") or content_type == "application/javascript":
content_type += "; charset=utf-8"
self._send(200, body, content_type)
def _download(self, report: dict, fmt: str, filename: str) -> None:
if fmt == "json":
body = (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
content_type = "application/json; charset=utf-8"
elif fmt == "html":
body = report_html(report, "h6" if report.get("harness") == "H6" else "run").encode("utf-8")
content_type = "text/html; charset=utf-8"
else:
self._json(400, {"error": "CASAN_REPORT_FORMAT_INVALID"})
return
if len(body) > MAX_RESPONSE_BYTES:
self._json(413, {"error": "CASAN_REPORT_TOO_LARGE"})
return
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Disposition", f'attachment; filename="{filename}.{fmt}"')
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Referrer-Policy", "no-referrer")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def do_HEAD(self) -> None:
self.do_GET()
def do_GET(self) -> None:
if self.client_address[0] not in {"127.0.0.1", "::1"} or not self._host_allowed():
self._json(403, {"error": "CASAN_VIEW_LOOPBACK_ONLY"})
return
parsed = urlparse(self.path)
path = unquote(parsed.path)
query = parse_qs(parsed.query, keep_blank_values=False)
if path in {"/", "/index.html", "/app.css", "/app.js"}:
self._asset(path)
return
if not self._authorized(query):
self._json(401, {"error": "CASAN_VIEW_TOKEN_REQUIRED"})
return
try:
if path == "/healthz":
self._json(200, {
"status": "ok",
"mode": "local_core",
"project_id": project_profile(self.viewer.root)["project_id"],
"pid": os.getpid(),
})
elif path == "/api/v1/project":
self._json(200, project_profile(self.viewer.root))
elif path == "/api/v1/runs":
self._json(200, runs(self.viewer.root, int(query.get("limit", ["100"])[0])))
elif path == "/api/v1/reports/h6":
report = self._h6(query)
self._json(200, report)
elif path == "/api/v1/reports/h6/export":
report = self._h6(query)
self._download(report, query.get("format", ["html"])[0], report["report_id"])
elif path == "/__shutdown":
self._json(200, {"status": "stopping"})
threading.Thread(target=self.viewer.shutdown, daemon=True).start()
elif path.startswith("/api/v1/runs/") and path.endswith("/graph"):
trace_id = path[len("/api/v1/runs/"):-len("/graph")].strip("/")
self._trace_required(trace_id)
self._json(200, trace_graph(self.viewer.root, trace_id))
elif path.startswith("/api/v1/reports/run/"):
suffix = path[len("/api/v1/reports/run/"):].strip("/")
exporting = suffix.endswith("/export")
trace_id = suffix[:-len("/export")].strip("/") if exporting else suffix
self._trace_required(trace_id)
report = run_report(self.viewer.root, trace_id)
if report["verdict"] == "not_found":
self._json(404, {"error": "CASAN_RUN_NOT_FOUND"})
elif exporting:
self._download(
report,
query.get("format", ["html"])[0],
report["report_id"],
)
else:
self._json(200, report)
else:
self._json(404, {"error": "CASAN_VIEW_NOT_FOUND"})
except (TypeError, ValueError):
self._json(400, {"error": "CASAN_VIEW_INVALID_QUERY"})
except BrokenPipeError:
return
except Exception as error: # fail closed without leaking project paths
self.log_error("request failed: %s", type(error).__name__)
self._json(500, {"error": "CASAN_VIEW_INTERNAL_ERROR"})
def _trace_required(self, trace_id: str) -> None:
if not SAFE_ID.fullmatch(trace_id):
raise ValueError("unsafe trace id")
def _h6(self, query: dict[str, list[str]]) -> dict:
return h6_report(
self.viewer.root,
project=query.get("project", [None])[0],
run=query.get("run", [None])[0],
from_date=query.get("from", [None])[0],
to_date=query.get("to", [None])[0],
limit=int(query.get("limit", ["50"])[0]),
)
def do_POST(self) -> None:
self._json(405, {"error": "CASAN_VIEW_READ_ONLY"})
do_PUT = do_POST
do_PATCH = do_POST
do_DELETE = do_POST
def serve(root: Path, port: int, token: str, state_file: Path) -> int:
if not token or len(token) < 32:
raise ValueError("viewer token is too short")
server = ViewerServer(("127.0.0.1", port), root, token)
actual_port = server.server_address[1]
atomic_json(state_file, {
"schema_version": 1,
"pid": os.getpid(),
"port": actual_port,
"token": token,
"project_root": str(root.resolve()),
"started_at": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc
).isoformat().replace("+00:00", "Z"),
})
def stop(_signum, _frame):
threading.Thread(target=server.shutdown, daemon=True).start()
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
try:
server.serve_forever(poll_interval=0.25)
finally:
server.server_close()
try:
current = json.loads(state_file.read_text(encoding="utf-8"))
if current.get("pid") == os.getpid():
state_file.unlink()
except (OSError, ValueError):
pass
return 0
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", required=True)
parser.add_argument("--port", type=int, default=0)
parser.add_argument("--token", required=True)
parser.add_argument("--state-file", required=True)
args = parser.parse_args(argv)
return serve(
Path(args.root).expanduser().resolve(),
max(0, min(args.port, 65535)),
args.token,
Path(args.state_file).expanduser().resolve(),
)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,527 @@
#!/usr/bin/env python3
"""CASAN commercial-readiness discovery and assessment.
This module is intentionally stdlib-only. It assesses the CASAN-owned
installation contract without changing application source or inventing domain
traceability. Core, Domain Pipeline, and Provider Telemetry are independent
dimensions because each has a different owner and activation lifecycle.
"""
from __future__ import annotations
from datetime import datetime, timezone
import json
import os
from pathlib import Path
from typing import Any
SCHEMA_VERSION = 1
SKIP_DIRECTORIES = {
".casan", ".git", ".hg", ".idea", ".specify", ".svn", ".venv",
"coverage", "dist", "node_modules", "target", "vendor",
}
PROJECT_MARKERS = (
"package.json", "pyproject.toml", "pom.xml", "build.gradle",
"build.gradle.kts", "go.mod", "Cargo.toml", "composer.json",
)
SOURCE_ROOT_NAMES = ("src", "app", "apps", "packages", "lib", "services")
REQUIREMENT_NAMES = (
"requirements.md", "requirement.md", "srs.md", "spec.md",
"product-requirements.md",
)
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
return value if isinstance(value, dict) else {}
except (OSError, TypeError, ValueError):
return {}
def write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
os.replace(temporary, path)
def configure_domain(root: str | Path, manifest: str | Path) -> dict[str, Any]:
"""Select an existing project manifest without copying or editing app code."""
project_root = Path(root).expanduser().resolve()
selected = Path(manifest).expanduser()
if not selected.is_absolute():
selected = project_root / selected
selected = selected.resolve()
try:
relative = selected.relative_to(project_root).as_posix()
except ValueError as error:
raise ValueError("domain manifest must stay inside the project root") from error
if not selected.is_file():
raise ValueError("domain manifest does not exist")
try:
document = json.loads(selected.read_text(encoding="utf-8"))
except (OSError, TypeError, ValueError) as error:
raise ValueError("domain manifest must be valid JSON") from error
if not isinstance(document, dict) or not (
document.get("project_id") or document.get("schema_version")
):
raise ValueError(
"domain manifest must declare project_id or schema_version")
value = {
"schema_version": SCHEMA_VERSION,
"configured_at": utc_now(),
"manifest": relative,
"ownership": "project_source_referenced_by_casan",
}
write_json(project_root / ".casan" / "domain.json", value)
return value
def _relative(root: Path, path: Path) -> str:
return path.relative_to(root).as_posix()
def _bounded_files(root: Path, maximum_depth: int = 5) -> list[Path]:
files: list[Path] = []
for current, directories, names in os.walk(root):
current_path = Path(current)
try:
depth = len(current_path.relative_to(root).parts)
except ValueError:
continue
directories[:] = sorted(
name for name in directories
if name not in SKIP_DIRECTORIES and not name.startswith(".cache")
)
if depth >= maximum_depth:
directories[:] = []
for name in sorted(names):
files.append(current_path / name)
if len(files) >= 10_000:
return files
return files
def discover(root: str | Path) -> dict[str, Any]:
project_root = Path(root).expanduser().resolve()
priority_files: list[Path] = []
for relative in ("apps", "packages", "docs"):
candidate = project_root / relative
if candidate.is_dir():
priority_files.extend(_bounded_files(candidate, maximum_depth=6))
files = list(dict.fromkeys(
priority_files + _bounded_files(project_root)
))
markers = [
name for name in PROJECT_MARKERS
if (project_root / name).is_file()
]
source_roots = [
name for name in SOURCE_ROOT_NAMES
if (project_root / name).is_dir()
]
domain_packs: list[str] = []
manifests: list[str] = []
requirements: list[str] = []
for path in files:
relative = _relative(project_root, path)
lowered = path.name.lower()
if lowered == "domain-pack.yaml":
domain_packs.append(relative)
elif lowered == "project.manifest.json":
manifests.append(relative)
elif (
lowered in REQUIREMENT_NAMES
or ("requirement" in lowered and lowered.endswith(".md"))
):
requirements.append(relative)
return {
"schema_version": SCHEMA_VERSION,
"generated_at": utc_now(),
"project_root": str(project_root),
"project_markers": markers[:32],
"source_roots": source_roots[:32],
"domain_pack_candidates": domain_packs[:64],
"project_manifest_candidates": manifests[:64],
"requirements_candidates": requirements[:64],
"scan": {
"maximum_depth": 5,
"maximum_files_per_root": 10_000,
"priority_roots": [
value for value in ("apps", "packages", "docs")
if (project_root / value).is_dir()
],
"files_observed": len(files),
"truncated": len(files) >= 10_000,
},
}
def _contains(path: Path, *needles: str) -> bool:
try:
if not path.is_file() or path.stat().st_size > 2 * 1024 * 1024:
return False
content = path.read_text(encoding="utf-8", errors="replace")
return all(needle in content for needle in needles)
except OSError:
return False
def _runtime_harness(root: Path, config: dict[str, Any]) -> Path | None:
runtime_path = config.get("runtime_path")
if not isinstance(runtime_path, str) or not runtime_path.strip():
return None
base = Path(runtime_path).expanduser()
if not base.is_absolute():
base = root / base
candidate = base.resolve()
nested = candidate / "packages" / "casan-harness"
if nested.is_dir():
return nested
return candidate if candidate.name == "casan-harness" and candidate.is_dir() else None
def _check(
gate: str,
title: str,
status: str,
summary: str,
evidence: list[str] | None = None,
) -> dict[str, Any]:
return {
"gate": gate,
"legacy_gate": gate,
"category": "readiness_check",
"check_id": "ReadinessCheck.%s" % gate,
"title": title,
"status": status,
"summary": summary,
"evidence": evidence or [],
}
def _provider_available(root: Path) -> bool:
candidates = (
root / ".specify" / "logs" / "level5" / "provider-usage.jsonl",
root / ".specify" / "logs" / "cost" / "metrics.jsonl",
)
for path in candidates:
try:
if path.is_file() and path.stat().st_size > 2:
text = path.read_text(encoding="utf-8", errors="replace")[-262_144:]
for line in reversed(text.splitlines()):
try:
row = json.loads(line)
except (TypeError, ValueError):
continue
if not isinstance(row, dict):
continue
if (
isinstance(row.get("provider"), str) and row["provider"].strip()
or isinstance(row.get("model"), str) and row["model"].strip()
or isinstance(row.get("total_tokens"), (int, float))
or isinstance(row.get("cost_estimate"), (int, float))
):
return True
except OSError:
continue
return False
def assess(
root: str | Path,
*,
persist: bool = False,
client_state: dict[str, Any] | None = None,
) -> dict[str, Any]:
project_root = Path(root).expanduser().resolve()
config_path = project_root / ".casan" / "config.json"
lock_path = project_root / ".casan" / "version.lock"
bootstrap_path = project_root / ".casan" / "casan-hook.py"
config = read_json(config_path)
lock = read_json(lock_path)
domain_config = read_json(project_root / ".casan" / "domain.json")
previous_readiness = read_json(project_root / ".casan" / "readiness.json")
if client_state is None:
previous_core = (
(previous_readiness.get("dimensions") or {}).get("core") or {}
)
previous_client_state = previous_core.get("client_activation")
client_state = (
previous_client_state
if isinstance(previous_client_state, dict) else {}
)
discovery = discover(project_root)
clients = [
str(value) for value in config.get("clients", [])
if isinstance(value, str)
]
runtime = _runtime_harness(project_root, config)
h1_ok = bool(config.get("project_id") and config.get("schema_version"))
h2_evidence: list[str] = []
client_integrations: list[dict[str, Any]] = []
for client in clients:
declared_state = (client_state or {}).get(client) or {}
if client == "claude":
relative = ".claude/settings.json"
present = _contains(
project_root / relative, "casan-hook.py", "--client claude")
operational = bool(
present and declared_state.get("operational", True))
elif client == "codex":
relative = ".codex/hooks.json"
present = _contains(
project_root / relative, "casan-hook.py", "--client codex")
trust_state = declared_state
operational = bool(
present and trust_state.get("verified")
and trust_state.get("operational")
)
elif client == "vscode-copilot":
relative = ".casan/casan-hook.py"
present = (project_root / relative).is_file()
operational = bool(
present and declared_state.get("operational", True))
else:
client_integrations.append({
"client": client,
"status": "unsupported",
"evidence": None,
})
continue
if present:
h2_evidence.append(relative)
client_integrations.append({
"client": client,
"status": (
"operational" if operational else
"action_required" if present else
"missing"
),
"evidence": relative if present else None,
})
operational_clients = sum(
item["status"] == "operational" for item in client_integrations)
configured_clients = sum(
item["status"] in {"operational", "action_required"}
for item in client_integrations)
h2_status = (
"warning" if operational_clients and operational_clients < len(client_integrations) else
"pass" if operational_clients else
"action_required" if configured_clients else
"fail"
)
report_engine = (
runtime / "scripts" / "python" / "local_report.py"
if runtime else Path()
)
h3_ok = bool(runtime and report_engine.is_file())
enforcement = str(config.get("enforcement_mode") or "unknown")
h4_status = "pass" if enforcement == "enforce" else "warning"
state_dirs = (
project_root / ".specify" / "logs",
project_root / ".specify" / "state",
)
h5_ok = all(
path.is_dir() and os.access(path, os.W_OK | os.X_OK)
for path in state_dirs
)
provider_available = _provider_available(project_root)
h7_ok = bool(
lock.get("harness_hash")
and lock.get("harness_version")
and bootstrap_path.is_file()
)
trust = (client_state or {}).get("codex") or {}
codex_action_required = (
"codex" in clients
and not (trust.get("verified") and trust.get("operational"))
)
checks = [
_check("H1", "Project contract", "pass" if h1_ok else "fail",
"CASAN project identity and schema are valid." if h1_ok
else "CASAN project configuration is missing or invalid.",
[".casan/config.json"] if h1_ok else []),
_check("H2", "Client activation", h2_status,
f"{operational_clients} configured client route(s) are operational."
if h2_status in {"pass", "warning"} else
"Configured client hooks require an activation step."
if h2_status == "action_required" else
"No complete supported client integration is installed.",
h2_evidence),
_check("H3", "Local report capability", "pass" if h3_ok else "fail",
"Core Local Assurance Viewer and on-demand export are available."
if h3_ok else "The resolved Core report runtime is unavailable.",
[str(report_engine)] if h3_ok else []),
_check("H4", "Enforcement boundary", h4_status,
"Policy enforcement is active." if enforcement == "enforce"
else "Observe mode records evidence but does not enforce decisions.",
[".casan/config.json"]),
_check("H5", "Evidence persistence", "pass" if h5_ok else "fail",
"Local logs and state directories are writable." if h5_ok
else "CASAN evidence directories are missing or not writable.",
[".specify/logs", ".specify/state"] if h5_ok else []),
_check("H6", "Provider telemetry", "pass" if provider_available else "optional_unavailable",
"Provider token/cost telemetry is available." if provider_available
else "Core runtime evidence is available; provider token/cost telemetry is optional and not observed yet."),
_check("H7", "Pinned runtime", "pass" if h7_ok else "fail",
"The project bootstrap and pinned runtime contract are present."
if h7_ok else "The bootstrap or version pin is incomplete.",
[".casan/casan-hook.py", ".casan/version.lock"] if h7_ok else []),
]
blocking = any(item["status"] == "fail" for item in checks)
action_required = any(
item["status"] == "action_required" for item in checks
)
attention = any(item["status"] == "warning" for item in checks)
core_status = (
"blocked" if blocking else
"action_required" if action_required else
"ready_with_attention" if attention else
"ready"
)
manifests = discovery["project_manifest_candidates"]
packs = discovery["domain_pack_candidates"]
selected_manifest = domain_config.get("manifest")
selected_path = (
project_root / selected_manifest
if isinstance(selected_manifest, str) else None
)
if selected_path and selected_path.is_file():
domain_status = "configured"
domain_summary = (
"A project manifest is selected. Run project validation or the "
"project-owned pipeline to produce domain acceptance evidence."
)
elif manifests:
domain_status = "discovered"
domain_summary = (
"Project manifest candidates were discovered but none is selected. "
"Use `casan domain configure <manifest>` when domain acceptance is needed."
)
elif packs:
domain_status = "discovered"
domain_summary = (
"A Domain Pack was discovered, but no project manifest proves a "
"runnable domain pipeline."
)
else:
domain_status = "not_configured"
domain_summary = (
"No Domain Pack is configured. Core prompt assurance and reports "
"remain fully available without modifying application source."
)
telemetry_status = "available" if provider_available else "optional_unavailable"
next_actions: list[dict[str, str]] = []
if blocking:
next_actions.append({
"code": "repair_core",
"message": "Run `casan init` again, then `casan doctor`.",
})
if codex_action_required:
reason = str(trust.get("reason") or trust.get("status") or "not_verified")
next_actions.append({
"code": "verify_codex_trust",
"message": (
"Open `/hooks` in Codex and review the project hooks, then run "
f"`casan doctor` (current trust state: {reason})."
),
})
for integration in client_integrations:
if (
integration["status"] != "operational"
and integration["client"] != "codex"
):
next_actions.append({
"code": "activate_%s" % integration["client"].replace("-", "_"),
"message": (
f"Complete activation for the selected "
f"{integration['client']} client route, then run `casan doctor`."
),
})
optional_actions: list[dict[str, str]] = []
if domain_status != "configured":
optional_actions.append({
"code": "configure_domain_pipeline",
"message": (
"Optional: install/configure a CASAN Domain Pack when the team "
"needs project-specific SRS→test acceptance."
),
})
readiness = {
"schema_version": SCHEMA_VERSION,
"generated_at": utc_now(),
"project_id": str(config.get("project_id") or project_root.name),
"overall_status": core_status,
"dimensions": {
"core": {
"status": core_status,
"summary": (
"Core is ready for governed prompts and visual local reports."
if core_status == "ready" else
"Core is operational through at least one client route; another selected route needs attention."
if core_status == "ready_with_attention" else
"Core requires an explicit activation step."
if core_status == "action_required" else
"Core installation is incomplete."
),
"acceptance": checks,
"client_activation": client_state or {},
"client_integrations": client_integrations,
},
"domain_pipeline": {
"status": domain_status,
"summary": domain_summary,
"domain_packs": packs,
"project_manifests": manifests,
"selected_manifest": selected_manifest
if selected_path and selected_path.is_file() else None,
},
"provider_telemetry": {
"status": telemetry_status,
"summary": (
"Provider token and cost telemetry has been observed."
if provider_available else
"Provider token and cost telemetry is optional and has not "
"been observed; this does not block Core."
),
},
},
"next_actions": next_actions,
"optional_actions": optional_actions,
"discovery_file": ".casan/discovery.json",
}
if persist:
write_json(project_root / ".casan" / "discovery.json", discovery)
write_json(project_root / ".casan" / "readiness.json", readiness)
return readiness
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", default=os.getcwd())
parser.add_argument("--write", action="store_true")
arguments = parser.parse_args()
print(json.dumps(
assess(arguments.root, persist=arguments.write),
ensure_ascii=False,
indent=2,
))
@@ -1,102 +1,188 @@
#!/usr/bin/env python3
"""Read-only CASAN report discovery for Core installations.
Core owns the trace/evidence source of truth but not the Control Plane UI. This
helper exposes the latest materialized receipt and opens an enrolled dashboard
without generating HTML on the prompt hot path.
"""
"""CASAN Core report, export and Local Assurance Viewer commands."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import re
import secrets
import subprocess
import sys
import time
from urllib.error import URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
import webbrowser
from urllib.parse import quote
from local_report import (
SAFE_ID,
h6_report,
read_json,
report_html,
run_report,
)
from readiness import (
assess as assess_readiness,
configure_domain,
discover as discover_project,
)
def find_root(start):
current = os.path.abspath(start)
while current != os.path.dirname(current):
if os.path.isfile(os.path.join(current, ".casan", "config.json")):
return current
current = os.path.dirname(current)
return os.path.abspath(start)
def find_root(start: str) -> Path:
current = Path(start).expanduser().resolve()
for candidate in (current, *current.parents):
if (candidate / ".casan" / "config.json").is_file():
return candidate
return current
def read_json(path):
def latest_receipt(root: Path) -> dict:
return read_json(root / ".specify" / "state" / "latest-run.json")
def require_trace(value, receipt) -> str:
trace_id = str(value or receipt.get("trace_id") or "")
if not SAFE_ID.fullmatch(trace_id):
raise ValueError("no safe trace id was supplied and no latest run exists")
return trace_id
def viewer_state_path(root: Path) -> Path:
return root / ".specify" / "state" / "local-viewer.json"
def viewer_log_path(root: Path) -> Path:
return root / ".specify" / "state" / "local-viewer.log"
def load_viewer_state(root: Path) -> dict:
state = read_json(viewer_state_path(root))
if state.get("project_root") != str(root.resolve()):
return {}
if not isinstance(state.get("port"), int) or not isinstance(state.get("token"), str):
return {}
return state
def health(state: dict, timeout: float = 0.7) -> dict:
if not state:
return {}
request = Request(
f"http://127.0.0.1:{state['port']}/healthz",
headers={"X-CASAN-Viewer-Token": state["token"]},
)
try:
with open(path, "r", encoding="utf-8") as handle:
value = json.load(handle)
return value if isinstance(value, dict) else {}
except (OSError, ValueError):
with urlopen(request, timeout=timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
return payload if response.status == 200 and isinstance(payload, dict) else {}
except (OSError, URLError, ValueError):
return {}
def dashboard_base(root):
configured = os.environ.get("CASAN_DASHBOARD_URL")
if not configured:
control_plane = read_json(
os.path.join(root, ".casan", "config.json")
).get("control_plane")
if isinstance(control_plane, dict):
configured = control_plane.get("dashboard_url")
base = str(configured or "").strip().rstrip("/")
return base if re.match(r"^https?://[a-zA-Z0-9]", base) else None
def ensure_viewer(root: Path, port: int = 0) -> dict:
existing = load_viewer_state(root)
if health(existing):
return existing
state_path = viewer_state_path(root)
state_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = state_path.with_suffix(".lock")
acquired = False
for _attempt in range(70):
try:
lock_path.mkdir()
acquired = True
break
except FileExistsError:
state = load_viewer_state(root)
if health(state):
return state
time.sleep(0.1)
if not acquired:
try:
if time.time() - lock_path.stat().st_mtime > 15:
lock_path.rmdir()
lock_path.mkdir()
acquired = True
except OSError:
pass
if not acquired:
raise RuntimeError("another viewer start is still in progress")
try:
return _start_viewer(root, port, state_path)
finally:
try:
lock_path.rmdir()
except OSError:
pass
def latest_receipt(root):
return read_json(
os.path.join(root, ".specify", "state", "latest-run.json")
)
def _start_viewer(root: Path, port: int, state_path: Path) -> dict:
existing = load_viewer_state(root)
if health(existing):
return existing
try:
state_path.unlink()
except OSError:
pass
log_path = viewer_log_path(root)
try:
if log_path.stat().st_size > 1024 * 1024:
rotated = log_path.with_suffix(".log.1")
try:
rotated.unlink()
except OSError:
pass
os.replace(log_path, rotated)
except OSError:
pass
token = secrets.token_urlsafe(32)
server = Path(__file__).with_name("local_viewer.py")
with log_path.open("ab", buffering=0) as log:
popen_options = {
"cwd": str(root),
"stdin": subprocess.DEVNULL,
"stdout": log,
"stderr": subprocess.STDOUT,
"close_fds": True,
}
if os.name == "nt":
popen_options["creationflags"] = (
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
| getattr(subprocess, "DETACHED_PROCESS", 0)
)
else:
popen_options["start_new_session"] = True
subprocess.Popen(
[
sys.executable, str(server),
"--root", str(root),
"--port", str(port),
"--token", token,
"--state-file", str(state_path),
],
**popen_options,
)
for _attempt in range(50):
time.sleep(0.1)
state = load_viewer_state(root)
if state.get("token") == token and health(state):
return state
raise RuntimeError(f"viewer did not become ready; inspect {log_path}")
def require_trace(value, receipt):
trace_id = value or receipt.get("trace_id")
if not trace_id or not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$", str(trace_id)):
raise ValueError("no safe trace id was supplied and no latest run exists")
return str(trace_id)
def viewer_url(state: dict, trace_id: str | None = None, view: str | None = None) -> str:
query = {"token": state["token"]}
if trace_id:
query.update({"view": "runs", "trace": trace_id})
elif view:
query["view"] = view
return f"http://127.0.0.1:{state['port']}/?{urlencode(query)}"
def report_url(root, trace_id):
base = dashboard_base(root)
if not base:
return None
return "%s/runs?trace=%s" % (base, quote(trace_id, safe=""))
def export_url(root, trace_id, export_format):
base = dashboard_base(root)
if not base:
return None
return "%s/api/v1/reports/run/%s/export?format=%s" % (
base,
quote(trace_id, safe=""),
export_format,
)
def print_receipt(receipt):
if not receipt:
print("CASAN_REPORT_NONE — no finalized prompt receipt exists yet")
return 1
print("CASAN %s" % ("CERTIFIED" if receipt.get("certified") else "NON-CERTIFIED"))
print("trace_id=%s" % (receipt.get("trace_id") or "unknown"))
print("project_id=%s" % (receipt.get("project_id") or "unknown"))
print("h6_quality=%s" % (receipt.get("telemetry_quality") or "unknown"))
print("duration_ms=%s tool_calls=%s failures=%s" % (
receipt.get("duration_ms"),
receipt.get("tool_calls", 0),
receipt.get("failures", 0),
))
if receipt.get("report_url"):
print("report_url=%s" % receipt["report_url"])
return 0
def open_or_print(url, no_open):
def open_or_print(url: str, no_open: bool) -> int:
print(url)
if no_open:
return 0
@@ -106,52 +192,232 @@ def open_or_print(url, no_open):
return 0
def main(argv=None):
parser = argparse.ArgumentParser(description="CASAN Core report discovery")
parser.add_argument("--root", default=os.environ.get("CASAN_APP_ROOT") or os.getcwd())
commands = parser.add_subparsers(dest="command", required=True)
def print_receipt(receipt: dict, root: Path) -> int:
if not receipt:
print("CASAN_REPORT_NONE — no finalized prompt receipt exists yet")
print("next=complete a prompt, then run `casan view`")
return 1
certified = receipt.get("certified") is True
print("CASAN %s" % ("CERTIFIED" if certified else "NON-CERTIFIED"))
print("trace_id=%s" % (receipt.get("trace_id") or "unknown"))
print("project_id=%s" % (receipt.get("project_id") or "unknown"))
print("h6_quality=%s" % (receipt.get("telemetry_quality") or "unknown"))
print("duration_ms=%s tool_calls=%s failures=%s" % (
receipt.get("duration_ms"),
receipt.get("tool_calls", 0),
receipt.get("failures", 0),
))
print("view_command=casan view %s" % (receipt.get("trace_id") or ""))
active_viewer = load_viewer_state(root)
if health(active_viewer):
print("local_report_url=%s" % viewer_url(
active_viewer, str(receipt.get("trace_id") or "") or None))
if receipt.get("report_url"):
print("central_report_url=%s" % receipt["report_url"])
return 0
def export_report(root: Path, args, receipt: dict) -> int:
if args.h6:
report = h6_report(
root,
project=args.project,
run=args.trace_id,
from_date=args.from_date,
to_date=args.to_date,
limit=args.limit,
)
stem = report["report_id"].lower()
kind = "h6"
else:
trace_id = require_trace(args.trace_id, receipt)
report = run_report(root, trace_id)
if report["verdict"] == "not_found":
print("CASAN_REPORT_NOT_FOUND — no evidence exists for that trace", file=sys.stderr)
return 2
stem = re.sub(r"[^a-zA-Z0-9._-]", "-", report["report_id"]).lower()
kind = "run"
destination = Path(args.output or f"{stem}.{args.format}").expanduser()
if not destination.is_absolute():
destination = Path.cwd() / destination
destination.parent.mkdir(parents=True, exist_ok=True)
if args.format == "json":
body = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
else:
body = report_html(report, kind)
destination.write_text(body, encoding="utf-8")
print(str(destination.resolve()))
return 0
def viewer_command(root: Path, action: str, port: int, no_open: bool) -> int:
if action in {"start", "open"}:
try:
state = ensure_viewer(root, port)
except RuntimeError as error:
print(f"CASAN_VIEW_START_FAILED — {error}", file=sys.stderr)
return 3
return open_or_print(viewer_url(state), no_open or action == "start")
state = load_viewer_state(root)
status = health(state)
if action == "status":
if not status:
print("CASAN_VIEW_STOPPED")
return 1
print(json.dumps({
"status": "running",
"pid": state.get("pid"),
"url": viewer_url(state),
"project_id": status.get("project_id"),
"mode": "local_core",
}, indent=2))
return 0
if action == "stop":
if not status:
try:
viewer_state_path(root).unlink()
except OSError:
pass
print("CASAN_VIEW_ALREADY_STOPPED")
return 0
request = Request(
f"http://127.0.0.1:{state['port']}/__shutdown",
headers={"X-CASAN-Viewer-Token": state["token"]},
)
try:
urlopen(request, timeout=1).read()
except (OSError, URLError):
print("CASAN_VIEW_STOP_FAILED", file=sys.stderr)
return 3
for _attempt in range(30):
time.sleep(0.1)
if not health(state, timeout=0.1):
print("CASAN_VIEW_STOPPED")
return 0
print("CASAN_VIEW_STOP_PENDING")
return 0
return 64
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser(description=__doc__)
value.add_argument("--root", default=os.environ.get("CASAN_APP_ROOT") or os.getcwd())
commands = value.add_subparsers(dest="command", required=True)
latest = commands.add_parser("latest", help="show the latest finalized prompt receipt")
latest.add_argument("--json", action="store_true")
view = commands.add_parser("view", help="open a trace in the enrolled Control Plane")
readiness = commands.add_parser(
"readiness",
help="assess Core, Domain Pipeline, and Provider Telemetry separately",
)
readiness.add_argument("--refresh", action="store_true")
readiness.add_argument("--json", action="store_true")
domain = commands.add_parser(
"domain",
help="discover or select an optional project-owned Domain Pack manifest",
)
domain.add_argument("action", choices=["status", "discover", "configure"])
domain.add_argument("manifest", nargs="?")
domain.add_argument("--json", action="store_true")
view = commands.add_parser("view", help="open a trace in the local Core viewer")
view.add_argument("trace_id", nargs="?")
view.add_argument("--no-open", action="store_true", help="print the URL without opening a browser")
view.add_argument("--no-open", action="store_true")
view.add_argument("--port", type=int, default=0)
export = commands.add_parser("export", help="download a trace evidence snapshot on demand")
export = commands.add_parser("export", help="write a self-contained report on demand")
export.add_argument("trace_id", nargs="?")
export.add_argument("--h6", action="store_true", help="export the H6 dossier instead of a run dossier")
export.add_argument("--format", choices=["html", "json"], default="html")
export.add_argument("--no-open", action="store_true", help="print the URL without opening a browser")
export.add_argument("--output", "-o")
export.add_argument("--project")
export.add_argument("--from", dest="from_date")
export.add_argument("--to", dest="to_date")
export.add_argument("--limit", type=int, default=50)
args = parser.parse_args(argv)
viewer = commands.add_parser("viewer", help="manage the local Core viewer")
viewer.add_argument("action", choices=["start", "status", "stop", "open"])
viewer.add_argument("--port", type=int, default=0)
viewer.add_argument("--no-open", action="store_true")
return value
def main(argv=None) -> int:
args = parser().parse_args(argv)
root = find_root(args.root)
receipt = latest_receipt(root)
if args.command == "latest":
if args.json:
print(json.dumps(receipt, ensure_ascii=False, indent=2))
return 0 if receipt else 1
return print_receipt(receipt)
return print_receipt(receipt, root)
if args.command == "readiness":
result = assess_readiness(root, persist=args.refresh)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
dimensions = result["dimensions"]
print(f"CASAN readiness — {result['project_id']}")
print(f" Core {dimensions['core']['status']}")
print(f" Domain Pipeline {dimensions['domain_pipeline']['status']}")
print(f" Provider Telemetry {dimensions['provider_telemetry']['status']}")
for action in result.get("next_actions", []):
print(f" → {action['message']}")
return 2 if result["overall_status"] == "blocked" else 0
if args.command == "domain":
try:
if args.action == "discover":
payload = discover_project(root)
elif args.action == "configure":
if not args.manifest:
raise ValueError(
"`casan domain configure` requires a manifest path")
configure_domain(root, args.manifest)
payload = assess_readiness(root, persist=True)
else:
payload = assess_readiness(root, persist=False)[
"dimensions"]["domain_pipeline"]
except ValueError as error:
print(f"CASAN_DOMAIN_INVALID — {error}", file=sys.stderr)
return 2
if args.json:
print(json.dumps(payload, ensure_ascii=False, indent=2))
elif args.action == "discover":
print("CASAN Domain discovery")
print(
f" Manifests {len(payload['project_manifest_candidates'])}")
print(f" Domain Packs {len(payload['domain_pack_candidates'])}")
print(f" Requirements {len(payload['requirements_candidates'])}")
else:
dimension = (
payload["dimensions"]["domain_pipeline"]
if "dimensions" in payload else payload
)
print(f"CASAN Domain Pipeline — {dimension['status']}")
print(f" {dimension['summary']}")
return 0
if args.command == "viewer":
return viewer_command(root, args.action, args.port, args.no_open)
if args.command == "export":
try:
return export_report(root, args, receipt)
except ValueError as error:
print(f"CASAN_REPORT_INVALID — {error}", file=sys.stderr)
return 2
try:
trace_id = require_trace(args.trace_id, receipt)
if args.trace_id and not SAFE_ID.fullmatch(str(args.trace_id)):
raise ValueError("unsafe trace id")
trace_id = str(args.trace_id or receipt.get("trace_id") or "") or None
state = ensure_viewer(root, args.port)
except ValueError as error:
print("CASAN_REPORT_NOT_FOUND — %s" % error, file=sys.stderr)
print(f"CASAN_REPORT_NOT_FOUND — {error}", file=sys.stderr)
return 2
url = (
report_url(root, trace_id)
if args.command == "view"
else export_url(root, trace_id, args.format)
)
if not url:
print(
"CASAN_CONTROL_PLANE_NOT_ENROLLED — run `casan init "
"--dashboard-url https://your-casan.example` or set CASAN_DASHBOARD_URL",
file=sys.stderr,
)
except RuntimeError as error:
print(f"CASAN_VIEW_START_FAILED — {error}", file=sys.stderr)
return 3
return open_or_print(url, args.no_open)
return open_or_print(viewer_url(state, trace_id), args.no_open)
if __name__ == "__main__":
@@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""Deterministic unit and cross-runtime conformance tests for the kernel."""
from __future__ import annotations
import os
import stat
import sys
import tempfile
import unittest
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch
HARNESS_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(HARNESS_ROOT))
from kernel.adapters import AgenticBridgeAdapter, NativeHarnessAdapter
from kernel.contracts import validate_bundle
from kernel.policy import (
H2GateContext,
classify_action,
evaluate_failure_policy,
evaluate_h2_gate,
evaluate_registry_configuration,
evaluate_risk,
evaluate_trust_capabilities,
)
from kernel.taxonomy import CertificationClaim, ReadinessCheck, ReportDimension, RuntimeControl, same_legacy_label
from kernel.supervision import RuntimeCapabilities, negotiate_intervention
class RiskFloorTests(unittest.TestCase):
def test_benign_deploy_has_high_floor_and_requires_approval(self) -> None:
decision = evaluate_risk(action="deploy", content="publish a harmless documentation update", actor="alice")
self.assertEqual(decision["action_class"], "deployment")
self.assertEqual(decision["risk_factors"]["content_risk"], "low")
self.assertEqual(decision["effective_risk"], "high")
self.assertEqual(decision["decision"], "require_approval")
def test_effective_risk_is_maximum_of_all_factors(self) -> None:
decision = evaluate_risk(action="read", resource="/.ssh/id_rsa", actor="alice")
self.assertEqual(decision["risk_factors"]["action_risk"], "high")
self.assertEqual(decision["risk_factors"]["resource_risk"], "high")
self.assertEqual(decision["effective_risk"], "high")
def test_missing_actor_denies_high_impact_action(self) -> None:
decision = evaluate_risk(action="migration", content="apply schema", actor="")
self.assertEqual(decision["decision"], "deny")
self.assertIn("actor_identity_required", decision["reason_codes"])
def test_read_only_shell_command_is_not_side_effecting(self) -> None:
decision = classify_action(tool="Bash", command="git status --short")
self.assertEqual(decision["action_class"], "read_only")
self.assertFalse(decision["side_effecting"])
class RegistryConfigurationTests(unittest.TestCase):
def test_enforce_defaults_registry_on(self) -> None:
result = evaluate_registry_configuration("enforce", "development", None)
self.assertTrue(result["enabled"])
self.assertEqual(result["reason_code"], "h2_registry_default_enabled")
def test_development_bypass_is_visible_and_non_silent(self) -> None:
result = evaluate_registry_configuration("enforce", "test", "0")
self.assertFalse(result["enabled"])
self.assertTrue(result["unsafe_bypass"])
self.assertEqual(result["severity"], "high")
def test_production_bypass_is_rejected(self) -> None:
result = evaluate_registry_configuration("enforce", "production", "0")
self.assertFalse(result["valid"])
self.assertEqual(result["reason_code"], "h2_registry_bypass_forbidden")
class FailurePolicyTests(unittest.TestCase):
def test_all_supported_policies_are_explicit(self) -> None:
for configured in ("halt", "quarantine", "require_approval", "record_only"):
result = evaluate_failure_policy(
side_effecting=False, mode="observe", profile="test", configured=configured,
)
self.assertEqual(result["failure_policy"], configured)
self.assertEqual(result["execution_result"], "failed")
self.assertEqual(result["telemetry_result"], "recorded")
def test_production_side_effect_cannot_be_record_only(self) -> None:
result = evaluate_failure_policy(
side_effecting=True, mode="enforce", profile="production", configured="record_only",
)
self.assertEqual(result["failure_policy"], "halt")
self.assertFalse(result["valid"])
self.assertEqual(result["source"], "production_record_only_forbidden")
def test_invalid_policy_fails_closed(self) -> None:
result = evaluate_failure_policy(
side_effecting=True, mode="enforce", profile="test", configured="continue_anyway",
)
self.assertEqual(result["failure_policy"], "halt")
self.assertFalse(result["valid"])
class TaxonomyTests(unittest.TestCase):
def test_same_h_label_cannot_be_interpreted_as_same_category(self) -> None:
self.assertTrue(same_legacy_label(RuntimeControl.H4, ReadinessCheck.H4))
self.assertNotEqual(RuntimeControl.H4.value, ReadinessCheck.H4.value)
self.assertNotEqual(ReportDimension.H4.value, CertificationClaim.H4.value)
def test_unsupported_runtime_intervention_is_explicit(self) -> None:
result = negotiate_intervention("i-1", "roll_back", "operator", RuntimeCapabilities())
self.assertEqual(result["status"], "unsupported")
self.assertEqual(result["reason_code"], "runtime_intervention_unsupported")
class H2DependencyTests(unittest.TestCase):
def context(self, mode: str) -> H2GateContext:
return H2GateContext(mode, "alice", "write_file", "Edit", "run-1", "test.h2", "idem", 0.5)
def script(self, directory: str, body: str) -> str:
path = Path(directory) / "gate.sh"
path.write_text("#!/usr/bin/env bash\n" + body + "\n", encoding="utf-8")
path.chmod(0o700)
return str(path)
def test_missing_gate_denies_enforce_and_degrades_observe(self) -> None:
with tempfile.TemporaryDirectory() as temp:
missing = str(Path(temp) / "missing.sh")
enforced = evaluate_h2_gate(missing, self.context("enforce"))
observed = evaluate_h2_gate(missing, self.context("observe"))
self.assertFalse(enforced["execution_allowed"])
self.assertEqual(enforced["reason_code"], "h2_gate_unavailable")
self.assertTrue(observed["execution_allowed"])
self.assertEqual(observed["decision"], "observe_only")
self.assertFalse(observed["certifiable"])
def test_permission_denied_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp:
path = self.script(temp, "echo 'TOOL_APPROVED tool=x reason=test'")
os.chmod(path, 0)
result = evaluate_h2_gate(path, self.context("enforce"))
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
self.assertFalse(result["execution_allowed"])
self.assertEqual(result["reason_code"], "h2_gate_permission_denied")
def test_timeout_malformed_and_internal_exception_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp:
timeout = self.script(temp, "sleep 1")
timed = evaluate_h2_gate(timeout, self.context("enforce"))
malformed = self.script(temp, "echo nonsense")
malformed_result = evaluate_h2_gate(malformed, self.context("enforce"))
with patch("kernel.policy.subprocess.run", side_effect=OSError("boom")):
internal = evaluate_h2_gate(malformed, self.context("enforce"))
self.assertEqual(timed["reason_code"], "h2_gate_timeout")
self.assertEqual(malformed_result["reason_code"], "h2_gate_malformed_response")
self.assertEqual(internal["reason_code"], "h2_gate_internal_error")
self.assertTrue(all(not item["execution_allowed"] for item in (timed, malformed_result, internal)))
def test_explicit_approval_and_denial_protocols(self) -> None:
with tempfile.TemporaryDirectory() as temp:
allowed = self.script(temp, "echo 'TOOL_APPROVED tool=write_file reason=registered'")
allow_result = evaluate_h2_gate(allowed, self.context("enforce"))
denied = self.script(temp, "echo 'TOOL_DENIED tool=write_file reason=missing_agent_identity' >&2; exit 2")
deny_result = evaluate_h2_gate(denied, self.context("enforce"))
self.assertTrue(allow_result["execution_allowed"])
self.assertFalse(deny_result["execution_allowed"])
self.assertEqual(deny_result["reason_code"], "h2_denied:missing_agent_identity")
class TrustRootTests(unittest.TestCase):
def test_production_refuses_local_fallback(self) -> None:
result = evaluate_trust_capabilities({"CASAN_PROFILE": "production"})
self.assertFalse(result["ready"])
self.assertFalse(result["certifiable"])
self.assertIn("external_signing_trust_root_required", result["reason_codes"])
def test_external_configuration_is_capable_but_not_claimed_provisioned(self) -> None:
result = evaluate_trust_capabilities({
"CASAN_PROFILE": "production",
"CASAN_SIGNING_PROVIDER": "vault_kms",
"VAULT_ADDR": "https://vault.example",
"VAULT_TOKEN": "redacted-runtime-token",
"CASAN_IMMUTABLE_ANCHOR_PROVIDER": "s3_object_lock",
"CASAN_S3_BUCKET": "audit-lock",
"CASAN_S3_KMS_KEY_ID": "kms-key",
})
self.assertTrue(result["ready"])
self.assertTrue(result["certifiable"])
def test_emergency_override_never_certifies(self) -> None:
result = evaluate_trust_capabilities({
"CASAN_PROFILE": "production",
"CASAN_TRUST_EMERGENCY_OVERRIDE": "1",
})
self.assertFalse(result["certifiable"])
self.assertEqual(result["severity"], "critical")
class CrossRuntimeConformanceTests(unittest.TestCase):
def events(self, **overrides: object) -> tuple[dict[str, object], dict[str, object]]:
common: dict[str, object] = {
"run_id": "run-1", "trace_id": "run-1", "action": "deploy", "tool": "Bash",
"command": "deploy harmless docs", "actor": "alice", "mode": "enforce",
"execution_status": "failed", "assurance_status": "failed", "certification_status": "non_certified",
"certified": False, "correlation_id": "corr-1", "parent_run_id": "parent-1",
"parent_step_id": "parent-step-1", "evidence": [
{"evidence_id": "e-1", "decision": "deny", "category": "runtime_control"},
],
}
common.update(overrides)
return dict(common), dict(common, adapter_version="20.2.0")
def map_both(self, **overrides: object) -> tuple[dict[str, object], dict[str, object]]:
native_event, agentic_event = self.events(**overrides)
native = NativeHarnessAdapter().map_execution(native_event)
agentic = AgenticBridgeAdapter().map_execution(agentic_event)
return native, agentic
def test_01_same_dangerous_action_classification(self) -> None:
native, agentic = self.map_both()
self.assertEqual(validate_bundle(native), [])
self.assertEqual(validate_bundle(agentic), [])
self.assertEqual(native["run"]["risk_summary"]["action_class"], "deployment")
self.assertEqual(agentic["run"]["risk_summary"]["action_class"], "deployment")
self.assertEqual(native["run"]["risk_summary"]["effective_risk"], "high")
def test_02_same_missing_actor_denial(self) -> None:
native, agentic = self.map_both(actor="")
for bundle in (native, agentic):
self.assertEqual(bundle["run"]["risk_summary"]["decision"], "deny")
self.assertIn("actor_identity_required", bundle["run"]["risk_summary"]["reason_codes"])
def test_03_same_missing_h2_gate_denial(self) -> None:
with tempfile.TemporaryDirectory() as temp:
missing = str(Path(temp) / "missing.sh")
decisions = [
evaluate_h2_gate(missing, H2GateContext("enforce", "alice", "write_file", "Edit", "run-1", point, "idem"))
for point in ("native-harness.h2", "agentic-bridge.h2")
]
native, agentic = self.map_both(
action="write_file", tool="Edit", command="safe edit",
policy_decisions=[decisions[0]], evidence=[{"evidence_id": "e-1", "decision": "deny"}],
)
# The second adapter receives the same kernel decision with only its
# enforcement-point extension changed.
agentic_event = self.events(
action="write_file", tool="Edit", command="safe edit",
policy_decisions=[decisions[1]], evidence=[{"evidence_id": "e-1", "decision": "deny"}],
)[1]
agentic = AgenticBridgeAdapter().map_execution(agentic_event)
for bundle in (native, agentic):
decision = bundle["steps"][0]["policy_decisions"][0]
self.assertEqual(decision["decision"], "deny")
self.assertIn("h2_gate_unavailable", decision["reason_codes"])
def test_04_same_approval_requirement(self) -> None:
native, agentic = self.map_both(actor="alice")
for bundle in (native, agentic):
self.assertEqual(bundle["run"]["risk_summary"]["decision"], "require_approval")
self.assertEqual(bundle["run"]["risk_summary"]["evidence_requirement"], "required")
def test_05_same_claim_evidence_relationship(self) -> None:
native, agentic = self.map_both()
for bundle in (native, agentic):
self.assertEqual(bundle["claims"][0]["evidence_refs"], ["e-1"])
self.assertEqual(bundle["evidence"][0]["claim_refs"], [bundle["claims"][0]["claim_id"]])
def test_06_observed_only_execution_is_never_certified(self) -> None:
native, agentic = self.map_both(
mode="observe", certification_status="certified", certified=True,
)
for bundle in (native, agentic):
self.assertIn("observed_only_cannot_be_certified", validate_bundle(bundle))
def test_07_execution_and_assurance_outcomes_are_distinct(self) -> None:
native, agentic = self.map_both(
execution_status="failed", assurance_status="passed", certification_status="non_certified",
)
for bundle in (native, agentic):
outcome = bundle["run"]["outcome"]
self.assertEqual(outcome["execution_result"], "failed")
self.assertEqual(outcome["assurance_result"], "passed")
self.assertEqual(outcome["certification_result"], "non_certified")
def test_08_correlation_and_causation_are_preserved(self) -> None:
native, agentic = self.map_both()
for bundle in (native, agentic):
self.assertEqual(bundle["run"]["correlation_id"], "corr-1")
self.assertEqual(bundle["run"]["parent_run_id"], "parent-1")
self.assertEqual(bundle["steps"][0]["parent_step_id"], "parent-step-1")
self.assertEqual(bundle["trace_links"][0]["from"], "run-1")
def test_09_same_verifier_detects_evidence_tampering(self) -> None:
native, agentic = self.map_both()
for original in (native, agentic):
self.assertEqual(validate_bundle(original), [])
tampered = deepcopy(original)
tampered["evidence"][0]["artifact_ref"] = "inline:tampered"
self.assertIn("evidence.0.integrity_invalid", validate_bundle(tampered))
def test_10_runtime_details_are_namespaced_extensions(self) -> None:
native, agentic = self.map_both()
self.assertNotEqual(native["run"]["runtime"]["type"], agentic["run"]["runtime"]["type"])
self.assertIn("casan.runtime.casan-native-harness", native["extensions"])
self.assertIn("casan.runtime.agentic-bridge", agentic["extensions"])
def test_delegation_depth_is_enforced(self) -> None:
native, _ = self.map_both()
native["actors"].append({
"actor_id": "child", "actor_type": "child_agent", "issuer": "alice",
"trust_level": "verified", "authentication_evidence_ref": None,
})
native["delegations"] = [
{"delegation_id": "d1", "delegator_ref": "alice", "delegate_ref": "child", "allowed_authority": ["read"], "resource_scope": ["project"], "expiry": "2030-01-01T00:00:00Z", "maximum_depth": 3, "parent_delegation_ref": None, "revoked": False, "approval_ref": None},
{"delegation_id": "d2", "delegator_ref": "alice", "delegate_ref": "child", "allowed_authority": ["read"], "resource_scope": ["project"], "expiry": "2030-01-01T00:00:00Z", "maximum_depth": 3, "parent_delegation_ref": "d1", "revoked": False, "approval_ref": None},
{"delegation_id": "d3", "delegator_ref": "alice", "delegate_ref": "child", "allowed_authority": ["read"], "resource_scope": ["project"], "expiry": "2030-01-01T00:00:00Z", "maximum_depth": 1, "parent_delegation_ref": "d2", "revoked": False, "approval_ref": None},
]
self.assertIn("delegations.2.maximum_depth_exceeded", validate_bundle(native))
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Focused bridge integration regressions for the Assurance Kernel upgrade."""
from __future__ import annotations
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
HARNESS_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(HARNESS_ROOT / "scripts" / "python"))
import agentic_bridge as bridge
class BridgeUpgradeIntegrationTests(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.state = str(Path(self.temp.name) / "state")
self.project = str(HARNESS_ROOT.parent.parent)
self.base_env = {
"CASAN_STATE_ROOT": self.state,
"CASAN_AGENTIC_ENFORCEMENT_MODE": "enforce",
"CASAN_AGENT": "boss",
"CASAN_ACTOR": "boss",
"CASAN_PROFILE": "test",
}
def tearDown(self) -> None:
self.temp.cleanup()
def begin(self, actor: str = "boss") -> dict[str, object]:
return bridge.op_begin({
"op": "begin", "client": "codex", "project": self.project,
"session": "integration", "prompt": "edit a source file safely",
"integration_mode": "project_hook", "actor": actor,
})
def fixture_gate(self, body: str) -> str:
path = Path(self.temp.name) / "h2-gate.sh"
path.write_text("#!/usr/bin/env bash\n" + body + "\n", encoding="utf-8")
path.chmod(0o700)
return str(path)
def pre_edit(self, admission_id: str) -> dict[str, object]:
return bridge.op_pre_tool({
"op": "pre-tool", "admission_id": admission_id, "tool": "Edit",
"tool_input": "update a source file", "project": self.project,
})
def test_missing_h2_gate_denies_enforce_with_structured_evidence(self) -> None:
missing = str(Path(self.temp.name) / "missing.sh")
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": missing}, clear=False):
begin = self.begin()
result = self.pre_edit(str(begin["admission_id"]))
record = bridge.load_admission(str(begin["admission_id"]))
self.assertEqual(result["decision"], "deny")
self.assertEqual(result["reason"], "h2_gate_unavailable")
evidence = next(item for item in record["evidence"] if item["kind"] == "tool-registry")
self.assertEqual(evidence["facts"]["mode"], "enforce")
self.assertEqual(evidence["facts"]["actor"], "boss")
self.assertEqual(evidence["facts"]["tool"], "Edit")
self.assertEqual(evidence["facts"]["execution_id"], begin["trace_id"])
self.assertEqual(evidence["facts"]["enforcement_path"], "agentic_bridge.pre_tool.h2_registry")
def test_missing_h2_gate_observe_allows_only_degraded_non_certifiable_execution(self) -> None:
missing = str(Path(self.temp.name) / "missing.sh")
env = {**self.base_env, "CASAN_AGENTIC_ENFORCEMENT_MODE": "observe", "CASAN_H2_GATE_PATH": missing}
with patch.dict(os.environ, env, clear=False):
begin = self.begin()
result = self.pre_edit(str(begin["admission_id"]))
finalized = bridge.op_finalize({"op": "finalize", "admission_id": begin["admission_id"], "stop_reason": "completed"})
self.assertEqual(result["decision"], "allow")
self.assertEqual(finalized["decision"], "non_certified")
self.assertEqual(finalized["certification_strength"], "observed_only")
def test_registry_defaults_on_and_development_bypass_is_visible(self) -> None:
missing = str(Path(self.temp.name) / "missing.sh")
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": missing}, clear=False):
begin = self.begin()
default_result = self.pre_edit(str(begin["admission_id"]))
self.assertEqual(default_result["reason"], "h2_gate_unavailable")
with patch.dict(os.environ, {**self.base_env, "CASAN_AGENTIC_H2_REGISTRY": "0", "CASAN_H2_GATE_PATH": missing}, clear=False):
begin = self.begin()
bypass = self.pre_edit(str(begin["admission_id"]))
record = bridge.load_admission(str(begin["admission_id"]))
self.assertEqual(bypass["decision"], "allow")
self.assertTrue(any("HIGH" in warning for warning in bypass["warnings"]))
self.assertEqual(record["certification_strength"], "observed_only")
def test_benign_deploy_requires_approval_from_action_floor(self) -> None:
gate = self.fixture_gate("echo 'TOOL_APPROVED tool=deploy reason=registered'")
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": gate}, clear=False):
begin = self.begin()
result = bridge.op_pre_tool({
"op": "pre-tool", "admission_id": begin["admission_id"], "tool": "Bash",
"tool_input": "deploy harmless documentation", "project": self.project,
})
self.assertEqual(result["decision"], "require_approval")
risk = result["policy_decision"]
self.assertEqual(risk["action_class"], "deployment")
self.assertEqual(risk["risk_factors"]["content_risk"], "medium")
self.assertEqual(risk["effective_risk"], "high")
def test_structured_shell_payload_classifies_the_inner_read_only_command(self) -> None:
with patch.dict(os.environ, self.base_env, clear=False):
begin = self.begin()
result = bridge.op_pre_tool({
"op": "pre-tool", "admission_id": begin["admission_id"], "tool": "Bash",
"tool_input": {"command": "ls"}, "project": self.project,
})
self.assertEqual(result["decision"], "allow")
self.assertEqual(result["policy_decision"]["action_class"], "read_only")
def test_failed_side_effect_halts_and_cannot_finalize_successfully(self) -> None:
gate = self.fixture_gate("echo 'TOOL_APPROVED tool=write_file reason=registered'")
with patch.dict(os.environ, {**self.base_env, "CASAN_H2_GATE_PATH": gate}, clear=False):
begin = self.begin()
admitted = self.pre_edit(str(begin["admission_id"]))
post = bridge.op_post_tool({
"op": "post-tool", "admission_id": begin["admission_id"], "tool": "Edit",
"status": "error", "duration_ms": 2, "result": "write failed",
})
finalized = bridge.op_finalize({
"op": "finalize", "admission_id": begin["admission_id"],
"stop_reason": "completed", "assistant_summary": "completed",
})
self.assertEqual(admitted["decision"], "allow")
self.assertEqual(post["decision"], "halt")
self.assertFalse(post["assurance_may_continue"])
self.assertEqual(finalized["decision"], "non_certified")
self.assertEqual(finalized["execution_outcome"], "failed")
self.assertEqual(finalized["assurance_outcome"], "failed")
self.assertIn("failed_tool_outcome", finalized["reason"])
kernel_files = list((Path(self.state) / "logs" / "kernel").glob("*.json"))
self.assertEqual(len(kernel_files), 1)
kernel = json.loads(kernel_files[0].read_text(encoding="utf-8"))
self.assertEqual(kernel["run"]["outcome"]["execution_result"], "failed")
self.assertEqual(kernel["run"]["outcome"]["certification_result"], "non_certified")
def test_production_without_external_trust_root_never_certifies(self) -> None:
env = {**self.base_env, "CASAN_PROFILE": "production", "CASAN_AGENTIC_ENFORCEMENT_MODE": "enforce"}
with patch.dict(os.environ, env, clear=False):
begin = self.begin()
finalized = bridge.op_finalize({"op": "finalize", "admission_id": begin["admission_id"], "stop_reason": "completed"})
self.assertEqual(finalized["decision"], "non_certified")
self.assertIn("external_signing_trust_root_required", finalized["reason"])
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
# Proves that the release artifact—not the source checkout—contains a complete,
# offline, read-only Local Assurance Viewer.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
WORK="$(mktemp -d)"
DIST="$WORK/dist"
PROJECT="$WORK/clean-project"
VIEWER_STARTED=0
cleanup() {
if [[ "$VIEWER_STARTED" == "1" && -n "${CASAN_BIN:-}" ]]; then
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard stop >/dev/null 2>&1 || true
fi
rm -rf "$WORK"
}
trap cleanup EXIT
CASAN_DIST_DIR="$DIST" bash "$ROOT/scripts/package-release.sh" core >/dev/null
ARTIFACT="$DIST/casan-core-v$(cat "$ROOT/VERSION").tar.gz"
tar -xzf "$ARTIFACT" -C "$WORK"
BUNDLE="$WORK/casan-core-v$(cat "$ROOT/VERSION")"
CASAN_BIN="$BUNDLE/bin/casan"
test -f "$BUNDLE/packages/casan-harness/assets/local-viewer/index.html"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_report.py"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_viewer.py"
test -f "$BUNDLE/packages/casan-harness/scripts/python/readiness.py"
test ! -e "$BUNDLE/packages/casan-control-panel"
test ! -e "$BUNDLE/package.json"
mkdir -p \
"$PROJECT/.casan" \
"$PROJECT/.specify/state" \
"$PROJECT/.specify/logs/trace" \
"$PROJECT/.specify/logs/trace-events" \
"$PROJECT/.specify/logs/cost"
python3 - "$PROJECT" "$BUNDLE" <<'PY'
import json
from pathlib import Path
import sys
root = Path(sys.argv[1])
bundle = Path(sys.argv[2])
(root / ".casan/config.json").write_text(json.dumps({
"schema_version": "21.3",
"project_id": "artifact-project",
"project_name": "Artifact Project",
"edition": "core",
"enforcement_mode": "enforce",
"runtime_path": str(bundle),
"clients": ["vscode-copilot"],
}), encoding="utf-8")
(root / ".casan/casan-hook.py").write_text("# casan-hook.py\n", encoding="utf-8")
(root / ".casan/version.lock").write_text(json.dumps({
"harness_version": "artifact",
"harness_hash": "artifact-hash",
}), encoding="utf-8")
trace_id = "artifact-run-1"
trace = {
"schema_version": "20.1",
"trace_id": trace_id,
"project_id": "artifact-project",
"certified": True,
"certification_strength": "project_hook",
"finalized_at": "2026-07-28T10:00:07Z",
"tool_calls": 1,
"failures": 0,
"evidence": [
{"h": f"H{i}", "at": f"2026-07-28T10:00:0{i}Z", "decision": "pass", "kind": "gate", "detail": f"H{i} passed"}
for i in range(1, 8)
],
}
(root / f".specify/logs/trace/agentic-{trace_id}.json").write_text(
json.dumps(trace), encoding="utf-8")
metric = {
"timestamp": "2026-07-28T10:00:06Z",
"trace_id": trace_id,
"project_id": "artifact-project",
"harness": "H6-agentic",
"step": "agentic-turn",
"status": "success",
"latency_ms": 812,
"retry_count": 0,
"input_tokens": None,
"output_tokens": None,
"total_tokens": None,
"cost_estimate": None,
"telemetry_quality": "insufficient",
"alerts": [],
}
(root / ".specify/logs/cost/metrics.jsonl").write_text(
json.dumps(metric) + "\n", encoding="utf-8")
(root / ".specify/state/latest-run.json").write_text(json.dumps({
"schema_version": 1,
"trace_id": trace_id,
"project_id": "artifact-project",
"certified": True,
"telemetry_quality": "insufficient",
"duration_ms": 812,
"tool_calls": 1,
"failures": 0,
"finalized_at": "2026-07-28T10:00:07Z",
}), encoding="utf-8")
PY
(
cd "$PROJECT"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" readiness --refresh --json > "$WORK/readiness.json"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report latest | grep -q "view_command=casan view artifact-run-1"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export artifact-run-1 \
--format html --output "$WORK/run.html" >/dev/null
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export --h6 \
--format json --output "$WORK/h6.json" >/dev/null
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export --h6 \
--format html --output "$WORK/h6.html" >/dev/null
)
grep -q "Independent assurance dossier" "$WORK/run.html"
grep -q "H6 · AgentOps Report" "$WORK/h6.html"
python3 - "$WORK/h6.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["harness"] == "H6"
assert d["summary"]["runs"] == 1
assert d["summary"]["tokens"]["total"] is None
assert d["summary"]["cost_usd"]["estimated"] is None
PY
python3 - "$WORK/readiness.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["dimensions"]["core"]["status"] == "ready"
assert d["dimensions"]["domain_pipeline"]["status"] == "not_configured"
assert d["dimensions"]["provider_telemetry"]["status"] == "optional_unavailable"
PY
URL="$(CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" view artifact-run-1 --no-open)"
VIEWER_STARTED=1
python3 - "$URL" <<'PY'
import json
import sys
from urllib.error import HTTPError
from urllib.parse import parse_qs, urlparse
from urllib.request import Request, urlopen
url = urlparse(sys.argv[1])
token = parse_qs(url.query)["token"][0]
base = f"{url.scheme}://{url.netloc}"
def get(path):
request = Request(base + path, headers={"X-CASAN-Viewer-Token": token})
return json.loads(urlopen(request, timeout=3).read())
project = get("/api/v1/project")
assert project["project_id"] == "artifact-project"
assert project["viewer"] == {
"mode": "local_core",
"scope": "single_project",
"read_only": True,
"network": "loopback_only",
}
run = get("/api/v1/reports/run/artifact-run-1")
assert run["verdict"] == "certified"
assert run["summary"]["gates_observed"] == 7
h6 = get("/api/v1/reports/h6?run=artifact-run-1")
assert h6["summary"]["runs"] == 1
try:
urlopen(Request(base + "/api/v1/project"), timeout=3)
raise AssertionError("API accepted a request without the session token")
except HTTPError as error:
assert error.code == 401
try:
urlopen(Request(
base + "/api/v1/project",
method="POST",
headers={"X-CASAN-Viewer-Token": token},
), timeout=3)
raise AssertionError("read-only viewer accepted POST")
except HTTPError as error:
assert error.code == 405
try:
urlopen(Request(
base + "/api/v1/project",
headers={"X-CASAN-Viewer-Token": token, "Host": "attacker.example"},
), timeout=3)
raise AssertionError("viewer accepted a non-loopback Host header")
except HTTPError as error:
assert error.code == 403
PY
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard status | grep -q '"mode": "local_core"'
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard stop | grep -q "CASAN_VIEW_STOPPED"
VIEWER_STARTED=0
echo "PASS: Core release artifact provides offline visual reports without Platform or Node"
@@ -0,0 +1,8 @@
# NEHOPS analyzer invariants. These expressions are project/domain policy,
# while generic severity-tag preservation belongs to the Core compressor.
VBSource root not found
common code tracing skipped
Error Messages
Output Directory
Generated [0-9]+ files
exit code
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""Acceptance tests for the stdlib-only CASAN Core reporting boundary."""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
MODULE = Path(__file__).resolve().parents[1] / "scripts" / "python" / "local_report.py"
APP_JS = Path(__file__).resolve().parents[1] / "assets" / "local-viewer" / "app.js"
SPEC = importlib.util.spec_from_file_location("casan_local_report", MODULE)
REPORT = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(REPORT)
class LocalReportTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
(self.root / ".casan").mkdir()
(self.root / ".casan" / "config.json").write_text(json.dumps({
"project_id": "project-one",
"project_name": "Project One",
"edition": "core",
"enforcement_mode": "enforce",
"maturity": {"level": 4, "status": "assessed"},
}), encoding="utf-8")
(self.root / ".casan" / "readiness.json").write_text(json.dumps({
"schema_version": 1,
"overall_status": "ready",
"dimensions": {
"core": {"status": "ready", "acceptance": []},
"domain_pipeline": {"status": "not_configured"},
"provider_telemetry": {"status": "optional_unavailable"},
},
"next_actions": [],
}), encoding="utf-8")
for path in (
".specify/state",
".specify/logs/trace",
".specify/logs/trace-events",
".specify/logs/cost",
".specify/logs/level5",
".specify/agentops",
):
(self.root / path).mkdir(parents=True, exist_ok=True)
def tearDown(self):
self.temporary.cleanup()
def write_jsonl(self, relative, rows):
path = self.root / relative
path.write_text(
"".join(json.dumps(row, separators=(",", ":")) + "\n" for row in rows),
encoding="utf-8",
)
def write_run(self, trace_id="trace-1", quality="partial"):
trace = {
"trace_id": trace_id,
"project_id": "project-one",
"certified": True,
"certification_strength": "project_hook",
"finalized_at": "2026-07-28T10:00:07Z",
"tool_calls": 2,
"failures": 0,
"evidence": [
{"h": f"H{index}", "at": f"2026-07-28T10:00:0{index}Z", "decision": "pass", "kind": "gate", "detail": f"H{index} passed"}
for index in range(1, 8)
],
}
(self.root / f".specify/logs/trace/agentic-{trace_id}.json").write_text(
json.dumps(trace), encoding="utf-8")
metric = {
"timestamp": "2026-07-28T10:00:06Z",
"trace_id": trace_id,
"project_id": "project-one",
"step": "agentic-turn",
"status": "success",
"latency_ms": 1200,
"retry_count": 0,
"input_tokens": None,
"output_tokens": None,
"total_tokens": None,
"cost_estimate": None,
"telemetry_quality": quality,
"alerts": [],
}
self.write_jsonl(".specify/logs/cost/metrics.jsonl", [metric])
return metric
def test_core_empty_state_is_truthful(self):
registry = REPORT.runs(self.root)
h6 = REPORT.h6_report(self.root)
self.assertEqual(registry["count"], 0)
self.assertEqual(h6["verdict"], "no_data")
self.assertEqual(h6["summary"]["runs"], 0)
self.assertIsNone(h6["summary"]["tokens"]["total"])
self.assertTrue(any(item["code"] == "METRICS_MISSING" for item in h6["findings"]))
profile = REPORT.project_profile(self.root)
self.assertEqual(profile["readiness"]["dimensions"]["core"]["status"], "ready")
def test_run_reconstructs_all_gates_and_redacts_sensitive_fields(self):
self.write_run()
event = {
"timestamp": "2026-07-28T10:00:01Z",
"trace_id": "trace-1",
"gate_id": "H1-context",
"status": "pass",
"reason": "admitted",
"evidence": {"kind": "context", "prompt": "private customer prompt", "access_token": "secret"},
}
self.write_jsonl(".specify/logs/trace-events/trace-1.jsonl", [event])
report = REPORT.run_report(self.root, "trace-1")
self.assertEqual(report["verdict"], "certified")
self.assertEqual(report["summary"]["gates_observed"], 1)
self.assertEqual(report["gates"][0]["evidence"]["prompt"], "[redacted]")
self.assertEqual(report["gates"][0]["evidence"]["access_token"], "[redacted]")
self.assertIsNone(report["h6"]["total_tokens"])
def test_legacy_trace_reconstructs_h1_to_h7(self):
self.write_run()
graph = REPORT.trace_graph(self.root, "trace-1")
self.assertTrue(graph["terminal"])
self.assertEqual(graph["progress"], 7)
self.assertEqual([node["status"] for node in graph["nodes"]], [
"pass", "pass", "pass", "pass", "pass", "warning", "pass",
])
def test_h6_preserves_unknown_token_and_cost_as_null(self):
self.write_run()
report = REPORT.h6_report(self.root, run="trace-1")
self.assertEqual(report["summary"]["coverage"]["token_pct"], 0)
self.assertEqual(report["summary"]["coverage"]["cost_pct"], 0)
self.assertIsNone(report["summary"]["tokens"]["total"])
self.assertIsNone(report["summary"]["cost_usd"]["estimated"])
self.assertEqual(report["data_quality"]["status"], "insufficient")
self.assertIn("TELEMETRY_COVERAGE_GAP", report["verdict_reasons"])
def test_self_contained_html_escapes_evidence(self):
self.write_run()
report = REPORT.run_report(self.root, "trace-1")
report["gates"][0]["reason"] = "<script>alert(1)</script>"
html = REPORT.report_html(report, "run")
self.assertIn("<!doctype html>", html.lower())
self.assertNotIn("<script>alert(1)</script>", html)
self.assertIn("&lt;script&gt;alert(1)&lt;/script&gt;", html)
self.assertIn("Machine-auditable contract", html)
def test_h6_html_visualizes_outcomes_and_hides_raw_contract(self):
self.write_run()
report = REPORT.h6_report(self.root, run="trace-1")
html = REPORT.report_html(report, "h6")
self.assertIn("Run distribution", html)
self.assertIn('class="distribution"', html)
self.assertIn("<details><summary>Open JSON evidence</summary>", html)
self.assertNotIn("None</strong>", html)
def test_unsafe_trace_id_never_reads_outside_project(self):
report = REPORT.run_report(self.root, "../../etc/passwd")
self.assertEqual(report["verdict"], "not_found")
self.assertFalse(report["source"]["trace_found"])
def test_trace_inspection_reveals_the_loaded_dossier(self):
script = APP_JS.read_text(encoding="utf-8")
self.assertIn('id="run-dossier"', script)
self.assertIn('tabindex="-1"', script)
self.assertIn('dossier.focus({ preventScroll: true })', script)
self.assertIn("dossier.scrollIntoView({", script)
self.assertIn("No operational KPIs are shown until evidence exists.", script)
self.assertIn("H6 begins with the first governed run.", script)
self.assertIn("No operational counters yet", script)
self.assertNotIn('"Ready for the first governed run."', script)
if __name__ == "__main__":
unittest.main()
@@ -40,6 +40,8 @@ print(d.get(sys.argv[1],""))'
echo "===== C1: normal turn = one admission + one trace + one metric (single model) ====="
newstate
export CASAN_AGENTIC_ENFORCEMENT_MODE=enforce
export CASAN_AGENT=boss
export CASAN_ACTOR=boss
B=$(bridge '{"op":"begin","client":"claude-code","project":"'"$PROJ"'","session":"c1","prompt":"add a helper","integration_mode":"project_hook"}')
DEC=$(printf '%s' "$B" | field decision)
AID=$(printf '%s' "$B" | field admission_id)
@@ -76,7 +78,8 @@ B=$(bridge '{"op":"begin","client":"claude-code","project":"'"$PROJ"'","session"
AID=$(printf '%s' "$B" | field admission_id); TID=$(printf '%s' "$B" | field trace_id)
OK3=1
for tool in Bash Edit Write; do
R=$(bridge '{"op":"pre-tool","admission_id":"'"$AID"'","tool":"'"$tool"'","tool_input":"x","project":"'"$PROJ"'"}')
INPUT_VALUE="x"; [[ "$tool" == "Bash" ]] && INPUT_VALUE="ls"
R=$(bridge '{"op":"pre-tool","admission_id":"'"$AID"'","tool":"'"$tool"'","tool_input":"'"$INPUT_VALUE"'","project":"'"$PROJ"'"}')
[[ "$(printf '%s' "$R" | field decision)" == "allow" ]] || OK3=0
bridge '{"op":"post-tool","admission_id":"'"$AID"'","tool":"'"$tool"'","status":"success"}' >/dev/null
done
@@ -245,7 +248,7 @@ AID=$(printf '%s' "$B" | field admission_id)
|| fail "no-bash begin did not degrade gracefully ($B)"
R=$(bridge '{"op":"pre-tool","admission_id":"'"$AID"'","tool":"Bash","tool_input":"ls","project":"'"$PROJ"'"}')
[[ "$(printf '%s' "$R" | field decision)" == "allow" ]] \
&& pass "no-bash: side-effect tool allowed (admission gate still governs)" \
&& pass "no-bash: classified read-only shell command remains available" \
|| fail "no-bash pre-tool blocked the developer ($R)"
F=$(bridge '{"op":"finalize","admission_id":"'"$AID"'","stop_reason":"completed"}')
[[ "$(printf '%s' "$F" | field decision)" == "non_certified" ]] \

Some files were not shown because too many files have changed in this diff Show More