feat: prepare CASAN paid PoC release package

This commit is contained in:
thanhnv
2026-07-18 00:07:04 +07:00
parent c818eaf8b0
commit 881ee01691
55 changed files with 5992 additions and 44 deletions
+1
View File
@@ -15,6 +15,7 @@ jobs:
CASAN_CI_RUN_FRONTEND: "1" CASAN_CI_RUN_FRONTEND: "1"
CASAN_CI_RUN_BACKEND: "1" CASAN_CI_RUN_BACKEND: "1"
CASAN_CI_RUN_INFRA_LAB: "0" CASAN_CI_RUN_INFRA_LAB: "0"
CASAN_CI_RUN_CONTROL_PANEL: "1"
CASAN_REQUIRE_LIVE_SCANNERS: "1" CASAN_REQUIRE_LIVE_SCANNERS: "1"
CASAN_TOOLCHAIN_TRUSTED_DIRS: /usr/bin:/bin:/usr/local/bin CASAN_TOOLCHAIN_TRUSTED_DIRS: /usr/bin:/bin:/usr/local/bin
steps: steps:
+12
View File
@@ -18,6 +18,7 @@ jobs:
env: env:
CASAN_CI_RUN_FRONTEND: "0" CASAN_CI_RUN_FRONTEND: "0"
CASAN_CI_RUN_BACKEND: "0" CASAN_CI_RUN_BACKEND: "0"
CASAN_CI_RUN_CONTROL_PANEL: "1"
CASAN_CI_RUN_INFRA_LAB: "0" CASAN_CI_RUN_INFRA_LAB: "0"
CASAN_CI_STEP_TIMEOUT_SEC: "1200" CASAN_CI_STEP_TIMEOUT_SEC: "1200"
steps: steps:
@@ -39,6 +40,9 @@ jobs:
echo "tag=$TAG version-file=$FILE_VER" echo "tag=$TAG version-file=$FILE_VER"
[ "$TAG" = "$FILE_VER" ] || { echo "::error::tag v$TAG != VERSION $FILE_VER — bump VERSION before tagging"; exit 1; } [ "$TAG" = "$FILE_VER" ] || { echo "::error::tag v$TAG != VERSION $FILE_VER — bump VERSION before tagging"; exit 1; }
- name: Install locked Node dependencies
run: npm ci
- name: Verify governance gate (must be green to release) - name: Verify governance gate (must be green to release)
run: bash packages/casan-harness/scripts/bash/ci-harness-gate.sh run: bash packages/casan-harness/scripts/bash/ci-harness-gate.sh
@@ -52,6 +56,14 @@ jobs:
scripts/package-release.sh enterprise || echo "enterprise skipped (future, expected)" scripts/package-release.sh enterprise || echo "enterprise skipped (future, expected)"
ls -lh dist/ ls -lh dist/
- name: Smoke extracted release artifacts
run: |
set -euo pipefail
scripts/smoke-release-artifact.sh "dist/casan-core-v$(cat VERSION).tar.gz"
scripts/smoke-release-artifact.sh "dist/casan-devkit-v$(cat VERSION).tar.gz"
CASAN_ARTIFACT_SMOKE_RUN_TESTS=1 \
scripts/smoke-release-artifact.sh "dist/casan-platform-preview-v$(cat VERSION).tar.gz"
- name: Publish bundles to Gitea package registry - name: Publish bundles to Gitea package registry
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+12
View File
@@ -0,0 +1,12 @@
CASAN Proprietary Commercial License
Copyright (c) CASAN copyright owner. All rights reserved.
This repository and its CASAN-authored source code, documentation, scripts,
and release artifacts are proprietary. No permission is granted to use, copy,
modify, distribute, sublicense, host, or create derivative works except under
an executed written agreement with the copyright owner.
Third-party components remain subject to their own licenses. See
docs/commercial/THIRD_PARTY_SOFTWARE.json and
docs/commercial/LICENSE_AND_IP_DECISION.md.
+1
View File
@@ -2,6 +2,7 @@
"name": "@ainative-okr/backend", "name": "@ainative-okr/backend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"license": "UNLICENSED",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "prisma generate && tsc -p tsconfig.build.json", "build": "prisma generate && tsc -p tsconfig.build.json",
+1
View File
@@ -2,6 +2,7 @@
"name": "@ainative-okr/frontend", "name": "@ainative-okr/frontend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"license": "UNLICENSED",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -0,0 +1,44 @@
# CASAN Commercial License and IP Decision
## Decision
CASAN is offered as **proprietary commercial software**. No rights to use,
copy, modify, distribute, host, or sublicense CASAN are granted except under a
written evaluation, PoC, subscription, or enterprise agreement signed by the
copyright owner and the customer.
For a paid PoC, the default grant is a non-exclusive, non-transferable,
time-limited right to deploy the delivered CASAN bundle in the customer’s
approved environment for the agreed evaluation scope. Production use, managed
service operation, redistribution, and source access require separate terms.
This decision applies to CASAN-authored material only. It does not override
third-party license obligations.
## Release controls
1. Regenerate `THIRD_PARTY_SOFTWARE.json` from the committed lockfile before
each paid-PoC or release candidate: `node scripts/generate-license-inventory.mjs`.
2. Review every `UNKNOWN`, copyleft, source-available, model, dataset, font,
icon, container-base-image, or copied-script license before shipment.
3. Include required notices and source-offer material in the release bundle
whenever a dependency license requires it.
4. Record the review owner, release version, decision, and exceptions in the
release Evidence Pack or legal review record.
5. Do not claim ownership of customer source code, prompts, or Evidence Pack
data; ownership and permitted processing are defined by the customer
agreement and data-processing terms.
## Known scope boundary
The generated inventory is complete for root-lockfile npm packages only. It is
not proof that Python tooling, operating-system packages, Docker base images,
models, datasets, or bundled assets are cleared. Those sources are release
blockers until separately inventoried and approved.
## Required legal review before external sale
Confirm copyright ownership/assignment for CASAN-authored code, trademark use,
customer contract templates, export-control applicability, open-source notices,
and country-specific privacy terms. This document records the product decision;
it is not a substitute for counsel.
+16
View File
@@ -0,0 +1,16 @@
{
"schema_version": "1.0",
"purpose": "Manual license-review findings for packages whose committed lockfile does not contain license metadata.",
"exceptions": {
"busboy@1.6.0": {
"reviewed_license": "MIT",
"evidence": "https://github.com/mscdex/busboy/blob/master/LICENSE",
"review_status": "requires-release-notice"
},
"streamsearch@1.1.0": {
"reviewed_license": "MIT",
"evidence": "https://github.com/mscdex/streamsearch/blob/master/LICENSE",
"review_status": "requires-release-notice"
}
}
}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
# 製品概要
## 提供価値
CASAN は、既存の AI コーディングツールを置き換える製品ではありません。
開発者、AI モデル、リポジトリ、CI の間に配置される独立した統制レイヤーです。
AI が生成・提案した変更を、ポリシー、テスト、セキュリティ検査、承認、監査証跡で確認し、根拠を Evidence Pack として残します。
## PoC の対象ユースケース
「Claude Code、Codex、Copilot 等を利用する開発チームで、AI が関与した変更を
merge 前に検査し、誰が何を行ったかを証明する。」
PoC では、1〜2 プロジェクト、1 つの Git リポジトリ連携、1 つの Domain Pack を対象とします。
## 提供範囲(CASAN Core + Control Center Lite)
- H1〜H7 のハーネス実行、アクションゲート、セキュリティ検査
- Gitea CI からの検査起動と PASS/BLOCK 判定
- Evidence Pack の作成、ハッシュ manifest の検証、Control Panel での閲覧
- 監査ログ、ツール呼出記録、コスト/トークンの可視化
- 承認が必要な変更を apply 前に停止するワークフロー
## PoC では提供しないもの
- 高可用性、RPO/RTO を伴う本番運用
- 本番用 SSO/OIDC、顧客 CA、マネージド運用
- KMS 必須運用、S3 Object Lock、外部ペネトレーションテスト
- GitHub/GitLab/Jenkins の本番コネクタ保証
- 自律エージェントによる無承認の変更適用
これらは Enterprise Pilot / Production の後続条件です。
@@ -0,0 +1,30 @@
# システム構成・データフロー
## 推奨配置
PoC では顧客管理のネットワーク内、または顧客クラウドアカウント内への self-hosted 配置を推奨します。
```text
Developer / AI coding tool
│ patch / PR
▼
Gitea ──► CASAN Runner ──► test / security / policy gates
│
├── Evidence Pack(manifest + reports)
└── Control Panel(run、承認、証跡表示)
```
## データフロー
1. 開発者が既存の AI ツールで変更を作成し、PR を作成します。
2. Gitea workflow が CASAN を呼び出します。
3. CASAN は許可されたワークスペースでポリシー、テスト、セキュリティを実行します。
4. 判定、実行記録、ハッシュを Evidence Pack に保存します。
5. Control Panel は Evidence Pack の manifest を再検証して表示します。
6. PASS でない変更は、顧客定義のマージ/承認ルールに従って停止します。
## データの扱い
ソースコード、ログ、Evidence Pack は顧客環境内に保存する構成を基本とします。
クラウドモデルへ送信するデータは、顧客が選択したモデル設定とポリシーに依存します。
PoC 開始時に、送信対象、保存場所、保持期間、マスキング方針を Data Flow Sheet に記録します。
@@ -0,0 +1,28 @@
# 導入・運用・制限事項
## 導入手順(PoC)
1. 顧客と対象リポジトリ、担当者、承認ルールを確定します。
2. CASAN bundle を顧客環境へ配置し、Docker Compose または提供手順で起動します。
3. Gitea workflow に CASAN gate を接続します。
4. Domain Pack と閾値を対象プロジェクトに合わせます。
5. テスト PR を用いて PASS/BLOCK/Evidence Pack を確認します。
6. 運用開始後、週次で KPI と誤検知をレビューします。
## 日常運用
- 開発者: PR の CASAN verdict と必要な修正を確認します。
- QA/セキュリティ: ブロック理由、Evidence Pack、例外承認を確認します。
- 管理者: ポリシー変更、ロール、バックアップ、障害連絡を管理します。
## 既知の制限
- Evidence Pack の署名は、PoC 環境では鍵未設定の場合に unsigned となります。
- テレメトリ完全性未検証またはコストスパイクがある場合、pack は certified になりません。
- 実際の隔離強度は配置する runner/container 設定に依存します。Production 用 hardened sandbox は PoC の範囲外です。
- Enterprise SSO、HA、DR、外部監査は PoC 完了条件には含みません。
## 障害時
モデル、runner、検査、または証跡生成が失敗した場合、CASAN は PASS を生成せず、失敗理由を記録します。
復旧判断とマージ例外の承認は顧客の責任者が行います。
+29
View File
@@ -0,0 +1,29 @@
# PoC計画(8〜12週間)
## 成功条件
- 対象リポジトリで CASAN gate が PR ごとに実行されること
- PASS/BLOCK の理由と Evidence Pack を担当者が確認できること
- AI が関与した変更のテスト/セキュリティ/承認記録を追跡できること
- 顧客と合意した KPI を最終報告書で提示できること
## 進め方
| 期間 | 内容 | 成果物 |
|---|---|---|
| 1〜2週 | キックオフ、対象選定、リスク確認 | PoC 設計、Data Flow Sheet |
| 3〜4週 | 環境配置、Gitea 連携、Domain Pack | 導入記録、テスト PR |
| 5〜8週 | 実運用、閾値調整、Evidence 確認 | 週次 KPI、課題一覧 |
| 9〜12週 | 最終評価、次フェーズ判断 | PoC 結果報告、Pilot 提案 |
## KPI(開始時に基準値を合意)
- CASAN が実行された PR 比率
- ブロックされたセキュリティ/ポリシー違反数
- Evidence Pack が完全性検証に成功した比率
- 誤検知レビュー件数と解決時間
- AI 関連の変更に対する承認・追跡可能比率
## 顧客に必要な協力
対象リポジトリ、テスト環境、Gitea 管理者、セキュリティ/QA 窓口、承認ルール、及びクラウドモデル利用時のデータ送信判断を提供してください。
@@ -0,0 +1,26 @@
# セキュリティ・責任分界
## CASAN が提供する統制
- AI 出力/ツール実行に対するポリシーとセキュリティ検査
- 実行・判定・承認の監査記録
- Evidence Pack の SHA-256 manifest 検証
- 失敗時に PASS を出さない fail-closed 動作
## 顧客の責任
- ネットワーク、ホスト、ID、リポジトリ権限、バックアップの管理
- モデル provider と送信データの選定
- 個人情報、秘密情報、保持期間、例外承認の決定
- PoC 環境のアクセス権とログ閲覧権の管理
## 提供者の責任
- 合意した CASAN bundle、導入支援、障害一次切り分けを提供すること
- 既知の制限、重大な脆弱性、PoC スコープ外の事項を明示すること
- 顧客データを合意外の目的に使用しないこと
## 事前確認事項
APPI を含む適用法令、個人情報の有無、海外移転、subprocessor、データ削除/export、インシデント通知窓口は契約・Data Flow Sheet で合意します。
この資料は法的助言ではありません。
+19
View File
@@ -0,0 +1,19 @@
# SLA / サポート条件ドラフト(PoC)
## 適用範囲
本ドラフトは PoC 期間の導入支援と障害対応の目安です。Production SLA ではありません。
| 区分 | 例 | 初回応答目標 |
|---|---|---|
| P1 | CASAN が全 PR で実行不能、証跡が作成不能 | 営業時間内 4 時間 |
| P2 | 一部 gate/表示機能の不具合、回避策あり | 営業時間内 1 営業日 |
| P3 | 質問、設定調整、改善要望 | 3 営業日以内 |
## 連絡とエスカレーション
顧客の PoC 責任者と提供者の技術窓口をキックオフ時に指定します。P1 の場合、run ID、発生時刻、影響範囲、表示された理由を添えて連絡します。
## 除外
顧客ネットワーク、顧客提供モデル、Gitea 自体、外部クラウド障害、未承認の構成変更は本ドラフトの保証対象外です。
+16
View File
@@ -0,0 +1,16 @@
# CASAN Japanese PoC Pack(最小版)
このパックは、有償 PoC の開始前に顧客へ提示する最小限の説明資料です。
実装済みの機能と未提供の機能を分け、未検証のエンタープライズ機能を約束しません。
| 資料 | 用途 |
|---|---|
| [01_製品概要](01_製品概要.md) | 課題、提供価値、対象範囲 |
| [02_システム構成・データフロー](02_システム構成・データフロー.md) | 配置、データ境界、証跡フロー |
| [03_導入・運用・制限事項](03_導入・運用・制限事項.md) | 導入手順、運用、既知の制限 |
| [04_PoC計画](04_PoC計画.md) | 8〜12 週間の評価計画と KPI |
| [05_セキュリティ・責任分界](05_セキュリティ・責任分界.md) | セキュリティ仕様と顧客・提供者の責任 |
| [06_SLAドラフト](06_SLAドラフト.md) | PoC 中の支援条件(ドラフト) |
製品名(提案): **CASAN AI-SDLC Governance Platform**
(生成AIによるソフトウェア開発を、セキュリティ・品質・コスト・監査証跡の観点から統制するプラットフォーム)
@@ -0,0 +1,17 @@
{
"files": {
"benign-fp-report.json": "52d3911e7b539f07391e187e3212f198f25e55d08396e2ab0e75d9db3178848b",
"decision-log.md": "6975cabd29baff1fbc1c9ceca8a44323832ecc28eafe6bdd30fbe94981563d70",
"h1-context-report.json": "64a1bbcdf38672eb1efcb8742e258850709297bd848a443fcf11b94b0f165d9d",
"h2-tool-audit.json": "095b369ae21d47ecfcbc38387f3255118bc32a4c28327f19b27e0bfcd7c945ae",
"h3-eval-scorecard.json": "b0e779fb219c4d9d38e55eefb15cd4370b4f0760e0a3bf70b5d22923407c8e47",
"h4-security-report.json": "2828cf13b2f4edba852c9da77e1397b3b44933a3bd6d04c81b4ac96e31026e9d",
"h5-audit-chain-proof.json": "f9918cc45311097be05ab852c36ed99170c65962f8c0f0e71b91d7468f9f2ba4",
"h6-cost-telemetry.json": "e01cc551016f3d4b7d525e11cf9556481cb1376408daca6e2314aba9a3ddd088",
"h7-orchestration-report.json": "9dd45a16004bd705c7307fae6c74975049a156a3dc1e14fc439ad816d5400066",
"redteam-result.json": "341f7985a8cceb2cb3cc7705e644a4407ab27f7203c148abb98788dcca5e5c8e",
"run-summary.json": "16cf96c55fa7b899e7b021b52a88cf3fa160366ab03e2361e431b0639196af29",
"traceability-matrix.json": "011ac8ac6ab008f1048e3bce1e3f7b5b28bc969de7a1574fed86af16e259de27"
},
"manifest_head": "c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47"
}
@@ -0,0 +1,41 @@
{
"generated": "deterministic (semantic OFF)",
"policy": {
"fp_max_pct": 3.0,
"adv_block_min_pct": 95.0,
"critical_block_pct": 100.0
},
"benign": {
"by_lang": {
"en": {
"total": 32,
"false_positives": 0
},
"vi": {
"total": 32,
"false_positives": 0
},
"ja": {
"total": 31,
"false_positives": 0
}
},
"total": 95,
"false_positives": 0,
"false_positive_rate_pct": 0.0,
"examples": []
},
"adversarial": {
"total": 12,
"blocked": 12,
"block_rate_pct": 100.0,
"missed": []
},
"critical": {
"total": 4,
"blocked": 4,
"block_rate_pct": 100.0
},
"within_budget": true,
"breaches": []
}
@@ -0,0 +1,17 @@
# CASAN Evidence Pack — Decision Log
Run: `9534d91f-4518-41c5-a7c4-56e22a56c2a4`
Certified: **False**
Reasons: telemetry_integrity_not_verified, unresolved_cost_spike
## Gate outcomes
- H4 security: 186 records, 38 blocked
- H5 audit chain: AUDIT_CHAIN_VALID anchor=signed last_hash=e715767a0d5ec67a8955c0e8e87262b5ca8c0f5826f1dee6583014ede8032bbe (ok=True)
- H5 telemetry integrity: none (ok=False)
- H6 cost: spike_detected, 56519 provider tokens
- H2 tool audit: 24 records, chain_ok=True
- H3 traceability: ok=True
- Red-team: 16 vectors (block_rate=100.0%)
_Summaries only — no raw secret/PII content is copied into the pack._
@@ -0,0 +1,6 @@
{
"harness": "H1-context",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"context_yaml": true,
"note": "path/artifact validation performed by context-validate.sh at run time"
}
@@ -0,0 +1,9 @@
{
"harness": "H2-tool",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"records": 24,
"denied": 6,
"approved": 5,
"chain_status": "TOOL_AUDIT_VALID anchor=signed last_hash=dda5685f038e171c7fdd8bb36b00793c3de7b14be7295fdfe34fff5e6a03c443",
"chain_ok": true
}
@@ -0,0 +1,6 @@
{
"harness": "H3-eval",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"judge_gate_tests": true,
"note": "judge-gate fail-before/fix cycle proven by phase3-judge-gate-tests.sh"
}
@@ -0,0 +1,10 @@
{
"harness": "H4-security",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"records": 186,
"by_status": {
"blocked": 38,
"pass": 148
},
"blocked": 38
}
@@ -0,0 +1,8 @@
{
"harness": "H5-governance",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"audit_chain": "AUDIT_CHAIN_VALID anchor=signed last_hash=e715767a0d5ec67a8955c0e8e87262b5ca8c0f5826f1dee6583014ede8032bbe",
"audit_chain_ok": true,
"telemetry_integrity": "none",
"telemetry_ok": false
}
@@ -0,0 +1,9 @@
{
"harness": "H6-agentops",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"provider_records": 56,
"metric_records": 40,
"total_provider_tokens": 56519,
"cost_spike_rc": 2,
"cost_spike_status": "spike_detected"
}
@@ -0,0 +1,6 @@
{
"harness": "H7-orchestration",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"rollback_log": true,
"note": "rollback/fallback/drift proven by adversarial + run-casan4 suites"
}
@@ -0,0 +1 @@
c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47
@@ -0,0 +1,8 @@
{
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"vectors_defined": 16,
"critical_vectors": 4,
"adversarial_block_rate_pct": 100.0,
"critical_block_rate_pct": 100.0,
"note": "block rates from benign-fp-report (deterministic layer); full suites: phase1-track-a + phase2-track-c"
}
@@ -0,0 +1,28 @@
{
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"pack_version": "1.0-mvp",
"certified": false,
"certification_reasons": [
"telemetry_integrity_not_verified",
"unresolved_cost_spike"
],
"required_gates": [
"H3-traceability",
"H4-security",
"H5-audit-chain",
"H5-telemetry",
"H6-cost",
"benign-fp-budget"
],
"harness_reports": [
"benign-fp-report.json",
"h1-context-report.json",
"h2-tool-audit.json",
"h3-eval-scorecard.json",
"h4-security-report.json",
"h5-audit-chain-proof.json",
"h6-cost-telemetry.json",
"h7-orchestration-report.json",
"redteam-result.json"
]
}
@@ -0,0 +1,168 @@
{
"generated_at": "2026-07-17T16:57:32Z",
"requirements_source": "apps/okr/domain/input/okr-requirement.md",
"mapping_source": "apps/okr/domain/traceability-map.json",
"summary": {
"requirements": 5,
"passed": 5,
"failed": 0,
"symbol_refs": 9,
"symbols_found": 9,
"symbols_missing": 0,
"orphan_mappings": []
},
"matrix": [
{
"id": "FR-01",
"name": "Login",
"status": "PASS",
"code": [
"apps/okr/backend/src/auth/auth.controller.ts",
"apps/okr/backend/src/auth/auth.service.ts",
"apps/okr/frontend/src/pages/Login.tsx",
"apps/okr/frontend/src/hooks/useAuth.tsx"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/auth/auth.service.ts",
"symbol": "AuthService",
"found": true
},
{
"file": "apps/okr/backend/src/auth/auth.service.ts",
"symbol": "login",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-02",
"name": "Create Objective",
"status": "PASS",
"code": [
"apps/okr/backend/src/objectives/objectives.controller.ts",
"apps/okr/backend/src/objectives/objectives.service.ts",
"apps/okr/frontend/src/pages/CreateObjective.tsx",
"apps/okr/frontend/src/schemas/objective.schema.ts"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts",
"apps/okr/frontend/src/__tests__/okr.test.tsx"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "ObjectivesService",
"found": true
},
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "create",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-03",
"name": "Create Key Result",
"status": "PASS",
"code": [
"apps/okr/backend/src/key-results/key-results.controller.ts",
"apps/okr/backend/src/key-results/key-results.service.ts",
"apps/okr/backend/src/key-results/dto/create-key-result.dto.ts"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
"symbol": "KeyResultsService",
"found": true
},
{
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
"symbol": "create",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-04",
"name": "Update Progress",
"status": "PASS",
"code": [
"apps/okr/backend/src/key-results/key-results.controller.ts",
"apps/okr/backend/src/key-results/key-results.service.ts",
"apps/okr/backend/src/key-results/dto/update-progress.dto.ts",
"apps/okr/frontend/src/pages/KeyResultDetail.tsx"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts",
"apps/okr/frontend/src/__tests__/okr.test.tsx"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
"symbol": "updateProgress",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-05",
"name": "Dashboard",
"status": "PASS",
"code": [
"apps/okr/backend/src/objectives/objectives.controller.ts",
"apps/okr/backend/src/objectives/objectives.service.ts",
"apps/okr/frontend/src/pages/Dashboard.tsx",
"apps/okr/frontend/src/hooks/useObjectives.ts"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts",
"apps/okr/frontend/src/__tests__/okr.test.tsx"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "ObjectivesService",
"found": true
},
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "list",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
}
]
}
+13 -7
View File
@@ -11,8 +11,8 @@ contents + maturity: [`packaging/levels.json`](../../packaging/levels.json).
|---|---|:--:|---| |---|---|:--:|---|
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | H1–H7 harness, security + action gates, evidence pack, audit, cost/telemetry, hardening tests, policy/config defaults, `bin/casan` CLI | | **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | H1–H7 harness, security + action gates, evidence pack, audit, cost/telemetry, hardening tests, policy/config defaults, `bin/casan` CLI |
| **2 — DevKit / Adoption Kit** | `casan-devkit`, `casan-project-kit` | ✅ implemented | Level 1 + project templates, domain-pack scaffold, Gitea workflow template, harness Dockerfile, install script, adoption/CI/domain-pack guides | | **2 — DevKit / Adoption Kit** | `casan-devkit`, `casan-project-kit` | ✅ implemented | Level 1 + project templates, domain-pack scaffold, Gitea workflow template, harness Dockerfile, install script, adoption/CI/domain-pack guides |
| **3 — Platform Components** | `casan-platform`, `casan-control-panel` | 🟡 preview | Control Panel, Dashboard, Evidence/Attack/Run-History viewers, read-only Ask CASAN, Gitea webhook. **AgentOps dashboard + Plan-13 Control Panel Track 1/2/3 + FinOps/SLO + Command Center + local-prod TLS/OIDC smoke exist today.** | | **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 | Governed Chat Console, Prompt Router, Model-Provider Mgmt, Operator/Codegen modes, Agent/Skill Registry, RBAC, approval, tenant isolation, KMS/WORM, policy versioning | | **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. |
Levels are cumulative: DevKit extends Core, Platform extends DevKit, Enterprise extends Levels are cumulative: DevKit extends Core, Platform extends DevKit, Enterprise extends
Platform. Platform.
@@ -60,8 +60,14 @@ docs/packaging/ # this plan + adoption/CI/domain-pack/gitea/docker g
## Implemented now vs future ## Implemented now vs future
- **Implemented now:** Level 1 Core packaging, Level 2 DevKit packaging, release script, - **Implemented now:** Level 1 Core packaging, Level 2 DevKit packaging, release script,
`bin/casan`, templates, docs, all-in-one-demo bundle. `bin/casan`, templates, docs, all-in-one-demo bundle.
- **Structure + docs only:** Level 3 Platform (dashboard exists; rest scaffolded), - **Platform preview:** a shipped preview bundle containing the Control Panel,
Level 4 Enterprise (RBAC/tenant/KMS/WORM/approval exist in core; governed console not built). governed-chat MVP, and Evidence Pack Viewer. It is explicitly incomplete.
- **Not built in this task (do not assume present):** Governed Chat Console, Prompt Mode - **Structure + docs only:** Level 4 Enterprise. RBAC/tenant/KMS/WORM/approval
Router, Model Provider Management, Operator/Codegen modes, Agent/Skill Registry, RBAC/ building blocks exist, but no Enterprise artifact is shipped.
tenant *console* UX. - **Not built in this task (do not assume present):** Attack Battery Viewer,
Gitea webhook evidence publishing, enterprise managed deployment, HA/DR/SLA,
external security assessment and compliance certification.
For customer claims and limitations, use
[`EDITION_FEATURE_LIMITATION_MATRIX.md`](EDITION_FEATURE_LIMITATION_MATRIX.md),
not this overview alone.
@@ -0,0 +1,40 @@
# CASAN Edition Feature and Limitation Matrix
This matrix is the customer-facing source of truth for edition claims. A check
means the capability is packaged and has repository evidence; it does not imply
an enterprise SLA, managed operation, or certification unless explicitly noted.
| Capability | Core | DevKit | Platform Preview | Enterprise |
|---|---:|---:|---:|---:|
| H1–H7 harness, policy/action gates | Included | Included | Included | Building blocks only |
| CLI and evidence pack creation/verification | Included | Included | Included | Building blocks only |
| Project/domain templates and CI adoption guides | — | Included | Included | — |
| Gitea CI gate template | — | Included | Included | — |
| Control Panel: runs, governance, security, cost, approvals | — | — | Included | Not a shipped edition |
| Evidence Pack Viewer | — | — | Included | Not a shipped edition |
| Governed chat/operator/codegen MVP | — | — | Included, preview | Not a shipped edition |
| Gitea webhook evidence publishing | — | — | Not yet available | Not available |
| Enterprise OIDC/CA/private-network deployment | — | — | Local smoke only | Not available |
| Vault/KMS enforced, immutable object storage | Harness building blocks | Harness building blocks | Not production packaged | Not available |
| HA, DR, RPO/RTO, production SLA | — | — | Not available | Not available |
| External pentest / ISO/SOC/ISMAP claim | — | — | Not available | Not available |
| Commercial license / support agreement | Required for paid use | Required for paid use | Required for paid use | Future contract only |
## 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.
- **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
`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.
## Claim guardrails
Do not claim “enterprise-ready”, “production HA”, “ISMAP compliant”, “APPI
compliant”, “immutable storage”, “signed evidence”, or “sandboxed codegen”
unless the corresponding deployment and verification evidence exists for that
customer environment.
+2 -1
View File
@@ -4,7 +4,7 @@
> bước tiếp theo cụ thể + cờ phụ-thuộc-hạ-tầng, để **bất kỳ AI/người nào tiếp quản > bước tiếp theo cụ thể + cờ phụ-thuộc-hạ-tầng, để **bất kỳ AI/người nào tiếp quản
> cũng làm tiếp được ngay**. Cập nhật mỗi khi hoàn thành một mục. > cũng làm tiếp được ngay**. Cập nhật mỗi khi hoàn thành một mục.
> >
> Cập nhật lần cuối: 2026-07-08 · Nhánh làm tiếp từ handoff Claude/Codex. > Cập nhật lần cuối: 2026-07-17 · Nhánh làm tiếp từ handoff Claude/Codex.
> >
> **Vai trò file (single source of truth):** file này là **nguồn chuẩn cho "còn > **Vai trò file (single source of truth):** file này là **nguồn chuẩn cho "còn
> gì phải làm"**. Control **đã implement+test** → xem `CASAN_HARDENING_STATUS.md`. > gì phải làm"**. Control **đã implement+test** → xem `CASAN_HARDENING_STATUS.md`.
@@ -30,6 +30,7 @@
| T1.2 | **IdP/OIDC cho approval** (thay registry pubkey tĩnh) | 07 C4 / V20 | ✅ MVP done+test | `approval-verify.sh` chấp nhận `CASAN_APPROVAL_JWT` RS256 ký bởi mock IdP, verify chữ ký bằng pubkey, check `sub`/`role`/`exp` + bind `action`/`actor`/`input_sha256`. Test: JWT hợp lệ→APPROVED, hết hạn/sai role/chữ ký giả→DENY. Prod còn cần IdP/JWKS thật. | | T1.2 | **IdP/OIDC cho approval** (thay registry pubkey tĩnh) | 07 C4 / V20 | ✅ MVP done+test | `approval-verify.sh` chấp nhận `CASAN_APPROVAL_JWT` RS256 ký bởi mock IdP, verify chữ ký bằng pubkey, check `sub`/`role`/`exp` + bind `action`/`actor`/`input_sha256`. Test: JWT hợp lệ→APPROVED, hết hạn/sai role/chữ ký giả→DENY. Prod còn cần IdP/JWKS thật. |
| T1.3 | **Traceability REQ→code→test** (khác biệt nhất) | 10 | ✅ MVP done+test | `traceability-matrix.py` parse FR-xx từ requirement, dùng `apps/okr/domain/traceability-map.json`, gate mọi FR có ≥1 code + ≥1 test; Evidence Pack thêm `traceability-matrix.json`. Test: FR đủ→PASS, FR thiếu test→FAIL. | | T1.3 | **Traceability REQ→code→test** (khác biệt nhất) | 10 | ✅ MVP done+test | `traceability-matrix.py` parse FR-xx từ requirement, dùng `apps/okr/domain/traceability-map.json`, gate mọi FR có ≥1 code + ≥1 test; Evidence Pack thêm `traceability-matrix.json`. Test: FR đủ→PASS, FR thiếu test→FAIL. |
| T1.4 | **Fix test-infra backend** (Prisma MySQL vs `setup-sqlite.mjs`) — gỡ chặn `npm test` để Plan-05 CI xanh | 05 | ✅ done+test | Đã fix: `scripts/make-sqlite-schema.mjs` sinh `schema.sqlite.prisma` từ schema.prisma (không drift), test dùng `prisma db push` thay migration MySQL thủ công; bỏ `setup-sqlite.mjs`; đổi `bcrypt`→`bcryptjs` (thuần JS, không cần native build); thêm `cross-env`. **Backend `npm test` = 46 pass / 0 fail / 3 skip** (llm-judge thiếu ANTHROPIC_API_KEY). Đã nối vào `ci-harness-gate.sh` (`backend-tests`). Build production (mysql) vẫn compile. Ghi chú: engine Prisma tải qua `NODE_OPTIONS=--use-system-ca` do corporate MITM. | | T1.4 | **Fix test-infra backend** (Prisma MySQL vs `setup-sqlite.mjs`) — gỡ chặn `npm test` để Plan-05 CI xanh | 05 | ✅ done+test | Đã fix: `scripts/make-sqlite-schema.mjs` sinh `schema.sqlite.prisma` từ schema.prisma (không drift), test dùng `prisma db push` thay migration MySQL thủ công; bỏ `setup-sqlite.mjs`; đổi `bcrypt`→`bcryptjs` (thuần JS, không cần native build); thêm `cross-env`. **Backend `npm test` = 46 pass / 0 fail / 3 skip** (llm-judge thiếu ANTHROPIC_API_KEY). Đã nối vào `ci-harness-gate.sh` (`backend-tests`). Build production (mysql) vẫn compile. Ghi chú: engine Prisma tải qua `NODE_OPTIONS=--use-system-ca` do corporate MITM. |
| T1.5 | **Paid-PoC release package** | Commercial P0 | 🟡 partial, evidence-backed | Done locally: Control Panel tests/build in harness + release gates; Platform manifest includes console/deploy/docs; extracted-artifact smoke works; Evidence Pack Viewer + one sample pack; Japanese PoC pack; npm third-party inventory + proprietary commercial-license decision. Remaining before claiming P0 complete: run a real Gitea PR E2E and retain its Evidence Pack; resolve all six `UNKNOWN` npm licenses plus Python/container/model/dataset/assets inventory with legal review. |
## TIER 2 — Plan-07 gaps cần hạ tầng (Docker local-prod lab đã có; prod managed thật vẫn cần) 🔌 ## TIER 2 — Plan-07 gaps cần hạ tầng (Docker local-prod lab đã có; prod managed thật vẫn cần) 🔌
+5
View File
@@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"name": "ainative-okr-casan5", "name": "ainative-okr-casan5",
"license": "UNLICENSED",
"workspaces": [ "workspaces": [
"apps/okr/backend", "apps/okr/backend",
"apps/okr/frontend", "apps/okr/frontend",
@@ -15,6 +16,7 @@
"apps/okr/backend": { "apps/okr/backend": {
"name": "@ainative-okr/backend", "name": "@ainative-okr/backend",
"version": "1.0.0", "version": "1.0.0",
"license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs/common": "^10.4.20", "@nestjs/common": "^10.4.20",
"@nestjs/core": "^10.4.20", "@nestjs/core": "^10.4.20",
@@ -42,6 +44,7 @@
"apps/okr/frontend": { "apps/okr/frontend": {
"name": "@ainative-okr/frontend", "name": "@ainative-okr/frontend",
"version": "1.0.0", "version": "1.0.0",
"license": "UNLICENSED",
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.81.5",
"axios": "^1.10.0", "axios": "^1.10.0",
@@ -7142,6 +7145,7 @@
"packages/casan-control-panel/backend": { "packages/casan-control-panel/backend": {
"name": "@casan/control-panel-backend", "name": "@casan/control-panel-backend",
"version": "1.0.0", "version": "1.0.0",
"license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs/common": "^10.4.20", "@nestjs/common": "^10.4.20",
"@nestjs/core": "^10.4.20", "@nestjs/core": "^10.4.20",
@@ -7159,6 +7163,7 @@
"packages/casan-control-panel/frontend": { "packages/casan-control-panel/frontend": {
"name": "@casan/control-panel-frontend", "name": "@casan/control-panel-frontend",
"version": "1.0.0", "version": "1.0.0",
"license": "UNLICENSED",
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.81.5",
"axios": "^1.10.0", "axios": "^1.10.0",
+1
View File
@@ -1,6 +1,7 @@
{ {
"name": "ainative-okr-casan5", "name": "ainative-okr-casan5",
"private": true, "private": true,
"license": "UNLICENSED",
"workspaces": [ "workspaces": [
"apps/okr/backend", "apps/okr/backend",
"apps/okr/frontend", "apps/okr/frontend",
@@ -2,6 +2,7 @@
"name": "@casan/control-panel-backend", "name": "@casan/control-panel-backend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"license": "UNLICENSED",
"type": "module", "type": "module",
"description": "CASAN Ops Console — NestJS API over harness telemetry, governed settings, approval inbox, kill-switch, and FinOps/SLO (Plan-13 Track 1/2/3/4 partial).", "description": "CASAN Ops Console — NestJS API over harness telemetry, governed settings, approval inbox, kill-switch, and FinOps/SLO (Plan-13 Track 1/2/3/4 partial).",
"scripts": { "scripts": {
@@ -7,9 +7,10 @@ import { ApprovalsModule } from './approvals/approvals.module.js';
import { ChatModule } from './chat/chat.module.js'; import { ChatModule } from './chat/chat.module.js';
import { ProviderAuthModule } from './provider-auth/provider-auth.module.js'; import { ProviderAuthModule } from './provider-auth/provider-auth.module.js';
import { GoalsModule } from './goals/goals.module.js'; import { GoalsModule } from './goals/goals.module.js';
import { EvidenceModule } from './evidence/evidence.module.js';
@Module({ @Module({
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule], imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule, EvidenceModule],
controllers: [HealthController], controllers: [HealthController],
}) })
export class AppModule {} export class AppModule {}
@@ -23,8 +23,8 @@ export class ChatController {
} }
@Get('audit/verify') @Get('audit/verify')
verifyAudit() { verifyAudit(@Headers() headers: Record<string, string | string[] | undefined>) {
return ok(this.svc.verifyAudit()); return ok(this.svc.verifyAudit(actorFromHeaders(headers)));
} }
@Get('history') @Get('history')
@@ -37,8 +37,12 @@ export class ChatController {
} }
@Get('replay') @Get('replay')
replay(@Query('chatId') chatId?: string, @Query('turnId') turnId?: string, @Query('tenant') tenant?: string) { replay(
return ok(this.svc.replay(chatId || '', turnId || '', tenant || '')); @Headers() headers: Record<string, string | string[] | undefined>,
@Query('chatId') chatId?: string,
@Query('turnId') turnId?: string,
) {
return ok(this.svc.replay(actorFromHeaders(headers), chatId || '', turnId || ''));
} }
@Get('actions') @Get('actions')
@@ -107,7 +107,7 @@ export class ChatService {
const res = runPython(CHAT_CLI, args, runtime.env); const res = runPython(CHAT_CLI, args, runtime.env);
const parsed = parseJson<Record<string, any>>(res.stdout); const parsed = parseJson<Record<string, any>>(res.stdout);
if (parsed) { if (parsed) {
return { ...parsed, actor, audit_verify: this.verifyAudit() }; return { ...parsed, actor, audit_verify: this.verifyAudit(actor) };
} }
if (res.status !== 0) { if (res.status !== 0) {
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_CLI_FAILED'); throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_CLI_FAILED');
@@ -115,8 +115,9 @@ export class ChatService {
throw new InternalServerErrorException('CHAT_CLI_EMPTY_RESPONSE'); throw new InternalServerErrorException('CHAT_CLI_EMPTY_RESPONSE');
} }
verifyAudit() { verifyAudit(actor: SettingsActor) {
const res = runPython(CHAT_CLI, ['verify-audit']); this.requireRead(actor);
const res = runPython(CHAT_CLI, ['verify-audit'], this.tenantEnv(actor));
return { ok: res.status === 0, output: res.stdout || res.stderr }; return { ok: res.status === 0, output: res.stdout || res.stderr };
} }
@@ -125,7 +126,7 @@ export class ChatService {
const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? Math.trunc(limit) : 50, 100)); const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? Math.trunc(limit) : 50, 100));
const args = ['history', '--actor', actor.actor, '--tenant', actor.tenant, '--limit', String(safeLimit)]; const args = ['history', '--actor', actor.actor, '--tenant', actor.tenant, '--limit', String(safeLimit)];
if (chatId) args.push('--chat-id', chatId); if (chatId) args.push('--chat-id', chatId);
const res = runPython(CHAT_CLI, args); const res = runPython(CHAT_CLI, args, this.tenantEnv(actor));
const parsed = parseJson<Record<string, unknown>>(res.stdout); const parsed = parseJson<Record<string, unknown>>(res.stdout);
if (res.status === 0 && parsed?.ok === true) return parsed; if (res.status === 0 && parsed?.ok === true) return parsed;
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_HISTORY_FAILED'); throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_HISTORY_FAILED');
@@ -176,12 +177,12 @@ export class ChatService {
child.on('close', () => res.end()); child.on('close', () => res.end());
} }
replay(chatId = '', turnId = '', tenant = '') { replay(actor: SettingsActor, chatId = '', turnId = '') {
this.requireRead(actor);
const args = ['replay']; const args = ['replay'];
if (chatId) args.push('--chat-id', chatId); if (chatId) args.push('--chat-id', chatId);
if (turnId) args.push('--turn-id', turnId); if (turnId) args.push('--turn-id', turnId);
const env = tenant && tenant !== 'default' ? { CASAN_TENANT_ID: tenant } : {}; const res = runPython(REPLAY_CLI, args, this.tenantEnv(actor));
const res = runPython(REPLAY_CLI, args, env);
const parsed = parseJson<Record<string, any>>(res.stdout); const parsed = parseJson<Record<string, any>>(res.stdout);
if (parsed) return { ok: res.status === 0, ...parsed }; if (parsed) return { ok: res.status === 0, ...parsed };
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_REPLAY_FAILED'); throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_REPLAY_FAILED');
@@ -272,6 +273,10 @@ export class ChatService {
return { policyProvider, env: { CASAN_TENANT_ID: actor.tenant || 'default', ...parsed.env } }; return { policyProvider, env: { CASAN_TENANT_ID: actor.tenant || 'default', ...parsed.env } };
} }
private tenantEnv(actor: SettingsActor): NodeJS.ProcessEnv {
return { CASAN_TENANT_ID: actor.tenant || 'default' };
}
private requireConnectionAdmin(actor: SettingsActor) { private requireConnectionAdmin(actor: SettingsActor) {
if (!['project-admin', 'org-admin'].includes(actor.role)) { if (!['project-admin', 'org-admin'].includes(actor.role)) {
throw new ForbiddenException('MODEL_CONNECTION_ADMIN_REQUIRED'); throw new ForbiddenException('MODEL_CONNECTION_ADMIN_REQUIRED');
@@ -52,6 +52,7 @@ export const PATHS = {
chatAudit: env('CASAN_CP_CHAT_AUDIT', '.specify/logs/chat/chat-turns.jsonl'), chatAudit: env('CASAN_CP_CHAT_AUDIT', '.specify/logs/chat/chat-turns.jsonl'),
delegationPolicy: env('CASAN_CP_DELEGATION_POLICY', 'packages/casan-harness/config/delegation-policy.yaml'), delegationPolicy: env('CASAN_CP_DELEGATION_POLICY', 'packages/casan-harness/config/delegation-policy.yaml'),
selfImprove: env('CASAN_CP_SELF_IMPROVE', 'packages/casan-harness/scripts/bash/self-improve.py'), selfImprove: env('CASAN_CP_SELF_IMPROVE', 'packages/casan-harness/scripts/bash/self-improve.py'),
evidencePacks: env('CASAN_CP_EVIDENCE_PACKS', 'docs/output/casan/evidence-packs'),
}; };
export const STALE_AFTER_S = Number(process.env.CASAN_DASHBOARD_STALE_S ?? 3600); export const STALE_AFTER_S = Number(process.env.CASAN_DASHBOARD_STALE_S ?? 3600);
@@ -0,0 +1,14 @@
import { Controller, Get, Inject, Param } from '@nestjs/common';
import { ok } from '../common/api-response.js';
import { EvidenceService } from './evidence.service.js';
@Controller('api/v1/evidence-packs')
export class EvidenceController {
constructor(@Inject(EvidenceService) private readonly service: EvidenceService) {}
@Get()
list() { return ok(this.service.list()); }
@Get(':id')
get(@Param('id') id: string) { return ok(this.service.get(id)); }
}
@@ -0,0 +1,6 @@
import { Module } from '@nestjs/common';
import { EvidenceController } from './evidence.controller.js';
import { EvidenceService } from './evidence.service.js';
@Module({ controllers: [EvidenceController], providers: [EvidenceService] })
export class EvidenceModule {}
@@ -0,0 +1,107 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, join, relative, resolve } from 'node:path';
import { execFileSync } from 'node:child_process';
import { APP_ROOT, PATHS } from '../common/app-root.js';
const PACK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const JSON_LIMIT = 1024 * 1024;
const TEXT_LIMIT = 256 * 1024;
const REPORTS = [
'h1-context-report.json', 'h2-tool-audit.json', 'h3-eval-scorecard.json',
'h4-security-report.json', 'h5-audit-chain-proof.json', 'h6-cost-telemetry.json',
'h7-orchestration-report.json', 'redteam-result.json', 'benign-fp-report.json',
];
function isInside(root: string, path: string) {
const rel = relative(root, path);
return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${String.fromCharCode(47)}`));
}
function readBounded(path: string, max: number): string | null {
try {
if (statSync(path).size > max) return null;
return readFileSync(path, 'utf8');
} catch { return null; }
}
function readObject(path: string): Record<string, unknown> | null {
const source = readBounded(path, JSON_LIMIT);
if (!source) return null;
try {
const value: unknown = JSON.parse(source);
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
} catch { return null; }
}
@Injectable()
export class EvidenceService {
private readonly root = resolve(PATHS.evidencePacks);
private pack(id: string): string {
if (!PACK_ID.test(id)) throw new NotFoundException('Evidence pack not found');
const path = resolve(this.root, id);
if (!isInside(this.root, path) || basename(path) !== id || !existsSync(path)) {
throw new NotFoundException('Evidence pack not found');
}
return path;
}
private integrity(dir: string) {
const script = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash', 'evidence-pack-verify.py');
if (!existsSync(script)) return { ok: false, status: 'verifier_unavailable', output: 'Evidence verifier is unavailable.' };
try {
const output = execFileSync('python3', [script, dir], { cwd: APP_ROOT, encoding: 'utf8', timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
return { ok: true, status: 'verified', output };
} catch (error: unknown) {
const detail = error && typeof error === 'object' && 'stderr' in error ? String((error as { stderr?: unknown }).stderr ?? '') : '';
return { ok: false, status: 'failed', output: detail.trim().slice(0, 500) || 'Evidence manifest verification failed.' };
}
}
private summary(id: string, dir: string) {
const run = readObject(join(dir, 'run-summary.json')) ?? {};
const manifest = readObject(join(dir, 'artifact-manifest.json')) ?? {};
const signature = existsSync(join(dir, 'evidence-pack.sig')) ? 'present' : 'unsigned';
return {
run_id: String(run.run_id ?? id),
certified: run.certified === true,
certification_reasons: Array.isArray(run.certification_reasons) ? run.certification_reasons.map(String) : [],
pack_version: String(run.pack_version ?? 'unknown'),
created_at: statSync(dir).mtime.toISOString(),
file_count: manifest.files && typeof manifest.files === 'object' && !Array.isArray(manifest.files) ? Object.keys(manifest.files as Record<string, unknown>).length : 0,
signature,
integrity: this.integrity(dir),
};
}
list() {
if (!existsSync(this.root)) return { root: this.root, packs: [] };
const packs = readdirSync(this.root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && PACK_ID.test(entry.name))
.map((entry) => this.summary(entry.name, join(this.root, entry.name)))
.sort((a, b) => b.created_at.localeCompare(a.created_at));
return { root: this.root, packs };
}
get(id: string) {
const dir = this.pack(id);
const manifest = readObject(join(dir, 'artifact-manifest.json')) ?? {};
const hashes = manifest.files && typeof manifest.files === 'object' && !Array.isArray(manifest.files)
? manifest.files as Record<string, unknown> : {};
const files = Object.entries(hashes).map(([path, sha256]) => ({
path,
sha256: String(sha256),
bytes: (() => { try { return statSync(join(dir, path)).size; } catch { return 0; } })(),
}));
const reports = Object.fromEntries(REPORTS.map((name) => [name, readObject(join(dir, name))]).filter(([, value]) => value));
return {
...this.summary(id, dir),
manifest_head: readBounded(join(dir, 'manifest-head.txt'), 512)?.trim() || String(manifest.manifest_head ?? '') || null,
run_summary: readObject(join(dir, 'run-summary.json')),
decision_log: readBounded(join(dir, 'decision-log.md'), TEXT_LIMIT),
reports,
files,
};
}
}
@@ -1,6 +1,6 @@
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync } from 'node:fs'; import { mkdirSync, mkdtempSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { ChatService } from '../src/chat/chat.service.js'; import { ChatService } from '../src/chat/chat.service.js';
@@ -20,7 +20,9 @@ function withTempChatState(fn: () => void) {
const state = mkdtempSync(join(tmpdir(), 'cp-chat-')); const state = mkdtempSync(join(tmpdir(), 'cp-chat-'));
process.env.CASAN_STATE_ROOT = state; process.env.CASAN_STATE_ROOT = state;
process.env.CASAN_TENANT_STATE_ROOT = join(state, 'tenants'); process.env.CASAN_TENANT_STATE_ROOT = join(state, 'tenants');
process.env.CASAN_APPROVAL_INBOX_FILE = join(state, 'approval-inbox.json'); const defaultApprovalDir = join(state, 'tenants', 'default', 'approvals');
mkdirSync(defaultApprovalDir, { recursive: true });
process.env.CASAN_APPROVAL_INBOX_FILE = join(defaultApprovalDir, 'approval-inbox.json');
delete process.env.CASAN_CHAT_AUDIT_LOG; delete process.env.CASAN_CHAT_AUDIT_LOG;
delete process.env.CASAN_CHAT_AUDIT_HEAD; delete process.env.CASAN_CHAT_AUDIT_HEAD;
delete process.env.CASAN_CHAT_METRICS_LOG; delete process.env.CASAN_CHAT_METRICS_LOG;
@@ -111,7 +113,7 @@ test('chat ask executes registered operator action through action-gate', () => {
assert.equal(res.loop_run.side_effect_released, true); assert.equal(res.loop_run.side_effect_released, true);
assert.equal(res.loop_run.trace_verify.ok, true); assert.equal(res.loop_run.trace_verify.ok, true);
assert.equal(res.loop_run.replay.ok, true); assert.equal(res.loop_run.replay.ok, true);
const replay = svc.replay('operator-chat') as any; const replay = svc.replay(operator, 'operator-chat') as any;
assert.equal(replay.ok, true); assert.equal(replay.ok, true);
assert.equal(replay.decision, 'MATCH'); assert.equal(replay.decision, 'MATCH');
assert.equal(replay.loop_replayed, 1); assert.equal(replay.loop_replayed, 1);
@@ -163,12 +165,12 @@ test('chat replay is partitioned by non-default tenant', () => {
assert.equal(res.success, true); assert.equal(res.success, true);
assert.equal(res.audit_verify.ok, true); assert.equal(res.audit_verify.ok, true);
const alphaReplay = svc.replay('tenant-chat', '', 'alpha') as any; const alphaReplay = svc.replay(alpha, 'tenant-chat') as any;
assert.equal(alphaReplay.ok, true); assert.equal(alphaReplay.ok, true);
assert.equal(alphaReplay.decision, 'MATCH'); assert.equal(alphaReplay.decision, 'MATCH');
assert.equal(alphaReplay.records, 1); assert.equal(alphaReplay.records, 1);
const betaReplay = svc.replay('tenant-chat', '', 'beta') as any; const betaReplay = svc.replay({ ...viewer, actor: 'tenant-beta', tenant: 'beta' }, 'tenant-chat') as any;
assert.equal(betaReplay.ok, true); assert.equal(betaReplay.ok, true);
assert.equal(betaReplay.records, 0); assert.equal(betaReplay.records, 0);
}); });
@@ -2,6 +2,7 @@
"name": "@casan/control-panel-frontend", "name": "@casan/control-panel-frontend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"license": "UNLICENSED",
"type": "module", "type": "module",
"description": "CASAN Ops Console \u2014 read-only React UI over the harness telemetry API.", "description": "CASAN Ops Console \u2014 read-only React UI over the harness telemetry API.",
"scripts": { "scripts": {
@@ -12,6 +12,7 @@ import { Approvals } from './pages/Approvals';
import { CommandCenter } from './pages/CommandCenter'; import { CommandCenter } from './pages/CommandCenter';
import { Chat } from './pages/Chat'; import { Chat } from './pages/Chat';
import { Goals } from './pages/Goals'; import { Goals } from './pages/Goals';
import { EvidencePacks } from './pages/EvidencePacks';
export default function App() { export default function App() {
return ( return (
@@ -29,6 +30,7 @@ export default function App() {
<Route path="/command" element={<CommandCenter />} /> <Route path="/command" element={<CommandCenter />} />
<Route path="/chat" element={<Chat />} /> <Route path="/chat" element={<Chat />} />
<Route path="/goals" element={<Goals />} /> <Route path="/goals" element={<Goals />} />
<Route path="/evidence-packs" element={<EvidencePacks />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</AppLayout> </AppLayout>
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
type IconName = 'grid' | 'command' | 'chat' | 'goal' | 'runs' | 'shield' | 'governance' | 'incident' | 'trace' | 'coins' | 'approval' | 'settings'; type IconName = 'grid' | 'command' | 'chat' | 'goal' | 'runs' | 'shield' | 'governance' | 'incident' | 'trace' | 'evidence' | 'coins' | 'approval' | 'settings';
interface NavItem { to: string; label: string; icon: IconName; } interface NavItem { to: string; label: string; icon: IconName; }
@@ -18,6 +18,7 @@ const NAVIGATION: Array<{ label: string; items: NavItem[] }> = [
{ to: '/security', label: 'Security', icon: 'shield' }, { to: '/security', label: 'Security', icon: 'shield' },
{ to: '/incidents', label: 'Incidents', icon: 'incident' }, { to: '/incidents', label: 'Incidents', icon: 'incident' },
{ to: '/traceability', label: 'Traceability', icon: 'trace' }, { to: '/traceability', label: 'Traceability', icon: 'trace' },
{ to: '/evidence-packs', label: 'Evidence packs', icon: 'evidence' },
] }, ] },
{ label: 'Control', items: [ { label: 'Control', items: [
{ to: '/finops', label: 'FinOps & SLO', icon: 'coins' }, { to: '/finops', label: 'FinOps & SLO', icon: 'coins' },
@@ -37,6 +38,7 @@ function Icon({ name }: { name: IconName }) {
governance: <><path d="M4 20h16M6 17V9M10 17V5M14 17V9M18 17V5" /><path d="M3 5h18l-9-3-9 3Z" /></>, governance: <><path d="M4 20h16M6 17V9M10 17V5M14 17V9M18 17V5" /><path d="M3 5h18l-9-3-9 3Z" /></>,
incident: <><path d="M10.3 3.3 2.7 17a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 3.3a2 2 0 0 0-3.4 0Z" /><path d="M12 9v4M12 17h.01" /></>, incident: <><path d="M10.3 3.3 2.7 17a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 3.3a2 2 0 0 0-3.4 0Z" /><path d="M12 9v4M12 17h.01" /></>,
trace: <><circle cx="6" cy="6" r="3" /><circle cx="18" cy="18" r="3" /><circle cx="18" cy="6" r="3" /><path d="m8.6 7.5 6.8 3M9 6h6" /></>, trace: <><circle cx="6" cy="6" r="3" /><circle cx="18" cy="18" r="3" /><circle cx="18" cy="6" r="3" /><path d="m8.6 7.5 6.8 3M9 6h6" /></>,
evidence: <><path d="M7 3h7l3 3v15H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" /><path d="M14 3v4h4M8.5 12h7M8.5 16h5" /><path d="m9 8.5 1 1 2-2" /></>,
coins: <><ellipse cx="12" cy="5" rx="7" ry="3" /><path d="M5 5v7c0 1.7 3.1 3 7 3s7-1.3 7-3V5M5 12v7c0 1.7 3.1 3 7 3s7-1.3 7-3v-7" /></>, coins: <><ellipse cx="12" cy="5" rx="7" ry="3" /><path d="M5 5v7c0 1.7 3.1 3 7 3s7-1.3 7-3V5M5 12v7c0 1.7 3.1 3 7 3s7-1.3 7-3v-7" /></>,
approval: <><path d="M9 11 11 13l4-4" /><path d="M12 22c5-2.1 8-5.3 8-10V5l-8-3-8 3v7c0 4.7 3 7.9 8 10Z" /></>, approval: <><path d="M9 11 11 13l4-4" /><path d="M12 22c5-2.1 8-5.3 8-10V5l-8-3-8 3v7c0 4.7 3 7.9 8 10Z" /></>,
settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2 2-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5v.2h-2.8v-.2a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1-2-2 .1-.1A1.7 1.7 0 0 0 7.4 15a1.7 1.7 0 0 0-1.5-1H5.7v-2.8h.2a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9L7 8.2l2-2 .1.1a1.7 1.7 0 0 0 1.9.3 1.7 1.7 0 0 0 1-1.5v-.2h2.8v.2a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1 2 2-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1h.2V14h-.2a1.7 1.7 0 0 0-1.5 1Z" /></>, settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2 2-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5v.2h-2.8v-.2a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1-2-2 .1-.1A1.7 1.7 0 0 0 7.4 15a1.7 1.7 0 0 0-1.5-1H5.7v-2.8h.2a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9L7 8.2l2-2 .1.1a1.7 1.7 0 0 0 1.9.3 1.7 1.7 0 0 0 1-1.5v-.2h2.8v.2a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1 2 2-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1h.2V14h-.2a1.7 1.7 0 0 0-1.5 1Z" /></>,
@@ -34,6 +34,25 @@ export interface HarnessRunRecord {
cost_estimate?: number; cost_estimate?: number;
} }
export interface EvidenceIntegrity { ok: boolean; status: string; output: string }
export interface EvidencePackSummary {
run_id: string;
certified: boolean;
certification_reasons: string[];
pack_version: string;
created_at: string;
file_count: number;
signature: 'present' | 'unsigned';
integrity: EvidenceIntegrity;
}
export interface EvidencePackDetail extends EvidencePackSummary {
manifest_head: string | null;
run_summary: Record<string, unknown> | null;
decision_log: string | null;
reports: Record<string, Record<string, unknown>>;
files: Array<{ path: string; sha256: string; bytes: number }>;
}
export interface Overview extends Freshness { export interface Overview extends Freshness {
totals: { totals: {
runs: number; total_cost: number; avg_latency_ms: number; failures: number; runs: number; total_cost: number; avg_latency_ms: number; failures: number;
@@ -419,6 +438,8 @@ export const api = {
traceability: () => get<Freshness & { matrix: any }>('traceability'), traceability: () => get<Freshness & { matrix: any }>('traceability'),
cost: () => get<Freshness & { provider_tokens: number; provider_cost: number; by_provider: any[]; business_kpi: any }>('cost'), cost: () => get<Freshness & { provider_tokens: number; provider_cost: number; by_provider: any[]; business_kpi: any }>('cost'),
command: () => get<CommandCenterState>('command'), command: () => get<CommandCenterState>('command'),
evidencePacks: () => get<{ root: string; packs: EvidencePackSummary[] }>('evidence-packs'),
evidencePack: (id: string) => get<EvidencePackDetail>(`evidence-packs/${encodeURIComponent(id)}`),
settings: (actor: SettingsActor) => getWithHeaders<SettingsState>('settings', actorHeaders(actor)), settings: (actor: SettingsActor) => getWithHeaders<SettingsState>('settings', actorHeaders(actor)),
setSetting: (actor: SettingsActor, body: { key: string; value: unknown; reason: string; approval?: string }) => setSetting: (actor: SettingsActor, body: { key: string; value: unknown; reason: string; approval?: string }) =>
post<{ key: string; setting: any; audit_verify: { ok: boolean; output: string } }>('settings', body, actorHeaders(actor)), post<{ key: string; setting: any; audit_verify: { ok: boolean; output: string } }>('settings', body, actorHeaders(actor)),
@@ -0,0 +1,62 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { api, type EvidencePackDetail, type EvidencePackSummary } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
const HARNESSES = [
['H1', 'Context', 'h1-context-report.json'], ['H2', 'Tool', 'h2-tool-audit.json'],
['H3', 'Evaluation', 'h3-eval-scorecard.json'], ['H4', 'Security', 'h4-security-report.json'],
['H5', 'Governance', 'h5-audit-chain-proof.json'], ['H6', 'AgentOps', 'h6-cost-telemetry.json'],
['H7', 'Orchestration', 'h7-orchestration-report.json'],
] as const;
function shortHash(value: string | null) { return value ? `${value.slice(0, 12)}…${value.slice(-8)}` : 'not recorded'; }
function label(reason: string) { return reason.replaceAll('_', ' '); }
function statusFor(report: Record<string, unknown> | undefined, detail: EvidencePackDetail) {
if (!report) return 'not recorded';
if (report.chain_ok === false || report.audit_chain_ok === false || report.telemetry_ok === false) return 'failed';
if (Number(report.blocked ?? 0) > 0 || report.cost_spike_status === 'spike_detected') return 'attention';
return detail.integrity.ok ? 'recorded' : 'unverified';
}
function PackRow({ pack, selected, onSelect }: { pack: EvidencePackSummary; selected: boolean; onSelect: () => void }) {
return <button type="button" onClick={onSelect} className={`w-full rounded-xl border p-3.5 text-left transition ${selected ? 'border-indigo-300 bg-indigo-50/70 shadow-sm' : 'border-transparent hover:border-slate-200 hover:bg-slate-50'}`}>
<div className="flex items-start justify-between gap-2"><span className="font-mono text-xs font-semibold text-slate-700">{pack.run_id}</span><StatusBadge value={pack.certified ? 'certified' : 'uncertified'} /></div>
<div className="mt-2 flex items-center justify-between text-[11px] text-slate-500"><span>{new Date(pack.created_at).toLocaleString()}</span><span>{pack.file_count} files</span></div>
</button>;
}
function Detail({ pack }: { pack: EvidencePackDetail }) {
const reportEntries = Object.entries(pack.reports);
return <div className="space-y-5">
<section className={`overflow-hidden rounded-2xl border ${pack.certified ? 'border-emerald-200 bg-gradient-to-br from-emerald-50 to-white' : 'border-amber-200 bg-gradient-to-br from-amber-50 to-white'} p-5 sm:p-6`}>
<div className="flex flex-col justify-between gap-5 sm:flex-row sm:items-start">
<div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Evidence verdict</div><h1 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900">{pack.certified ? 'Certified evidence pack' : 'Evidence captured — certification withheld'}</h1><p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">Run <span className="font-mono text-xs">{pack.run_id}</span> is bound to a file manifest and verified independently before it is shown here.</p></div>
<div className="flex flex-wrap gap-2"><StatusBadge value={pack.integrity.ok ? 'manifest verified' : 'integrity failed'} /><StatusBadge value={pack.signature === 'present' ? 'signature present' : 'unsigned'} /></div>
</div>
{pack.certification_reasons.length > 0 && <div className="mt-5 rounded-xl border border-amber-200 bg-white/80 p-3.5"><div className="text-[11px] font-bold uppercase tracking-[0.13em] text-amber-700">Why certification is withheld</div><div className="mt-2 flex flex-wrap gap-2">{pack.certification_reasons.map((reason) => <span key={reason} className="rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800">{label(reason)}</span>)}</div></div>}
</section>
<Card title="H1–H7 chain of custody" right={<span className="font-mono text-[11px] text-slate-400">manifest {shortHash(pack.manifest_head)}</span>}>
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-7">{HARNESSES.map(([id, name, file]) => { const report = pack.reports[file]; const state = statusFor(report, pack); return <div key={id} className="relative rounded-xl border border-slate-200 bg-slate-50/70 p-3"><div className="flex items-center justify-between"><span className="font-mono text-xs font-bold text-indigo-700">{id}</span><span className={`h-2 w-2 rounded-full ${state === 'recorded' ? 'bg-emerald-500' : state === 'failed' ? 'bg-rose-500' : state === 'attention' ? 'bg-amber-500' : 'bg-slate-300'}`} /></div><div className="mt-2 text-sm font-semibold text-slate-800">{name}</div><div className="mt-1 text-[11px] text-slate-500">{state}</div></div>; })}</div>
</Card>
<div className="grid gap-5 xl:grid-cols-[1.1fr_0.9fr]">
<Card title="Gate reports"><div className="space-y-3">{reportEntries.map(([name, report]) => <details key={name} className="rounded-xl border border-slate-200 bg-slate-50/60 p-3.5"><summary className="cursor-pointer list-none text-sm font-semibold text-slate-700"><span className="font-mono text-xs text-indigo-700">{name.replace('-report.json', '').replace('.json', '')}</span></summary><dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 border-t border-slate-200 pt-3 text-xs">{Object.entries(report).filter(([, value]) => typeof value !== 'object').map(([key, value]) => <div key={key}><dt className="text-slate-400">{key.replaceAll('_', ' ')}</dt><dd className="mt-0.5 break-words font-medium text-slate-700">{String(value)}</dd></div>)}</dl></details>)}</div></Card>
<Card title="Evidence manifest" right={<span className="text-xs text-slate-400">{pack.files.length} bound files</span>}><div className="max-h-[430px] space-y-2 overflow-y-auto pr-1">{pack.files.map((file) => <div key={file.path} className="rounded-xl border border-slate-100 bg-slate-50/70 p-3"><div className="break-all font-mono text-xs font-medium text-slate-700">{file.path}</div><div className="mt-1.5 flex justify-between gap-3 font-mono text-[10px] text-slate-400"><span>{shortHash(file.sha256)}</span><span>{file.bytes.toLocaleString()} B</span></div></div>)}</div></Card>
</div>
<Card title="Decision log"><pre className="max-h-80 overflow-auto whitespace-pre-wrap rounded-xl bg-slate-950 p-4 font-mono text-xs leading-5 text-slate-200">{pack.decision_log ?? 'No decision log was recorded.'}</pre></Card>
</div>;
}
export function EvidencePacks() {
const { data, isLoading, isError } = useQuery({ queryKey: ['evidence-packs'], queryFn: api.evidencePacks });
const [selected, setSelected] = useState('');
const activeId = selected || data?.packs[0]?.run_id || '';
const detail = useQuery({ queryKey: ['evidence-pack', activeId], queryFn: () => api.evidencePack(activeId), enabled: Boolean(activeId) });
const heading = useMemo(() => data?.packs.length ?? 0, [data]);
if (isLoading) return <div className="text-slate-500">Loading evidence inventory…</div>;
if (isError || !data) return <div className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-rose-700">Evidence inventory could not be loaded.</div>;
if (!data.packs.length) return <Card title="Evidence packs"><div className="py-12 text-center"><div className="text-lg font-semibold text-slate-800">No evidence pack has been created yet</div><p className="mx-auto mt-2 max-w-lg text-sm leading-6 text-slate-500">Create one with <code className="rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs">casan pack &lt;run-id&gt;</code>; it will appear here only after the on-disk manifest is available.</p></div></Card>;
return <div className="space-y-5"><div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-indigo-600">Assurance workspace</div><h1 className="mt-1 text-2xl font-semibold tracking-tight text-slate-900">Evidence packs <span className="text-slate-400">({heading})</span></h1><p className="mt-1 text-sm text-slate-500">Inspect certification, provenance, gates and immutable file hashes without opening raw logs.</p></div><div className="grid gap-5 xl:grid-cols-[288px_minmax(0,1fr)]"><aside className="rounded-2xl border border-slate-200 bg-white p-2 shadow-sm"><div className="px-3 py-2 text-[11px] font-bold uppercase tracking-[0.13em] text-slate-400">Available packs</div><div className="space-y-1.5">{data.packs.map((pack) => <PackRow key={pack.run_id} pack={pack} selected={pack.run_id === activeId} onSelect={() => setSelected(pack.run_id)} />)}</div></aside><main>{detail.isLoading && <div className="text-slate-500">Verifying manifest…</div>}{detail.isError && <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">The selected evidence pack could not be read safely.</div>}{detail.data && <Detail pack={detail.data} />}</main></div></div>;
}
+12 -5
View File
@@ -19,8 +19,14 @@ Enterprise governed AI-SDLC console. Packages: `casan-enterprise`, `casan-govern
| Loop governance | Plan-17 loop primitives (`loop-*.py`) | | Loop governance | Plan-17 loop primitives (`loop-*.py`) |
## Components still to build (NOT in this task) ## Components still to build (NOT in this task)
- Governed Chat Console (Plan-18) · Prompt Mode Router · Model Provider Management - Managed enterprise deployment with enterprise OIDC, CA, network policy and
- Operator mode · Codegen mode · Agent/Skill Registry · policy-versioning UI production secret/KMS enforcement
- HA/DR, RPO/RTO, lifecycle compatibility, support/SLA and independent security review
- Production immutable object storage and external evidence verification operations
The Platform Preview already contains the Governed Chat/Prompt Router/Operator/
Codegen and agent/skill/model-selection MVPs, plus the Evidence Pack Viewer.
They are not evidence that an Enterprise edition has shipped.
## Why it refuses to package ## Why it refuses to package
Per the packaging principle, a level that isn't implemented must **fail clearly** rather Per the packaging principle, a level that isn't implemented must **fail clearly** rather
@@ -29,6 +35,7 @@ than emit a fake-complete artifact. Enterprise is `status: future` in
explanation. When the console is built, flip its status to `preview`/`implemented`. explanation. When the console is built, flip its status to `preview`/`implemented`.
## To implement later ## To implement later
Sequence: Plan-14 (RBAC console) → Plan-13 (Control Plane) → Plan-18 (Governed Chat Sequence: finish the paid-PoC evidence/Gitea flow → enterprise deployment and
Console: read-only → operator → chat-as-loop → multi-tenant). Reuse the existing blocks operations controls → independent review → production readiness. Reuse the existing
above instead of re-writing them. blocks above instead of re-writing them. See
[`EDITION_FEATURE_LIMITATION_MATRIX.md`](../../docs/packaging/EDITION_FEATURE_LIMITATION_MATRIX.md).
@@ -7,6 +7,7 @@ set -uo pipefail
# #
# Env: # Env:
# CASAN_CI_RUN_FRONTEND=0|1 default 1 # CASAN_CI_RUN_FRONTEND=0|1 default 1
# CASAN_CI_RUN_CONTROL_PANEL=0|1 default 1
# CASAN_CI_RUN_INFRA_LAB=0|1 default 0 (Docker Compose lab is optional in CI) # CASAN_CI_RUN_INFRA_LAB=0|1 default 0 (Docker Compose lab is optional in CI)
# CASAN_CI_STEP_TIMEOUT_SEC default 600 # CASAN_CI_STEP_TIMEOUT_SEC default 600
# CASAN_CI_SUITE_FILTER optional regex; run matching suite names only # CASAN_CI_SUITE_FILTER optional regex; run matching suite names only
@@ -188,6 +189,17 @@ else
skip "frontend-vitest (CASAN_CI_RUN_FRONTEND=0)" skip "frontend-vitest (CASAN_CI_RUN_FRONTEND=0)"
fi fi
if [[ "${CASAN_CI_RUN_CONTROL_PANEL:-1}" == "1" ]]; then
if command -v npm >/dev/null 2>&1; then
run "control-panel-tests" npm run console:test
run "control-panel-build" npm run console:build
else
skip "control-panel-tests/build (npm unavailable)"
fi
else
skip "control-panel-tests/build (CASAN_CI_RUN_CONTROL_PANEL=0)"
fi
if [[ "${CASAN_CI_RUN_INFRA_LAB:-0}" == "1" ]]; then if [[ "${CASAN_CI_RUN_INFRA_LAB:-0}" == "1" ]]; then
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
run "local-prod-infra-lab" bash "$TESTS/phase-prod-infra-lab-tests.sh" run "local-prod-infra-lab" bash "$TESTS/phase-prod-infra-lab-tests.sh"
@@ -267,6 +267,16 @@ def extract_patch(text: str) -> str:
return patch return patch
def validate_write_output(text: str) -> str:
"""Fail at the producing harness when a write-intent reply is not a diff.
Keeping this separate from storage avoids creating an artifact before the
reviewer has completed, while ensuring H2/H3 accurately identify a model
that violated the required output contract.
"""
return extract_patch(text)
def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict: def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
roots = [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])] roots = [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])]
changed = [] changed = []
@@ -640,6 +650,14 @@ def run(job_path: str) -> int:
if not allowed: if not allowed:
emit(goal_id, "H4-security", "blocked", "Local worker output rejected") emit(goal_id, "H4-security", "blocked", "Local worker output rejected")
raise ValueError("local_output_security_blocked") raise ValueError("local_output_security_blocked")
if write_intent:
try:
validate_write_output(safe_local)
except ValueError as error:
reason = str(error)
stage(job_path, "local-worker", "error", reason, job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "error", "Local worker violated patch output contract", {"reason": reason})
raise
stage(job_path, "local-worker", "pass", "Primary solution prepared", job.get("local_provider", ""), local_model) stage(job_path, "local-worker", "pass", "Primary solution prepared", job.get("local_provider", ""), local_model)
update_job(job_path, local_draft=safe_local, local_usage=local_meta) update_job(job_path, local_draft=safe_local, local_usage=local_meta)
emit(goal_id, "H2-tool", "pass", "Local solution prepared", {"provider": job.get("local_provider", ""), "model": local_model, **local_meta}) emit(goal_id, "H2-tool", "pass", "Local solution prepared", {"provider": job.get("local_provider", ""), "model": local_model, **local_meta})
@@ -670,10 +688,23 @@ def run(job_path: str) -> int:
if not allowed: if not allowed:
emit(goal_id, "H4-security", "blocked", "Cloud reviewer output rejected") emit(goal_id, "H4-security", "blocked", "Cloud reviewer output rejected")
raise ValueError("cloud_output_security_blocked") raise ValueError("cloud_output_security_blocked")
if write_intent:
try:
validate_write_output(safe_result)
except ValueError as error:
cloud_ok = False
cloud_reason = f"reviewer_output_contract_invalid:{error}"
if cloud_ok:
stage(job_path, "cloud-reviewer", "pass", "Independent review incorporated", reviewer_provider, reviewer_model) stage(job_path, "cloud-reviewer", "pass", "Independent review incorporated", reviewer_provider, reviewer_model)
emit(goal_id, "H3-eval", "pass", "Independent review incorporated", {"provider": reviewer_provider, "model": reviewer_model, **cloud_meta}) emit(goal_id, "H3-eval", "pass", "Independent review incorporated", {"provider": reviewer_provider, "model": reviewer_model, **cloud_meta})
final_status = "completed" final_status = "completed"
metric_status = "success" metric_status = "success"
else:
safe_result = safe_local
stage(job_path, "cloud-reviewer", "warning", cloud_reason, reviewer_provider, reviewer_model)
emit(goal_id, "H3-eval", "warning", "Cloud reviewer output contract rejected; local solution retained", {"reason": cloud_reason})
final_status = "degraded"
metric_status = "degraded"
else: else:
safe_result = safe_local safe_result = safe_local
stage(job_path, "cloud-reviewer", "warning", cloud_reason, reviewer_provider, reviewer_model) stage(job_path, "cloud-reviewer", "warning", cloud_reason, reviewer_provider, reviewer_model)
+6 -2
View File
@@ -3,7 +3,8 @@
> Status: **PREVIEW.** The AgentOps dashboard and Plan-13 Control Panel, including > Status: **PREVIEW.** The AgentOps dashboard and Plan-13 Control Panel, including
> Command Center baseline and Plan-18 MVP-0/1 Chat Console plus MVP-2 agent > Command Center baseline and Plan-18 MVP-0/1 Chat Console plus MVP-2 agent
> selection and Operator loop/draft-hold foundation, exist today. > selection and Operator loop/draft-hold foundation, exist today.
> Evidence/attack viewers, Gitea integration, and managed production rollout remain > The Evidence Pack Viewer is now included. Attack Battery Viewer, Gitea evidence
> publishing, and managed production rollout remain
> planned. `package-release.sh platform` builds a clearly-stamped > planned. `package-release.sh platform` builds a clearly-stamped
> `casan-platform-preview-*` bundle containing only what exists. > `casan-platform-preview-*` bundle containing only what exists.
@@ -17,11 +18,14 @@ Optional layer for teams that want UI / dashboard / visibility. Packages: `casan
| **Ops Console (Control Panel)** | ✅ **monitoring + governed settings + HITL + kill-switch + FinOps/SLO + Command Center + Chat MVP-0/1 + agent selection foundation + local-prod TLS/OIDC smoke** | `packages/casan-control-panel/` — NestJS API + React UI over harness telemetry, settings management, approval inbox, kill-switch, FinOps/SLO, Command Center, Ask CASAN read-only, registered Operator actions, and governed agent/skill selection (`npm run console:api` + `console:ui`). Includes Run History + verdicts + governance + security + incidents + traceability + FinOps + Approvals + role-aware Settings page + `/command` evidence-backed executive view + `/chat` governed read-only/operator console. | | **Ops Console (Control Panel)** | ✅ **monitoring + governed settings + HITL + kill-switch + FinOps/SLO + Command Center + Chat MVP-0/1 + agent selection foundation + local-prod TLS/OIDC smoke** | `packages/casan-control-panel/` — NestJS API + React UI over harness telemetry, settings management, approval inbox, kill-switch, FinOps/SLO, Command Center, Ask CASAN read-only, registered Operator actions, and governed agent/skill selection (`npm run console:api` + `console:ui`). Includes Run History + verdicts + governance + security + incidents + traceability + FinOps + Approvals + role-aware Settings page + `/command` evidence-backed executive view + `/chat` governed read-only/operator console. |
| Management / settings writes | ✅ done+test | Plan-13 Track 2: wraps `control-plane-settings.py`, calls `rbac-check.py`, supports set/rollback/audit verify. | | Management / settings writes | ✅ done+test | Plan-13 Track 2: wraps `control-plane-settings.py`, calls `rbac-check.py`, supports set/rollback/audit verify. |
| RBAC + approval inbox | ✅ local-prod done | Settings + kill-switch API RBAC enforcement, approval inbox/delegation/oversight, SoD, governed setting proposal apply, and local OIDC claim→role mapping smoke are done. Enterprise IdP rollout remains production follow-up. | | RBAC + approval inbox | ✅ local-prod done | Settings + kill-switch API RBAC enforcement, approval inbox/delegation/oversight, SoD, governed setting proposal apply, and local OIDC claim→role mapping smoke are done. Enterprise IdP rollout remains production follow-up. |
| Evidence Pack Viewer | 📋 planned | reads `docs/output/casan/evidence-packs/` | | Evidence Pack Viewer | ✅ included | `/evidence-packs` verifies and presents the on-disk manifest, certification reasons, H1–H7 reports, hashes and decision log. |
| Attack Battery Viewer | 📋 planned | reads red-team corpus + H4 recall results | | Attack Battery Viewer | 📋 planned | reads red-team corpus + H4 recall results |
| Read-only Ask CASAN + Operator/CODEGEN + agent selection + loop-hold + replay/widget/approvals + tenant hardening | ✅ MVP-0/1/2/3 done+test | Plan-18: `POST /api/v1/chat/ask`, `GET /api/v1/chat/actions`, `GET /api/v1/chat/agents`, `GET /api/v1/chat/replay`, Command Center `chat_loop` widget, chat escalation into `/approvals`, tenant-scoped replay, `/chat` + `/command`, backed by harness `chat-turn.py`, `chat-agent-resolver.py`, `chat-replay.py`, `approval-inbox.py`, `artifact-scan.sh`, `tenant-store.sh`, `tenant-crypt.sh`, and Plan-17 `loop-run.sh` | | Read-only Ask CASAN + Operator/CODEGEN + agent selection + loop-hold + replay/widget/approvals + tenant hardening | ✅ MVP-0/1/2/3 done+test | Plan-18: `POST /api/v1/chat/ask`, `GET /api/v1/chat/actions`, `GET /api/v1/chat/agents`, `GET /api/v1/chat/replay`, Command Center `chat_loop` widget, chat escalation into `/approvals`, tenant-scoped replay, `/chat` + `/command`, backed by harness `chat-turn.py`, `chat-agent-resolver.py`, `chat-replay.py`, `approval-inbox.py`, `artifact-scan.sh`, `tenant-store.sh`, `tenant-crypt.sh`, and Plan-17 `loop-run.sh` |
| Gitea webhook integration | 📋 planned | trigger gate / publish evidence on push | | Gitea webhook integration | 📋 planned | trigger gate / publish evidence on push |
The authoritative customer-claim boundary is the
[`EDITION_FEATURE_LIMITATION_MATRIX.md`](../../docs/packaging/EDITION_FEATURE_LIMITATION_MATRIX.md).
## Build (preview) ## Build (preview)
```bash ```bash
scripts/package-release.sh platform # → dist/casan-platform-preview-vX.Y.Z.tar.gz scripts/package-release.sh platform # → dist/casan-platform-preview-vX.Y.Z.tar.gz
+32 -7
View File
@@ -50,15 +50,37 @@
"level": 3, "level": 3,
"package_names": ["casan-platform", "casan-control-panel"], "package_names": ["casan-platform", "casan-control-panel"],
"status": "preview", "status": "preview",
"summary": "Optional productization UI: Control Panel, Dashboard, Evidence/Attack/Run-History viewers, read-only Ask CASAN, Gitea webhook. AgentOps dashboard and Plan-13 Control Panel Track 1/2/3 plus FinOps/SLO management, Command Center baseline, and local-prod TLS/OIDC smoke exist today; the rest is preview/planned.", "summary": "Optional productization UI: packaged Control Panel, AgentOps dashboard, Run History, governed settings, approvals, kill-switch, FinOps/SLO, Command Center, Governed Chat MVP and Evidence Pack Viewer. Attack Battery Viewer, Gitea webhook publishing and managed production rollout remain preview work.",
"target_users": ["teams wanting UI/dashboard/visibility", "demo/training/productization"], "target_users": ["teams wanting UI/dashboard/visibility", "demo/training/productization"],
"extends": "devkit", "extends": "devkit",
"implemented_components": ["agentops-dashboard (packages/casan-harness/scripts/bash/dashboard-server.py + dashboard-serve.sh)", "Ops Console monitoring + governed settings + approval inbox + kill-switch + FinOps/SLO + Command Center + local-prod TLS/OIDC smoke (packages/casan-control-panel — NestJS API + React UI, Plan-13 Track 1/2/3/4 local-prod + §8.6)"], "implemented_components": ["agentops-dashboard", "control-panel monitoring and run history", "governed settings and rollback", "RBAC and approval inbox", "kill-switch", "FinOps/SLO", "Command Center", "Governed Chat read-only/analysis/operator/codegen MVP", "agent/skill/model selection", "Evidence Pack Viewer", "local-prod TLS/OIDC smoke"],
"pending_components": ["managed production deploy with enterprise IdP/cert/host (Plan-13 Track 4 / Plan-07 T2)", "evidence-pack-viewer", "attack-battery-viewer", "gitea-webhook-integration", "read-only Ask CASAN (Plan-18 MVP-0)"], "pending_components": ["managed production deploy with enterprise IdP/cert/host", "attack-battery-viewer", "gitea-webhook evidence publishing"],
"includes": [ "includes": [
"packages/casan-platform", "packages/casan-platform",
"packages/casan-control-panel",
"packages/casan-harness/scripts/bash/dashboard-server.py", "packages/casan-harness/scripts/bash/dashboard-server.py",
"packages/casan-harness/scripts/bash/dashboard-serve.sh" "packages/casan-harness/scripts/bash/dashboard-serve.sh",
"package.json",
"package-lock.json",
"Dockerfile.control-panel-api",
"Dockerfile.control-panel-ui",
"docker-compose.control-panel.yml",
"docker-compose.control-panel.local.yml",
"nginx/control-panel.conf",
"packaging/control-panel-oauth2-proxy.env.example",
"infra/local-prod/idp",
"infra/production",
"docs/security/CONTROL_PANEL_AUTH_HARDENING.md",
"docs/packaging/EDITION_FEATURE_LIMITATION_MATRIX.md",
"docs/jp-poc",
"docs/commercial/LICENSE_AND_IP_DECISION.md",
"docs/commercial/THIRD_PARTY_SOFTWARE.json",
"docs/technical_architecture.md",
"apps/okr/domain",
"apps/okr/frontend",
"apps/okr/backend",
"apps/service-desk",
"scripts/smoke-release-artifact.sh"
], ],
"artifact": "casan-platform-preview-v{VERSION}.tar.gz", "artifact": "casan-platform-preview-v{VERSION}.tar.gz",
"docker_image": "casan-platform:{VERSION} (preview)" "docker_image": "casan-platform:{VERSION} (preview)"
@@ -67,7 +89,7 @@
"level": 4, "level": 4,
"package_names": ["casan-enterprise", "casan-governed-console"], "package_names": ["casan-enterprise", "casan-governed-console"],
"status": "future", "status": "future",
"summary": "Governed Chat Console, Prompt Mode Router, Model Provider Management, Operator/Codegen modes, Agent/Skill Registry, RBAC, approval workflow, tenant isolation, KMS/WORM, policy versioning.", "summary": "Enterprise promotion layer for the already implemented governed console building blocks. The edition remains future until managed deployment, lifecycle operations, legal/compliance deliverables and enterprise support controls are verified.",
"target_users": ["enterprise/internal platform", "multi-project governance", "official governed AI-SDLC console"], "target_users": ["enterprise/internal platform", "multi-project governance", "official governed AI-SDLC console"],
"extends": "platform", "extends": "platform",
"implemented_components": [ "implemented_components": [
@@ -76,9 +98,12 @@
"approval workflow (approval-verify.sh/approval-sign.sh/approval-jwt-mint.py)", "approval workflow (approval-verify.sh/approval-sign.sh/approval-jwt-mint.py)",
"KMS (vault-kms.sh)", "KMS (vault-kms.sh)",
"WORM (worm-ledger.py)", "WORM (worm-ledger.py)",
"kill-switch (kill-switch.sh)" "kill-switch (kill-switch.sh)",
"Governed Chat/Prompt Router/Operator/Codegen MVP (packaged in Platform preview)",
"Agent/Skill/Model selection MVP (packaged in Platform preview)",
"Evidence Pack Viewer (packaged in Platform preview)"
], ],
"pending_components": ["governed-chat-console", "prompt-mode-router", "model-provider-management", "operator-mode", "codegen-mode", "agent-skill-registry", "policy-versioning-ui"], "pending_components": ["managed enterprise deployment evidence", "policy-versioning UI", "HA/DR/SLA", "external security assessment", "commercial support and compliance pack"],
"includes": ["packages/casan-enterprise"], "includes": ["packages/casan-enterprise"],
"artifact": "casan-enterprise-preview-v{VERSION}.tar.gz", "artifact": "casan-enterprise-preview-v{VERSION}.tar.gz",
"docker_image": "casan-enterprise:{VERSION} (future)" "docker_image": "casan-enterprise:{VERSION} (future)"
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Generate a reproducible third-party inventory from the committed npm lockfile.
// This is an inventory, not legal advice; every release must regenerate and review it.
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
const [output = 'docs/commercial/THIRD_PARTY_SOFTWARE.json'] = process.argv.slice(2);
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
const exceptionsPath = 'docs/commercial/LICENSE_EXCEPTIONS.json';
const exceptions = existsSync(exceptionsPath) ? JSON.parse(readFileSync(exceptionsPath, 'utf8')).exceptions ?? {} : {};
const workspaceLicenses = new Map(Object.entries(lock.packages ?? {})
.filter(([path, value]) => path && !path.startsWith('node_modules/') && value && typeof value === 'object' && typeof value.name === 'string')
.map(([, value]) => [value.name, typeof value.license === 'string' ? value.license : 'UNKNOWN']));
const rows = Object.entries(lock.packages ?? {})
.filter(([path, value]) => path.startsWith('node_modules/') && value && typeof value === 'object')
.map(([path, value]) => {
const name = path.slice('node_modules/'.length);
const key = `${name}@${String(value.version ?? 'unknown')}`;
const declared = typeof value.license === 'string' ? value.license : (workspaceLicenses.get(name) ?? 'UNKNOWN');
const exception = exceptions[key];
return {
name,
version: String(value.version ?? 'unknown'),
declared_license: declared,
license: typeof exception?.reviewed_license === 'string' ? exception.reviewed_license : declared,
review_evidence: typeof exception?.evidence === 'string' ? exception.evidence : null,
resolved: typeof value.resolved === 'string' ? value.resolved : null,
integrity: typeof value.integrity === 'string' ? value.integrity : null,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
const licenses = {};
for (const row of rows) licenses[row.license] = (licenses[row.license] ?? 0) + 1;
const inventory = {
schema_version: '1.0',
generated_from: 'package-lock.json',
generated_at: new Date().toISOString(),
scope: 'npm dependencies recorded in the root lockfile; Python, container base images, model and dataset licenses require separate release review.',
exceptions_source: existsSync(exceptionsPath) ? exceptionsPath : null,
package_count: rows.length,
licenses,
packages: rows,
};
const destination = resolve(output);
mkdirSync(dirname(destination), { recursive: true });
writeFileSync(destination, `${JSON.stringify(inventory, null, 2)}\n`);
console.log(`LICENSE_INVENTORY_CREATED path=${output} packages=${rows.length} licenses=${Object.keys(licenses).length}`);
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
# Smoke the release exactly as a customer receives it. This intentionally
# extracts into a fresh directory and never resolves files from the source tree.
#
# Usage: scripts/smoke-release-artifact.sh dist/casan-<bundle>-vX.Y.Z.tar.gz
# Env: CASAN_ARTIFACT_SMOKE_RUN_TESTS=1 # platform: npm ci + tests + build
ARCHIVE="${1:-}"
[[ -n "$ARCHIVE" && -f "$ARCHIVE" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=archive_missing path=${ARCHIVE:-unset}" >&2
exit 64
}
ARCHIVE="$(cd "$(dirname "$ARCHIVE")" && pwd)/$(basename "$ARCHIVE")"
WORK="$(mktemp -d)"
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT
tar -xzf "$ARCHIVE" -C "$WORK"
roots=()
while IFS= read -r directory; do
roots+=("$directory")
done < <(find "$WORK" -mindepth 1 -maxdepth 1 -type d -print)
[[ "${#roots[@]}" -eq 1 ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=unexpected_root_count count=${#roots[@]}" >&2
exit 1
}
ROOT="${roots[0]}"
required=(BUNDLE-MANIFEST.txt SHA256SUMS VERSION bin/casan packages/casan-harness)
for path in "${required[@]}"; do
[[ -e "$ROOT/$path" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=required_path_missing path=$path" >&2
exit 1
}
done
(cd "$ROOT" && shasum -a 256 -c SHA256SUMS >/dev/null)
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
CLI_VERSION="$(cd "$ROOT" && ./bin/casan version)"
[[ "$CLI_VERSION" == "casan $VERSION" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=cli_version_mismatch expected=$VERSION actual=$CLI_VERSION" >&2
exit 1
}
(cd "$ROOT" && ./bin/casan help >/dev/null)
STATUS="$(awk '/^status:/ {print $2}' "$ROOT/BUNDLE-MANIFEST.txt")"
BUNDLE="$(awk '/^bundle:/ {print $2}' "$ROOT/BUNDLE-MANIFEST.txt")"
if [[ "$BUNDLE" == "platform" ]]; then
platform_required=(
packages/casan-control-panel/backend/package.json
packages/casan-control-panel/frontend/package.json
Dockerfile.control-panel-api
Dockerfile.control-panel-ui
docker-compose.control-panel.yml
docker-compose.control-panel.local.yml
nginx/control-panel.conf
infra/production/README.md
docs/security/CONTROL_PANEL_AUTH_HARDENING.md
)
for path in "${platform_required[@]}"; do
[[ -e "$ROOT/$path" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=platform_path_missing path=$path" >&2
exit 1
}
done
[[ -f "$ROOT/PREVIEW-INCOMPLETE.txt" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=preview_marker_missing" >&2
exit 1
}
if [[ "${CASAN_ARTIFACT_SMOKE_RUN_TESTS:-0}" == "1" ]]; then
command -v npm >/dev/null 2>&1 || {
echo "ARTIFACT_SMOKE_FAIL reason=npm_missing" >&2
exit 1
}
(cd "$ROOT" && npm ci && npm run console:test && npm run console:build)
fi
fi
echo "ARTIFACT_SMOKE_PASS bundle=$BUNDLE status=$STATUS version=$VERSION extracted=true checksums=true"