feat: prepare CASAN paid PoC release package
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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 を生成せず、失敗理由を記録します。
|
||||||
|
復旧判断とマージ例外の承認は顧客の責任者が行います。
|
||||||
@@ -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 で合意します。
|
||||||
|
この資料は法的助言ではありません。
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# SLA / サポート条件ドラフト(PoC)
|
||||||
|
|
||||||
|
## 適用範囲
|
||||||
|
|
||||||
|
本ドラフトは PoC 期間の導入支援と障害対応の目安です。Production SLA ではありません。
|
||||||
|
|
||||||
|
| 区分 | 例 | 初回応答目標 |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 | CASAN が全 PR で実行不能、証跡が作成不能 | 営業時間内 4 時間 |
|
||||||
|
| P2 | 一部 gate/表示機能の不具合、回避策あり | 営業時間内 1 営業日 |
|
||||||
|
| P3 | 質問、設定調整、改善要望 | 3 営業日以内 |
|
||||||
|
|
||||||
|
## 連絡とエスカレーション
|
||||||
|
|
||||||
|
顧客の PoC 責任者と提供者の技術窓口をキックオフ時に指定します。P1 の場合、run ID、発生時刻、影響範囲、表示された理由を添えて連絡します。
|
||||||
|
|
||||||
|
## 除外
|
||||||
|
|
||||||
|
顧客ネットワーク、顧客提供モデル、Gitea 自体、外部クラウド障害、未承認の構成変更は本ドラフトの保証対象外です。
|
||||||
@@ -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によるソフトウェア開発を、セキュリティ・品質・コスト・監査証跡の観点から統制するプラットフォーム)
|
||||||
+17
@@ -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"
|
||||||
|
}
|
||||||
+41
@@ -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._
|
||||||
+6
@@ -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"
|
||||||
|
}
|
||||||
+9
@@ -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
|
||||||
|
}
|
||||||
+6
@@ -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"
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"harness": "H4-security",
|
||||||
|
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||||
|
"records": 186,
|
||||||
|
"by_status": {
|
||||||
|
"blocked": 38,
|
||||||
|
"pass": 148
|
||||||
|
},
|
||||||
|
"blocked": 38
|
||||||
|
}
|
||||||
+8
@@ -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
|
||||||
|
}
|
||||||
+9
@@ -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"
|
||||||
|
}
|
||||||
+6
@@ -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"
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47
|
||||||
+8
@@ -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"
|
||||||
|
}
|
||||||
+28
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+168
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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) 🔌
|
||||||
|
|
||||||
|
|||||||
Generated
+5
@@ -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,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 <run-id></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>;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)"
|
||||||
|
|||||||
@@ -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}`);
|
||||||
Executable
+84
@@ -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"
|
||||||
Reference in New Issue
Block a user