Merge pull request 'feat: prepare CASAN paid PoC release package' (#9) from codex/paid-poc-release-readiness into main

Reviewed-on: http://161.33.139.73:3000/admin/casan5/pulls/9
This commit is contained in:
admin
2026-07-18 02:30:09 +00:00
79 changed files with 6774 additions and 56 deletions
+1
View File
@@ -15,6 +15,7 @@ jobs:
CASAN_CI_RUN_FRONTEND: "1"
CASAN_CI_RUN_BACKEND: "1"
CASAN_CI_RUN_INFRA_LAB: "0"
CASAN_CI_RUN_CONTROL_PANEL: "1"
CASAN_REQUIRE_LIVE_SCANNERS: "1"
CASAN_TOOLCHAIN_TRUSTED_DIRS: /usr/bin:/bin:/usr/local/bin
steps:
+7 -6
View File
@@ -22,6 +22,9 @@ jobs:
frontend-tests:
name: "Frontend Tests (H3 gate)"
runs-on: ubuntu-latest
# A stalled self-hosted runner must fail the gate visibly instead of keeping
# every dependent security/deploy job queued indefinitely.
timeout-minutes: 15
defaults:
run:
working-directory: .
@@ -33,14 +36,12 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install frontend dependencies
run: npm ci -w frontend
run: npm ci -w @ainative-okr/frontend
- name: Run Vitest (16 tests)
run: npm test -w frontend
run: npm test -w @ainative-okr/frontend
# ──────────────────────────────────────────────────────────────────────────
# Job 2 — CASAN Security Gate + Vault KMS signing (H4/H5/H2/H6/H7)
@@ -51,6 +52,7 @@ jobs:
name: "CASAN Security Gate + Vault KMS (H4/H5)"
runs-on: ubuntu-latest
needs: [frontend-tests]
timeout-minutes: 30
defaults:
run:
working-directory: .
@@ -66,8 +68,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install test tools
run: |
@@ -148,6 +148,7 @@ jobs:
runs-on: [ci-runner]
needs: [frontend-tests, security-gate]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
timeout-minutes: 35
defaults:
run:
working-directory: .
+17
View File
@@ -35,3 +35,20 @@ jobs:
- name: Run CASAN harness gate
working-directory: .
run: bash packages/casan-harness/scripts/bash/ci-harness-gate.sh
- name: Build and verify Evidence Pack for this CI run
if: always()
working-directory: .
run: |
set -euo pipefail
RUN_ID="gitea-${GITHUB_RUN_ID:-local}-${GITHUB_SHA:-$(git rev-parse --short HEAD)}"
bash packages/casan-harness/scripts/bash/evidence-pack.sh pack "$RUN_ID"
bash packages/casan-harness/scripts/bash/evidence-pack.sh verify-pack "$RUN_ID"
- name: Publish Evidence Pack
if: always()
uses: actions/upload-artifact@v3
with:
name: casan-evidence-pack-${{ github.run_id }}
path: docs/output/casan/evidence-packs/
retention-days: 90
+12
View File
@@ -18,6 +18,7 @@ jobs:
env:
CASAN_CI_RUN_FRONTEND: "0"
CASAN_CI_RUN_BACKEND: "0"
CASAN_CI_RUN_CONTROL_PANEL: "1"
CASAN_CI_RUN_INFRA_LAB: "0"
CASAN_CI_STEP_TIMEOUT_SEC: "1200"
steps:
@@ -39,6 +40,9 @@ jobs:
echo "tag=$TAG version-file=$FILE_VER"
[ "$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)
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)"
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
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+12
View File
@@ -0,0 +1,12 @@
CASAN Proprietary Commercial License
Copyright (c) CASAN copyright owner. All rights reserved.
This repository and its CASAN-authored source code, documentation, scripts,
and release artifacts are proprietary. No permission is granted to use, copy,
modify, distribute, sublicense, host, or create derivative works except under
an executed written agreement with the copyright owner.
Third-party components remain subject to their own licenses. See
docs/commercial/THIRD_PARTY_SOFTWARE.json and
docs/commercial/LICENSE_AND_IP_DECISION.md.
+1
View File
@@ -2,6 +2,7 @@
"name": "@ainative-okr/backend",
"version": "1.0.0",
"private": true,
"license": "UNLICENSED",
"type": "module",
"scripts": {
"build": "prisma generate && tsc -p tsconfig.build.json",
+1
View File
@@ -2,6 +2,7 @@
"name": "@ainative-okr/frontend",
"version": "1.0.0",
"private": true,
"license": "UNLICENSED",
"type": "module",
"scripts": {
"dev": "vite",
@@ -0,0 +1,44 @@
# CASAN Commercial License and IP Decision
## Decision
CASAN is offered as **proprietary commercial software**. No rights to use,
copy, modify, distribute, host, or sublicense CASAN are granted except under a
written evaluation, PoC, subscription, or enterprise agreement signed by the
copyright owner and the customer.
For a paid PoC, the default grant is a non-exclusive, non-transferable,
time-limited right to deploy the delivered CASAN bundle in the customer’s
approved environment for the agreed evaluation scope. Production use, managed
service operation, redistribution, and source access require separate terms.
This decision applies to CASAN-authored material only. It does not override
third-party license obligations.
## Release controls
1. Regenerate `THIRD_PARTY_SOFTWARE.json` from the committed lockfile before
each paid-PoC or release candidate: `node scripts/generate-license-inventory.mjs`.
2. Review every `UNKNOWN`, copyleft, source-available, model, dataset, font,
icon, container-base-image, or copied-script license before shipment.
3. Include required notices and source-offer material in the release bundle
whenever a dependency license requires it.
4. Record the review owner, release version, decision, and exceptions in the
release Evidence Pack or legal review record.
5. Do not claim ownership of customer source code, prompts, or Evidence Pack
data; ownership and permitted processing are defined by the customer
agreement and data-processing terms.
## Known scope boundary
The generated inventory is complete for root-lockfile npm packages only. It is
not proof that Python tooling, operating-system packages, Docker base images,
models, datasets, or bundled assets are cleared. Those sources are release
blockers until separately inventoried and approved.
## Required legal review before external sale
Confirm copyright ownership/assignment for CASAN-authored code, trademark use,
customer contract templates, export-control applicability, open-source notices,
and country-specific privacy terms. This document records the product decision;
it is not a substitute for counsel.
+16
View File
@@ -0,0 +1,16 @@
{
"schema_version": "1.0",
"purpose": "Manual license-review findings for packages whose committed lockfile does not contain license metadata.",
"exceptions": {
"busboy@1.6.0": {
"reviewed_license": "MIT",
"evidence": "https://github.com/mscdex/busboy/blob/master/LICENSE",
"review_status": "requires-release-notice"
},
"streamsearch@1.1.0": {
"reviewed_license": "MIT",
"evidence": "https://github.com/mscdex/streamsearch/blob/master/LICENSE",
"review_status": "requires-release-notice"
}
}
}
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
# APPI Data Processing and Cross-Border Assessment
## Status and use
This is a customer-specific assessment template for CASAN deployments in Japan.
It records facts needed for an APPI review; it does **not** claim that CASAN or
a customer deployment is APPI-compliant. The customer’s privacy/legal owner
must approve the completed version before production processing begins.
## Processing inventory
| Data category | Typical source | CASAN purpose | Storage / recipient | Default retention | Deployment decision required |
|---|---|---|---|---|---|
| Source code and diffs | Git repository, AI coding tool | policy/test/security evaluation; Evidence Pack | customer workspace and Evidence Pack store | customer policy | whether any code may leave customer network |
| Prompts and chat content | Control Panel / AI tool | governed response, routing, audit | customer runtime; selected model provider only when configured | customer policy | provider, country/region, model and allowed fields |
| Secrets / credentials | source, logs, environment | detection/blocking only | should be masked; must not be placed in Evidence Pack | not retained intentionally | scanner/exception procedure |
| Developer identifiers | IdP claims, audit record | RBAC, accountability, approval trace | customer identity/audit store | customer policy | legitimate purpose, access roles |
| Telemetry and costs | model/provider and runner | FinOps, reliability, anomaly detection | customer telemetry store / selected provider | customer policy | provider API and export destination |
| Evidence Pack metadata | CASAN gates and manifests | auditability and proof | customer Evidence Pack storage; optional Gitea artifact | customer policy | retention, immutable-storage setting, artifact access |
## Required deployment register
For every customer project, complete the following fields and attach it to the
PoC or production change record.
| Field | Value / approval |
|---|---|
| Customer data controller / privacy contact | |
| CASAN operator and support contact | |
| Processing purpose and affected projects | |
| Personal information expected in source, prompts or logs | |
| Model providers, model IDs, processing country/region | |
| Subprocessors and service locations | |
| Cross-border transfer assessment / required information | |
| Retention, deletion and export schedule | |
| Evidence Pack, backup and artifact storage location | |
| Incident notification contacts and target timing | |
| Approved masking / secret-scanning policy | |
| Approval date, reviewer and next review date | |
## CASAN control mapping
- H4 / data-exfil guard scans prompts and artifacts before release. A BLOCK is
evidence of detection, not evidence that no sensitive data was ever sent.
- H5 audit records accountable actions and policy changes.
- H6 records execution/cost telemetry. It must be included in the retention
decision because it can identify a user, project, provider or run.
- Evidence Pack stores summaries and hashes; it still needs a retention,
access-control, backup and deletion decision.
- Cloud-model use is opt-in at deployment time. The customer must document what
fields are transmitted and select an allowed provider/region.
## Data-subject and operational procedures
The operator must implement customer-approved procedures for access requests,
correction/deletion/export where applicable, retention expiry, secret leak
investigation, incident triage and vendor/subprocessor changes. A Gitea Actions
artifact is not an immutable long-term retention system by itself.
## Evidence required before a production claim
1. Completed register for each tenant/project and model route.
2. Network/data-flow diagram tested against the deployed configuration.
3. Provider and subprocessor review, including cross-border transfer facts.
4. Retention/deletion test for logs, Evidence Packs, backups and CI artifacts.
5. Incident-response tabletop or drill with named contacts.
6. Review by the customer’s Japanese privacy/legal owner.
## Reference
Use the authoritative Japanese APPI text and Personal Information Protection
Commission guidance for the final legal assessment. English translations and
this template are working aids only; they are not the controlling legal text.
+43
View File
@@ -0,0 +1,43 @@
# CASAN Certification and Audited-Operations Roadmap
## Claim rule
CASAN must not claim a certification, audit result, compliance attestation, or
government-cloud eligibility until the named certification body, assessor or
procurement process has completed it for the relevant legal entity and service
scope.
## Roadmap
| Stage | Objective | Evidence to prepare | Exit evidence |
|---|---|---|---|
| Paid PoC | Answer security questionnaires consistently | architecture, data flow, inventory, SDLC policy, incident/backup/patch procedures, known limitations | customer PoC acceptance; no certification claim |
| Enterprise pilot | Establish repeatable ISMS-like operations | asset/owner register, risk register, access reviews, vulnerability management, change approvals, restore drills, supplier register | internal control review and remediation log |
| ISO/IEC 27001 readiness | Scope an information-security management system | statement of applicability, policies, risk treatment, training, internal audit, management review | accredited certification audit decision |
| Cloud privacy readiness | Assess cloud PII processing where in scope | processor/subprocessor records, retention/deletion controls, encryption/KMS evidence, privacy impact assessment | applicable ISO/IEC 27017/27018 or equivalent assessment decision |
| AI management readiness | Establish AI management-system controls | AI risk register, human oversight, model/provider evaluation, transparency/accountability evidence | ISO/IEC 42001 scope and audit decision, if pursued |
| Government procurement | Meet exact customer/government service requirements | service-specific security evidence, residency, operational audit evidence | ISMAP or other required procurement assessment, if applicable |
## Operating-process baseline
Before external audit, maintain versioned records for access provisioning,
production changes, release provenance, incident handling, patch/vulnerability
management, vendor review, Evidence Pack retention, backup/restore drills,
availability review and management review. Each record must identify an owner,
date, scope and retained evidence.
## Ownership
| Area | Accountable owner |
|---|---|
| Security management system and risk treatment | Security officer |
| Privacy/APPI record | Privacy/legal owner |
| Release, SLSA/provenance and CI evidence | Engineering/release owner |
| Incident/on-call and DR | Operations owner |
| Vendor/model/provider due diligence | Procurement + security owner |
## Current boundary
The repository contains technical controls and templates; it is not evidence of
an audited operating system. A certification roadmap should be revisited after
each customer deployment because scope, service model and data flows change.
@@ -0,0 +1,82 @@
# CASAN — External Security Review / Penetration-Test Scope
This is the procurement and evidence baseline for an **independent** security
review before an enterprise pilot. It is not a penetration-test report and must
not be used as a claim that CASAN has passed an external assessment.
## 1. Engagement boundary
| In scope | Required test focus |
|---|---|
| Control Panel API, UI, reverse proxy and deployment manifests | authentication boundary, RBAC/SoD, tenant/project isolation, input/output validation, HTTP/TLS configuration |
| Harness CLIs and CI workflows | command/path injection, policy bypass, unsafe fallback, artifact/evidence tampering, secret handling and supply-chain controls |
| Governed Chat and approval paths | unauthorized tool execution, approval replay/bypass, audit-chain integrity, cross-tenant replay and data exfiltration |
| Evidence Pack and publication flow | manifest signature/verification, retention/immutability assumptions, artifact access control and chain of custody |
| Production-like deployment | OIDC claim mapping, Vault/KMS access, Object Lock, rootless runner/sandbox, network egress and observability |
Out of scope tests require a written change order. Production data, destructive
load tests, phishing and social engineering are forbidden unless the customer
explicitly authorizes them in writing.
## 2. Prerequisites supplied by CASAN/customer
- A dedicated, non-production test tenant with synthetic code, prompts and
secrets; no customer production source code or personal data.
- DNS name, CA-issued test certificate, enterprise OIDC test application and
least-privilege assessor accounts for viewer, operator, project-admin and
org-admin roles.
- A deployment inventory: image digests, SBOM, workflow commit, configuration
version, data-flow diagram, network/egress policy and known limitations.
- A change window, emergency contact, on-call escalation path and explicit
stop-test authority.
- A current Evidence Pack and commands for `verify-pack`, audit-chain
verification, backup restore drill and release-provenance verification.
## 3. Required attack scenarios
The assessor must cover at least these scenario classes and record the exact
target version/configuration for every finding:
1. Authentication/session handling, OIDC issuer/audience/JWKS failures and
browser-supplied identity-header spoofing.
2. RBAC scope escalation, separation-of-duties bypass and cross-project or
cross-tenant reads/writes/replays.
3. Prompt/tool-output injection, unregistered tool execution, CODEGEN write
attempts and approval-token replay.
4. Secret/PII exfiltration via logs, Evidence Pack, model routing, artifacts,
API responses, error paths and egress configuration.
5. Evidence/audit/trace tampering, clock/retention assumptions and failure of
manifest/provenance verification.
6. Container/sandbox privilege, rootful daemon usage, writable host mounts,
network escape, resource exhaustion and queue starvation.
7. Dependency, image and CI supply-chain validation against the live scanner,
SBOM and digest/provenance assertions.
## 4. Rules of engagement
- Assessors must use named accounts and an agreed source-IP range; all activity
is captured in the H5 audit trail and preserved in a review Evidence Pack.
- Do not alter production state, delete artifacts, rotate real keys, or bypass
legal/contractual third-party rate limits.
- Critical findings trigger immediate notification to the security contact;
CASAN may engage the kill switch while preserving evidence.
- Retesting is required for every Critical/High finding and for any control
used as an enterprise procurement claim.
## 5. Acceptance evidence
The independent assessor must deliver a dated, signed report with scope,
methodology, target versions, findings/CVSS or agreed severity, reproduction
evidence, limitations and retest results. CASAN must retain:
| Evidence | Owner |
|---|---|
| Signed statement of work and rules of engagement | Security owner |
| Report plus finding/remediation register | Security + engineering owners |
| Retest attestation for remediated findings | Assessor |
| Evidence Pack and audit export for the assessment window | Release/security owner |
| Risk acceptance signed by accountable business owner for any deferred finding | Customer/security owner |
Enterprise-pilot readiness requires no unresolved Critical findings. Any
unresolved High finding needs a time-bounded, documented risk acceptance; this
does not make the platform "pentest certified".
@@ -0,0 +1,15 @@
# Tenant Isolation Review Packet
Run the read-only pre-audit against the deployed tenant state root and retain
the JSON output with the deployment evidence:
```bash
python3 packages/casan-harness/scripts/bash/tenant-isolation-review.py \
--root /srv/casan/state/tenants --out tenant-isolation-review.json
```
The tool checks partition permissions, unexpected entries, forbidden symlinks,
tenant key permissions and reused local tenant-key material. A non-zero result
blocks promotion until findings are resolved. It does not assess identity
provider claims, database row-level controls, network isolation, application
authorization logic or cloud KMS policy; those must be assessed independently.
+32
View File
@@ -0,0 +1,32 @@
# 製品概要
## 提供価値
CASAN は、既存の AI コーディングツールを置き換える製品ではありません。
開発者、AI モデル、リポジトリ、CI の間に配置される独立した統制レイヤーです。
AI が生成・提案した変更を、ポリシー、テスト、セキュリティ検査、承認、監査証跡で確認し、根拠を Evidence Pack として残します。
## PoC の対象ユースケース
「Claude Code、Codex、Copilot 等を利用する開発チームで、AI が関与した変更を
merge 前に検査し、誰が何を行ったかを証明する。」
PoC では、1〜2 プロジェクト、1 つの Git リポジトリ連携、1 つの Domain Pack を対象とします。
## 提供範囲(CASAN Core + Control Center Lite)
- H1〜H7 のハーネス実行、アクションゲート、セキュリティ検査
- Gitea CI からの検査起動と PASS/BLOCK 判定
- Evidence Pack の作成、ハッシュ manifest の検証、Control Panel での閲覧
- 監査ログ、ツール呼出記録、コスト/トークンの可視化
- 承認が必要な変更を apply 前に停止するワークフロー
## PoC では提供しないもの
- 高可用性、RPO/RTO を伴う本番運用
- 本番用 SSO/OIDC、顧客 CA、マネージド運用
- KMS 必須運用、S3 Object Lock、外部ペネトレーションテスト
- GitHub/GitLab/Jenkins の本番コネクタ保証
- 自律エージェントによる無承認の変更適用
これらは Enterprise Pilot / Production の後続条件です。
@@ -0,0 +1,30 @@
# システム構成・データフロー
## 推奨配置
PoC では顧客管理のネットワーク内、または顧客クラウドアカウント内への self-hosted 配置を推奨します。
```text
Developer / AI coding tool
│ patch / PR
▼
Gitea ──► CASAN Runner ──► test / security / policy gates
│
├── Evidence Pack(manifest + reports)
└── Control Panel(run、承認、証跡表示)
```
## データフロー
1. 開発者が既存の AI ツールで変更を作成し、PR を作成します。
2. Gitea workflow が CASAN を呼び出します。
3. CASAN は許可されたワークスペースでポリシー、テスト、セキュリティを実行します。
4. 判定、実行記録、ハッシュを Evidence Pack に保存します。
5. Control Panel は Evidence Pack の manifest を再検証して表示します。
6. PASS でない変更は、顧客定義のマージ/承認ルールに従って停止します。
## データの扱い
ソースコード、ログ、Evidence Pack は顧客環境内に保存する構成を基本とします。
クラウドモデルへ送信するデータは、顧客が選択したモデル設定とポリシーに依存します。
PoC 開始時に、送信対象、保存場所、保持期間、マスキング方針を Data Flow Sheet に記録します。
@@ -0,0 +1,28 @@
# 導入・運用・制限事項
## 導入手順(PoC)
1. 顧客と対象リポジトリ、担当者、承認ルールを確定します。
2. CASAN bundle を顧客環境へ配置し、Docker Compose または提供手順で起動します。
3. Gitea workflow に CASAN gate を接続します。
4. Domain Pack と閾値を対象プロジェクトに合わせます。
5. テスト PR を用いて PASS/BLOCK/Evidence Pack を確認します。
6. 運用開始後、週次で KPI と誤検知をレビューします。
## 日常運用
- 開発者: PR の CASAN verdict と必要な修正を確認します。
- QA/セキュリティ: ブロック理由、Evidence Pack、例外承認を確認します。
- 管理者: ポリシー変更、ロール、バックアップ、障害連絡を管理します。
## 既知の制限
- Evidence Pack の署名は、PoC 環境では鍵未設定の場合に unsigned となります。
- テレメトリ完全性未検証またはコストスパイクがある場合、pack は certified になりません。
- 実際の隔離強度は配置する runner/container 設定に依存します。Production 用 hardened sandbox は PoC の範囲外です。
- Enterprise SSO、HA、DR、外部監査は PoC 完了条件には含みません。
## 障害時
モデル、runner、検査、または証跡生成が失敗した場合、CASAN は PASS を生成せず、失敗理由を記録します。
復旧判断とマージ例外の承認は顧客の責任者が行います。
+29
View File
@@ -0,0 +1,29 @@
# PoC計画(8〜12週間)
## 成功条件
- 対象リポジトリで CASAN gate が PR ごとに実行されること
- PASS/BLOCK の理由と Evidence Pack を担当者が確認できること
- AI が関与した変更のテスト/セキュリティ/承認記録を追跡できること
- 顧客と合意した KPI を最終報告書で提示できること
## 進め方
| 期間 | 内容 | 成果物 |
|---|---|---|
| 1〜2週 | キックオフ、対象選定、リスク確認 | PoC 設計、Data Flow Sheet |
| 3〜4週 | 環境配置、Gitea 連携、Domain Pack | 導入記録、テスト PR |
| 5〜8週 | 実運用、閾値調整、Evidence 確認 | 週次 KPI、課題一覧 |
| 9〜12週 | 最終評価、次フェーズ判断 | PoC 結果報告、Pilot 提案 |
## KPI(開始時に基準値を合意)
- CASAN が実行された PR 比率
- ブロックされたセキュリティ/ポリシー違反数
- Evidence Pack が完全性検証に成功した比率
- 誤検知レビュー件数と解決時間
- AI 関連の変更に対する承認・追跡可能比率
## 顧客に必要な協力
対象リポジトリ、テスト環境、Gitea 管理者、セキュリティ/QA 窓口、承認ルール、及びクラウドモデル利用時のデータ送信判断を提供してください。
@@ -0,0 +1,26 @@
# セキュリティ・責任分界
## CASAN が提供する統制
- AI 出力/ツール実行に対するポリシーとセキュリティ検査
- 実行・判定・承認の監査記録
- Evidence Pack の SHA-256 manifest 検証
- 失敗時に PASS を出さない fail-closed 動作
## 顧客の責任
- ネットワーク、ホスト、ID、リポジトリ権限、バックアップの管理
- モデル provider と送信データの選定
- 個人情報、秘密情報、保持期間、例外承認の決定
- PoC 環境のアクセス権とログ閲覧権の管理
## 提供者の責任
- 合意した CASAN bundle、導入支援、障害一次切り分けを提供すること
- 既知の制限、重大な脆弱性、PoC スコープ外の事項を明示すること
- 顧客データを合意外の目的に使用しないこと
## 事前確認事項
APPI を含む適用法令、個人情報の有無、海外移転、subprocessor、データ削除/export、インシデント通知窓口は契約・Data Flow Sheet で合意します。
この資料は法的助言ではありません。
+19
View File
@@ -0,0 +1,19 @@
# SLA / サポート条件ドラフト(PoC)
## 適用範囲
本ドラフトは PoC 期間の導入支援と障害対応の目安です。Production SLA ではありません。
| 区分 | 例 | 初回応答目標 |
|---|---|---|
| P1 | CASAN が全 PR で実行不能、証跡が作成不能 | 営業時間内 4 時間 |
| P2 | 一部 gate/表示機能の不具合、回避策あり | 営業時間内 1 営業日 |
| P3 | 質問、設定調整、改善要望 | 3 営業日以内 |
## 連絡とエスカレーション
顧客の PoC 責任者と提供者の技術窓口をキックオフ時に指定します。P1 の場合、run ID、発生時刻、影響範囲、表示された理由を添えて連絡します。
## 除外
顧客ネットワーク、顧客提供モデル、Gitea 自体、外部クラウド障害、未承認の構成変更は本ドラフトの保証対象外です。
@@ -0,0 +1,49 @@
# 本番 SLA・サポート・エスカレーション案
## 位置付け
本書は Enterprise Production 契約に向けたドラフトです。実際の SLA、
サポート時間、サービスクレジット、通知義務は、顧客環境・運用体制・契約書で
合意した値だけが有効です。
## 重要度と初回応答目標
| 重要度 | 例 | 初回応答目標 | 更新頻度 |
|---|---|---:|---:|
| S1 | 全体停止、監査証跡の完全性失敗、重大なデータ漏えい疑い | 1 時間以内(24x7 を契約した場合) | 2 時間ごと |
| S2 | 主要 project の gate/approval/Evidence Pack が使えない | 4 時間以内(契約サポート時間内) | 1 営業日ごと |
| S3 | 一部機能の障害、回避策あり、誤検知の調整 | 1 営業日以内 | 3 営業日ごと |
| S4 | 問い合わせ、改善要望、設定相談 | 3 営業日以内 | 合意時 |
## エスカレーション手順
1. 顧客は severity、時刻、tenant/project、run ID、影響範囲、Evidence Pack
または manifest hash をサポート窓口へ送ります。
2. 提供者は受領、再現性、影響範囲を確認し、S1/S2 は incident owner を指定します。
3. S1 では、顧客承認者と連携して kill-switch、モデル route 停止、Evidence
preservation の要否を判断します。CASAN は根拠なく PASS に切り替えません。
4. 復旧後、原因、影響、復旧、再発防止、証跡を含む報告書を合意した期限で提出します。
## 役割分担
| 役割 | 責任 |
|---|---|
| 顧客 Service Owner | ビジネス影響、例外承認、顧客向け通知 |
| 顧客 Security/Privacy Owner | データ漏えい評価、規制・通知判断 |
| CASAN Incident Owner | 技術調査、Evidence 保全、復旧計画 |
| CASAN On-call | 受付、初動、escalation、状況更新 |
| Cloud/Model Provider | 顧客が契約する外部サービスの障害対応 |
## SLA 測定の前提
測定対象、監視、可用性、RPO/RTO、planned maintenance、顧客ネットワークや
外部モデルの除外条件を契約前に定義します。HA/DR と managed on-call が未検証の
環境では、24x7 または uptime の本番 SLA を提示してはいけません。
## 必要な運用証拠
- on-call rota と backup contact
- Alert webhook / dead-letter の日次確認
- 月次 availability・incident・Evidence verification report
- restore drill と disaster-recovery drill の記録
- support ticket のアクセス制御と保持期間
+21
View File
@@ -0,0 +1,21 @@
# 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 中の支援条件(ドラフト) |
| [07_本番SLA・サポート・エスカレーション案](07_本番SLA・サポート・エスカレーション案.md) | Production 契約向けの運用 draft |
製品名(提案): **CASAN AI-SDLC Governance Platform**
(生成AIによるソフトウェア開発を、セキュリティ・品質・コスト・監査証跡の観点から統制するプラットフォーム)
本番移行時には、顧客ごとの個人情報・越境移転・保持期間を
[`APPI_DATA_PROCESSING.md`](../compliance/APPI_DATA_PROCESSING.md) で記録し、
法務/プライバシー責任者の承認を得ます。
@@ -0,0 +1,17 @@
{
"files": {
"benign-fp-report.json": "52d3911e7b539f07391e187e3212f198f25e55d08396e2ab0e75d9db3178848b",
"decision-log.md": "6975cabd29baff1fbc1c9ceca8a44323832ecc28eafe6bdd30fbe94981563d70",
"h1-context-report.json": "64a1bbcdf38672eb1efcb8742e258850709297bd848a443fcf11b94b0f165d9d",
"h2-tool-audit.json": "095b369ae21d47ecfcbc38387f3255118bc32a4c28327f19b27e0bfcd7c945ae",
"h3-eval-scorecard.json": "b0e779fb219c4d9d38e55eefb15cd4370b4f0760e0a3bf70b5d22923407c8e47",
"h4-security-report.json": "2828cf13b2f4edba852c9da77e1397b3b44933a3bd6d04c81b4ac96e31026e9d",
"h5-audit-chain-proof.json": "f9918cc45311097be05ab852c36ed99170c65962f8c0f0e71b91d7468f9f2ba4",
"h6-cost-telemetry.json": "e01cc551016f3d4b7d525e11cf9556481cb1376408daca6e2314aba9a3ddd088",
"h7-orchestration-report.json": "9dd45a16004bd705c7307fae6c74975049a156a3dc1e14fc439ad816d5400066",
"redteam-result.json": "341f7985a8cceb2cb3cc7705e644a4407ab27f7203c148abb98788dcca5e5c8e",
"run-summary.json": "16cf96c55fa7b899e7b021b52a88cf3fa160366ab03e2361e431b0639196af29",
"traceability-matrix.json": "011ac8ac6ab008f1048e3bce1e3f7b5b28bc969de7a1574fed86af16e259de27"
},
"manifest_head": "c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47"
}
@@ -0,0 +1,41 @@
{
"generated": "deterministic (semantic OFF)",
"policy": {
"fp_max_pct": 3.0,
"adv_block_min_pct": 95.0,
"critical_block_pct": 100.0
},
"benign": {
"by_lang": {
"en": {
"total": 32,
"false_positives": 0
},
"vi": {
"total": 32,
"false_positives": 0
},
"ja": {
"total": 31,
"false_positives": 0
}
},
"total": 95,
"false_positives": 0,
"false_positive_rate_pct": 0.0,
"examples": []
},
"adversarial": {
"total": 12,
"blocked": 12,
"block_rate_pct": 100.0,
"missed": []
},
"critical": {
"total": 4,
"blocked": 4,
"block_rate_pct": 100.0
},
"within_budget": true,
"breaches": []
}
@@ -0,0 +1,17 @@
# CASAN Evidence Pack — Decision Log
Run: `9534d91f-4518-41c5-a7c4-56e22a56c2a4`
Certified: **False**
Reasons: telemetry_integrity_not_verified, unresolved_cost_spike
## Gate outcomes
- H4 security: 186 records, 38 blocked
- H5 audit chain: AUDIT_CHAIN_VALID anchor=signed last_hash=e715767a0d5ec67a8955c0e8e87262b5ca8c0f5826f1dee6583014ede8032bbe (ok=True)
- H5 telemetry integrity: none (ok=False)
- H6 cost: spike_detected, 56519 provider tokens
- H2 tool audit: 24 records, chain_ok=True
- H3 traceability: ok=True
- Red-team: 16 vectors (block_rate=100.0%)
_Summaries only — no raw secret/PII content is copied into the pack._
@@ -0,0 +1,6 @@
{
"harness": "H1-context",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"context_yaml": true,
"note": "path/artifact validation performed by context-validate.sh at run time"
}
@@ -0,0 +1,9 @@
{
"harness": "H2-tool",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"records": 24,
"denied": 6,
"approved": 5,
"chain_status": "TOOL_AUDIT_VALID anchor=signed last_hash=dda5685f038e171c7fdd8bb36b00793c3de7b14be7295fdfe34fff5e6a03c443",
"chain_ok": true
}
@@ -0,0 +1,6 @@
{
"harness": "H3-eval",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"judge_gate_tests": true,
"note": "judge-gate fail-before/fix cycle proven by phase3-judge-gate-tests.sh"
}
@@ -0,0 +1,10 @@
{
"harness": "H4-security",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"records": 186,
"by_status": {
"blocked": 38,
"pass": 148
},
"blocked": 38
}
@@ -0,0 +1,8 @@
{
"harness": "H5-governance",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"audit_chain": "AUDIT_CHAIN_VALID anchor=signed last_hash=e715767a0d5ec67a8955c0e8e87262b5ca8c0f5826f1dee6583014ede8032bbe",
"audit_chain_ok": true,
"telemetry_integrity": "none",
"telemetry_ok": false
}
@@ -0,0 +1,9 @@
{
"harness": "H6-agentops",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"provider_records": 56,
"metric_records": 40,
"total_provider_tokens": 56519,
"cost_spike_rc": 2,
"cost_spike_status": "spike_detected"
}
@@ -0,0 +1,6 @@
{
"harness": "H7-orchestration",
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"rollback_log": true,
"note": "rollback/fallback/drift proven by adversarial + run-casan4 suites"
}
@@ -0,0 +1 @@
c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47
@@ -0,0 +1,8 @@
{
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"vectors_defined": 16,
"critical_vectors": 4,
"adversarial_block_rate_pct": 100.0,
"critical_block_rate_pct": 100.0,
"note": "block rates from benign-fp-report (deterministic layer); full suites: phase1-track-a + phase2-track-c"
}
@@ -0,0 +1,28 @@
{
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
"pack_version": "1.0-mvp",
"certified": false,
"certification_reasons": [
"telemetry_integrity_not_verified",
"unresolved_cost_spike"
],
"required_gates": [
"H3-traceability",
"H4-security",
"H5-audit-chain",
"H5-telemetry",
"H6-cost",
"benign-fp-budget"
],
"harness_reports": [
"benign-fp-report.json",
"h1-context-report.json",
"h2-tool-audit.json",
"h3-eval-scorecard.json",
"h4-security-report.json",
"h5-audit-chain-proof.json",
"h6-cost-telemetry.json",
"h7-orchestration-report.json",
"redteam-result.json"
]
}
@@ -0,0 +1,168 @@
{
"generated_at": "2026-07-17T16:57:32Z",
"requirements_source": "apps/okr/domain/input/okr-requirement.md",
"mapping_source": "apps/okr/domain/traceability-map.json",
"summary": {
"requirements": 5,
"passed": 5,
"failed": 0,
"symbol_refs": 9,
"symbols_found": 9,
"symbols_missing": 0,
"orphan_mappings": []
},
"matrix": [
{
"id": "FR-01",
"name": "Login",
"status": "PASS",
"code": [
"apps/okr/backend/src/auth/auth.controller.ts",
"apps/okr/backend/src/auth/auth.service.ts",
"apps/okr/frontend/src/pages/Login.tsx",
"apps/okr/frontend/src/hooks/useAuth.tsx"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/auth/auth.service.ts",
"symbol": "AuthService",
"found": true
},
{
"file": "apps/okr/backend/src/auth/auth.service.ts",
"symbol": "login",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-02",
"name": "Create Objective",
"status": "PASS",
"code": [
"apps/okr/backend/src/objectives/objectives.controller.ts",
"apps/okr/backend/src/objectives/objectives.service.ts",
"apps/okr/frontend/src/pages/CreateObjective.tsx",
"apps/okr/frontend/src/schemas/objective.schema.ts"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts",
"apps/okr/frontend/src/__tests__/okr.test.tsx"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "ObjectivesService",
"found": true
},
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "create",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-03",
"name": "Create Key Result",
"status": "PASS",
"code": [
"apps/okr/backend/src/key-results/key-results.controller.ts",
"apps/okr/backend/src/key-results/key-results.service.ts",
"apps/okr/backend/src/key-results/dto/create-key-result.dto.ts"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
"symbol": "KeyResultsService",
"found": true
},
{
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
"symbol": "create",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-04",
"name": "Update Progress",
"status": "PASS",
"code": [
"apps/okr/backend/src/key-results/key-results.controller.ts",
"apps/okr/backend/src/key-results/key-results.service.ts",
"apps/okr/backend/src/key-results/dto/update-progress.dto.ts",
"apps/okr/frontend/src/pages/KeyResultDetail.tsx"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts",
"apps/okr/frontend/src/__tests__/okr.test.tsx"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
"symbol": "updateProgress",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
},
{
"id": "FR-05",
"name": "Dashboard",
"status": "PASS",
"code": [
"apps/okr/backend/src/objectives/objectives.controller.ts",
"apps/okr/backend/src/objectives/objectives.service.ts",
"apps/okr/frontend/src/pages/Dashboard.tsx",
"apps/okr/frontend/src/hooks/useObjectives.ts"
],
"tests": [
"apps/okr/backend/test/services.test.ts",
"apps/okr/backend/test/e2e.test.ts",
"apps/okr/frontend/src/__tests__/okr.test.tsx"
],
"missing_code": [],
"missing_tests": [],
"symbol_refs": [
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "ObjectivesService",
"found": true
},
{
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
"symbol": "list",
"found": true
}
],
"missing_symbols": [],
"missing_lines": []
}
]
}
+13 -7
View File
@@ -11,8 +11,8 @@ contents + maturity: [`packaging/levels.json`](../../packaging/levels.json).
|---|---|:--:|---|
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | H1–H7 harness, security + action gates, evidence pack, audit, cost/telemetry, hardening tests, policy/config defaults, `bin/casan` CLI |
| **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.** |
| **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 |
| **3 — Platform Components** | `casan-platform`, `casan-control-panel` | 🟡 preview | Control Panel, Dashboard, Run History, governed chat MVP and **Evidence Pack Viewer**. Attack Battery Viewer, Gitea evidence publishing, and managed rollout are still pending. |
| **4 — Enterprise / Governed Console** | `casan-enterprise`, `casan-governed-console` | 📋 future | Promotion layer requiring managed deployment, KMS/Object Lock operations, HA/DR/SLA, external review and compliance/support evidence. |
Levels are cumulative: DevKit extends Core, Platform extends DevKit, Enterprise extends
Platform.
@@ -60,8 +60,14 @@ docs/packaging/ # this plan + adoption/CI/domain-pack/gitea/docker g
## Implemented now vs future
- **Implemented now:** Level 1 Core packaging, Level 2 DevKit packaging, release script,
`bin/casan`, templates, docs, all-in-one-demo bundle.
- **Structure + docs only:** Level 3 Platform (dashboard exists; rest scaffolded),
Level 4 Enterprise (RBAC/tenant/KMS/WORM/approval exist in core; governed console not built).
- **Not built in this task (do not assume present):** Governed Chat Console, Prompt Mode
Router, Model Provider Management, Operator/Codegen modes, Agent/Skill Registry, RBAC/
tenant *console* UX.
- **Platform preview:** a shipped preview bundle containing the Control Panel,
governed-chat MVP, and Evidence Pack Viewer. It is explicitly incomplete.
- **Structure + docs only:** Level 4 Enterprise. RBAC/tenant/KMS/WORM/approval
building blocks exist, but no Enterprise artifact is shipped.
- **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.
+18 -4
View File
@@ -4,7 +4,7 @@
> bước tiếp theo cụ thể + cờ phụ-thuộc-hạ-tầng, để **bất kỳ AI/người nào tiếp quản
> 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
> gì phải làm"**. Control **đã implement+test** → xem `CASAN_HARDENING_STATUS.md`.
@@ -30,17 +30,22 @@
| 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.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) 🔌
| # | Hạng mục | Trạng thái | Bước tiếp theo (bản production) |
|---|---|:--:|---|
| T2.1 | KMS mặc định + HSM | 🟡 Docker lab ✅ (Vault Transit) | `infra/local-prod` dựng Vault dev @ `:18200`; `infra-lab verify` chứng minh sign+verify. Prod: Vault/AWS-KMS/CloudHSM + token ngắn hạn qua IdP; bỏ fallback khoá local ở môi trường prod. |
| T2.1 | KMS mặc định + HSM | 🟡 production fail-closed + Docker lab ✅ | `infra/local-prod` dựng Vault dev @ `:18200`; `infra-lab verify` chứng minh sign+verify. `sign-audit-head.sh` nay từ chối local-key fallback khi `CASAN_PROFILE=prod` (SEC-02 test). Còn: Vault/AWS-KMS/CloudHSM thật + token ngắn hạn qua IdP và Transit encryption cho dữ liệu tenant. |
| T2.2 | WORM store thật (S3 Object Lock) | 🟡 Docker lab ✅ (MinIO Object Lock) | MinIO bucket `casan-worm` bật Object Lock COMPLIANCE 1d. Prod: S3 Object Lock/QLDB + trusted timestamp; cần AWS creds + bucket Object-Lock. |
| T2.3 | Dashboard deploy + auth | 🟡 Docker lab ✅ (nginx basic auth + /healthz) | Dashboard container + nginx reverse-proxy @ `:18080`, user/pass lab `casan/casan`; `/healthz` exposed. Prod: host/TLS/OIDC or enterprise auth. |
| T2.4 | Kênh alert managed + on-call | 🟡 Docker lab ✅ (webhook emulator) | `alert-webhook` @ `:19092` nhận live POST. Prod: trỏ `CASAN_ALERT_WEBHOOK` tới Slack/PagerDuty + rota/on-call thật. |
| T2.4 | Kênh alert managed + on-call | 🟡 Docker lab + metrics export ✅ | `alert-webhook` @ `:19092` nhận live POST; Control Panel exports aggregate-only Prometheus text at `/api/v1/metrics` (no trace/tenant/prompt labels). Prod: restrict scrape endpoint at the reverse proxy, trỏ `CASAN_ALERT_WEBHOOK` tới Slack/PagerDuty và thiết lập rota/on-call thật. |
| T2.5 | Billing-API telemetry thật | 🟡 Docker lab ✅ (billing API mock) | `billing-api` @ `:19093/usage`; `provider-usage-fetch.sh` import được provider telemetry. Prod: OpenAI/Anthropic usage API thật + key. |
| T2.6 | Sandbox: rootless/nsjail + base image CI | 🟡 (container isolation live via Docker) | Thêm profile nsjail/bubblewrap cho Linux CI (không cần Docker daemon); hardened base image tối thiểu. `sandbox-container.sh` đã có bản Docker. |
| T2.6 | Sandbox: rootless/nsjail + base image CI | 🟡 hardened Docker runner + test | `sandbox-container.sh` forces a non-root workload user and production refuses a rootful daemon; network, root filesystem, capabilities and quotas remain locked down. Remaining: provision rootless Docker on the customer runner, image allowlist/signature policy, and optional nsjail/bubblewrap profile for daemonless Linux CI. |
| T2.7 | Backup/restore + restore drill | 🟡 automated + test | `state-backup.sh` creates manifest/hash-bound state snapshots, requires encryption key in prod, restores only to an empty explicit directory; `phase-state-backup` is in CI. Remaining: customer object-store replication, retention/RPO/RTO, and a retained production restore-drill record. |
| T2.8 | Upgrade/rollback compatibility | 🟡 automated + test | `upgrade-compatibility.sh` is fail-closed and permits only explicit matrix rules; the 1.0.x patch/rollback rule requires provenance, backup, artifact smoke and post-deploy Evidence Pack verification. Remaining: a reviewed migration rule for every future minor/major release and a retained live rollout/rollback record. |
| T2.9 | Gitea Evidence Pack publishing | 🟡 workflow implemented | `harness-ci.yml` creates, verifies, and uploads `casan-evidence-pack-<run>` on every PR/push workflow. Remaining: first hosted run must be retained and checked in the Gitea UI; API token/PR creation is still needed from this environment to initiate that evidence. |
| T2.10 | External security review / pentest | 🟡 scope ready, assessment not started | `docs/compliance/EXTERNAL_SECURITY_REVIEW_SCOPE.md` defines the independent engagement boundary, attack scenarios, rules of engagement and required signed/retest evidence. Remaining: procure an assessor, execute against a dedicated environment, remediate findings and retain the signed report. |
## TIER 3 — Platform plans (INDEX 01–12, việc lớn nhiều phiên) 📋
@@ -62,6 +67,15 @@
| **17 Loop Engineering** | � T1–T6 done+test (offline) | **Agentic Loop Governance** — đủ 5 primitive + orchestrator (97/0 WSL, nối CI). T1 **Governor** (`loop-governor.py`; deny-by-default, no/corrupt policy→strict/HALT, on_exceed halt/escalate) 15/0; T2 **Convergence** (`loop-convergence.py`; repeat/thrash→OSCILLATING, flat→STALLED, fail-closed) 15/0; T3 **Verify Contract** (`loop-gate.py`; H4→DENY, unmet→FAIL, correction bounded→ESCALATE, no self-declared DONE) 20/0; T4 **Trace/Replay** (`loop-trace.py`; append-only hash-linked, edited→BREAK, tampered artifact→replay DRIFT) 16/0; T5 **Meta-loop** (`loop-metaloop.py`; propose≠apply, SoD, loosen>org_ceiling refused, apply qua governed CP store→đổi thật ceiling + rollback) 15/0; T6 **Orchestrator** (`loop-run.sh`; gate→governor→convergence→trace/turn, secure-by-default opt-out, nén giữa vòng) 16/0. State qua `CASAN_LOOP_STATE_ROOT` (repo `.specify/state` sạch). **Còn (infra):** T4 KMS-anchor head (A7 Vault), T6 widget Command Center (17.22, C5), live H3-judge. Chi tiết: `CASAN_PLAN_17_LOOP_ENGINEERING.md`. |
| **16 Security audit remediation** | � P0/P1/P2 phần lớn done+test | **Remediation đã thực thi:** 28 SEC suite (151/0 WSL, nối `ci-harness-gate.sh`). Done: SEC-01..10, 12, 13, **14** (model-digest bỏ env-override ở prod/strict), 15, 16..21, **22** (trusted-time JWT `exp` ARCH-06 + tag proposal nguồn-không-tin ARCH-08), **26** (stored/second-order injection scan), 27..30, **23 Phase 1–5 offline** (multi-tenant: tenant-store+guard · per-tenant CP/audit/telemetry · RBAC data-boundary · tenant kill-switch/quota · ký registry · crypt at-rest per-tenant), **24 offline** (image digest-pin + ký workflow), **25 offline** (artifact attestation tested==deployed); **SEC-11 gộp vào SEC-17** (`CASAN_PROFILE=prod` enforce-by-default). **Còn 📋 planned (hạ tầng/process):** SEC-22 ARCH-10 (attestation ngoài) · SEC-23 23.11 (crypt qua Vault Transit) · **SEC-24 còn** (live CVE/OSV + scan image thật — offline image-pin/ký-workflow đã done) · **SEC-25 còn** (signed-commit enrollment + SLSA chain — offline artifact-attestation đã done). Chi tiết: `CASAN_PLAN_16` §0a/§2d. |
| **18 Chat Console** | ✅ **MVP-0 + MVP-1 + MVP-2 + MVP-3 + Track M done+test** | **Governed Chat Console** (cắt lát MVP chống lan man). **MVP-0 Ask CASAN read-only DONE**: `prompt-mode-router.py`, `chat-readonly.py`, `chat-session.schema.json`, H4 input/output scan, H5 chat audit hash-chain, H6 token telemetry, answer kèm evidence sources. **MVP-1 Operator DONE**: registered actions through `action-gate`, no free-command, action artifacts with provenance. **MVP-2 Track 4 DONE**: `agent-registry.yaml`, `chat-agent-resolver.py`, `chat:select_agent` RBAC, delegation hold, tool allowlist BLOCK, Control Panel agent picker, CODEGEN draft-only through `artifact-scan` + Plan-17 loop certification. **MVP-2 OPERATOR Track 5/6 DONE**: `chat-turn.py` certifies UNCERTIFIED draft through Plan-17 `loop-run.sh` + trace verify/replay before side-effect release. **Track 8.1/8.2 DONE**: `chat-replay.py` verifies chat chain, evidence artifact hash, and OPERATOR/CODEGEN loop replay; Control Panel `GET /api/v1/chat/replay`. **Track 8.3 DONE**: Command Center `chat_loop` widget reads chat audit/replay evidence, loop ticker, budget gauge, and click-through evidence drawer. **Track 8.4 DONE**: `REQUIRES_APPROVAL` chat turns become pending `chat.escalate` approvals, SoD/reason enforced, strict/fake JWT denied before mutation. **Track 9 DONE**: non-default tenant chat state is partitioned by `tenant-store.sh`, explicit cross-tenant replay paths are denied, encrypted audit snapshots are written via `tenant-crypt.sh`, tenant kill-switch/quota are isolated. Test: chat suites **59/0** (`phase-chat-prompt-router` 9/0, `phase-chat-readonly` 5/0, `phase-chat-session-audit` 3/0, `phase-chat-operator` 8/0, `phase-chat-agent-select` 8/0, `phase-chat-pipeline` 4/0, `phase-chat-stream-hold` 2/0, `phase-chat-replay` 4/0, `phase-chat-approval` 4/0, `phase-chat-codegen` 4/0, `phase-chat-tenant` 8/0), Control Panel **29/0** + build xanh. **Track M (2026-07-09) DONE**: model-optional grounded synthesis — read-only Ask CASAN tổng hợp câu trả lời tự nhiên có citations qua `model-router.sh` khi `CASAN_CHAT_MODEL_MODE=model` (config `model-providers.yaml`), offline-first (mặc định deterministic), fail-safe fallback, cloud→ép preflight PII-guard, model output vẫn qua H4 (secret→DENY), H6 real token/cost. `phase-chat-model-synthesis` **7/0** (chat suites **66/0**), nối `ci-harness-gate.sh`, UI badge model/deterministic. **Uplift items 1–5 (2026-07-09b) DONE**: (1) ANALYSIS mode reasoning/compare; (2) multi-turn memory per-chat/tenant nén Plan-08; (3) streaming draft UNCERTIFIED→final (API `/chat/ask/stream` + UI toggle); (4) CODEGEN full model-router draft (artifact-scan + loop-cert); (5) `chat-cloud-smoke.sh` (SKIP nếu thiếu key). `phase-chat-advanced` **8/0**, chat suites **74/0**. Còn: cloud live-smoke với key thật; token-level SSE (hiện 2-pha). |
## Production-enterprise preparation (P2)
| Hạng mục | Trạng thái | Bước tiếp theo |
|---|:--:|---|
| APPI/data-processing documentation | 🟡 template ready | `docs/compliance/APPI_DATA_PROCESSING.md` records the customer/project/model data register, retention, transfer and incident-review evidence. Complete and approve it with the Japanese privacy/legal owner for each production tenant. |
| Japanese SLA/support/escalation | 🟡 draft ready | `docs/jp-poc/07_本番SLA・サポート・エスカレーション案.md` defines severity, ownership and evidence expectations. Finalize service hours, credits, uptime, RPO/RTO and 24x7 coverage only after managed operations/DR are proven. |
| Certification and audited operations roadmap | 🟡 roadmap ready | `docs/compliance/CERTIFICATION_ROADMAP.md` maps PoC → pilot → ISO/cloud privacy/AI management/government procurement evidence. It expressly forbids claims before the relevant accredited audit or procurement process completes. |
| Tenant isolation pre-audit | 🟡 CLI + test ready | `tenant-isolation-review.py` checks filesystem partition permissions, symlink escapes and local key separation; retain its output for the independent review. It does not replace an independent identity/database/network isolation assessment. |
---
## Trần điểm & điều kiện lên "Strong (81+)"
+30
View File
@@ -63,3 +63,33 @@ sudo systemctl daemon-reload
sudo systemctl enable --now casan-audit-anchor.timer
systemctl list-timers casan-audit-anchor.timer
```
## Backup and restore drill
Back up CASAN runtime state using `state-backup.sh`. In production the command
requires `CASAN_BACKUP_PASSPHRASE_FILE`; store that file via the customer secret
manager, not in the repository. Restore refuses a non-empty target directory,
so a drill is non-destructive by default.
```bash
export CASAN_PROFILE=prod
export CASAN_BACKUP_PASSPHRASE_FILE=/run/secrets/casan-backup-passphrase
export CASAN_BACKUP_ROOT=/srv/casan/backups
bash packages/casan-harness/scripts/bash/state-backup.sh create "$(date -u +%Y%m%dT%H%M%SZ)"
bash packages/casan-harness/scripts/bash/state-backup.sh verify "<backup-id>"
bash packages/casan-harness/scripts/bash/state-backup.sh restore "<backup-id>" --to /srv/casan/restore-drill
```
Perform and retain a restore drill before enabling write actions. A production
runbook must define retention, off-site replication, an owner, RPO/RTO targets,
and approval for any restore into a live state directory.
## Hardened runner requirement
Production sandbox execution requires a **rootless Docker daemon** plus a
non-root container process (`65532:65532` by default). CASAN rejects a rootful
daemon when `CASAN_PROFILE=prod`; the workload has no network, read-only root
filesystem, no Linux capabilities, no-new-privileges, bounded PID/memory/CPU,
and only its explicit workspace bind mount. Provision the workspace so the
non-root runner identity can write it.
+18
View File
@@ -0,0 +1,18 @@
# Upgrade and Rollback Runbook
The compatibility gate is mandatory before every deployment or rollback:
```bash
bash packages/casan-harness/scripts/bash/upgrade-compatibility.sh check \
--from 1.0.0 --to 1.0.1
```
Only an explicit rule in `compatibility-matrix.json` can permit a version pair.
Before rollout, verify provenance, create and verify a state backup, smoke the
extracted artifact, deploy immutable image digests, and verify an Evidence Pack
after deployment. A rollback requires the same gate with `--rollback`, plus an
approved change record and a verified backup.
Do not infer compatibility from matching image tags, passing unit tests, or
successful container startup. Add and review a new matrix entry before any
cross-minor or cross-major migration.
@@ -0,0 +1,18 @@
{
"schema_version": "1.0",
"product": "CASAN",
"rules": [
{
"from": "1.0.x",
"to": "1.0.x",
"policy_schema": "1",
"evidence_pack_schema": "1.0-mvp",
"rollback": "supported",
"notes": "Patch-level upgrades only. Back up runtime state and verify the candidate artifact before rollout."
}
],
"unsupported": [
"Cross-major upgrades require a reviewed migration entry before deployment.",
"Downgrades that change policy or Evidence Pack schema are denied until an explicit rollback entry is added."
]
}
+5
View File
@@ -5,6 +5,7 @@
"packages": {
"": {
"name": "ainative-okr-casan5",
"license": "UNLICENSED",
"workspaces": [
"apps/okr/backend",
"apps/okr/frontend",
@@ -15,6 +16,7 @@
"apps/okr/backend": {
"name": "@ainative-okr/backend",
"version": "1.0.0",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^10.4.20",
"@nestjs/core": "^10.4.20",
@@ -42,6 +44,7 @@
"apps/okr/frontend": {
"name": "@ainative-okr/frontend",
"version": "1.0.0",
"license": "UNLICENSED",
"dependencies": {
"@tanstack/react-query": "^5.81.5",
"axios": "^1.10.0",
@@ -7142,6 +7145,7 @@
"packages/casan-control-panel/backend": {
"name": "@casan/control-panel-backend",
"version": "1.0.0",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^10.4.20",
"@nestjs/core": "^10.4.20",
@@ -7159,6 +7163,7 @@
"packages/casan-control-panel/frontend": {
"name": "@casan/control-panel-frontend",
"version": "1.0.0",
"license": "UNLICENSED",
"dependencies": {
"@tanstack/react-query": "^5.81.5",
"axios": "^1.10.0",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "ainative-okr-casan5",
"private": true,
"license": "UNLICENSED",
"workspaces": [
"apps/okr/backend",
"apps/okr/frontend",
+6
View File
@@ -23,6 +23,12 @@ Open http://127.0.0.1:5174 — panels show REAL metrics from `.specify/logs/**`.
`incidents` · `tools` · `traceability` · `drift` · `cost` · `GET /healthz` (200 fresh /
503 stale — fail-loud, mirrors `dashboard-server.py`).
Metrics export: `GET /api/v1/metrics` provides Prometheus text exposition for
aggregate freshness, run/failure/cost/token and H4/H5/action/incident counters.
It intentionally contains no tenant, actor, trace, prompt or Evidence Pack
labels. In production restrict the path at the authenticated reverse proxy to
the monitoring network or service account.
Command Center:
- `GET /api/v1/command` — Plan-13 §8.6 read-only executive surface. Returns eight
@@ -2,6 +2,7 @@
"name": "@casan/control-panel-backend",
"version": "1.0.0",
"private": true,
"license": "UNLICENSED",
"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).",
"scripts": {
@@ -7,9 +7,10 @@ import { ApprovalsModule } from './approvals/approvals.module.js';
import { ChatModule } from './chat/chat.module.js';
import { ProviderAuthModule } from './provider-auth/provider-auth.module.js';
import { GoalsModule } from './goals/goals.module.js';
import { EvidenceModule } from './evidence/evidence.module.js';
@Module({
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule],
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule, EvidenceModule],
controllers: [HealthController],
})
export class AppModule {}
@@ -23,8 +23,8 @@ export class ChatController {
}
@Get('audit/verify')
verifyAudit() {
return ok(this.svc.verifyAudit());
verifyAudit(@Headers() headers: Record<string, string | string[] | undefined>) {
return ok(this.svc.verifyAudit(actorFromHeaders(headers)));
}
@Get('history')
@@ -37,8 +37,12 @@ export class ChatController {
}
@Get('replay')
replay(@Query('chatId') chatId?: string, @Query('turnId') turnId?: string, @Query('tenant') tenant?: string) {
return ok(this.svc.replay(chatId || '', turnId || '', tenant || ''));
replay(
@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')
@@ -107,7 +107,7 @@ export class ChatService {
const res = runPython(CHAT_CLI, args, runtime.env);
const parsed = parseJson<Record<string, any>>(res.stdout);
if (parsed) {
return { ...parsed, actor, audit_verify: this.verifyAudit() };
return { ...parsed, actor, audit_verify: this.verifyAudit(actor) };
}
if (res.status !== 0) {
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_CLI_FAILED');
@@ -115,8 +115,9 @@ export class ChatService {
throw new InternalServerErrorException('CHAT_CLI_EMPTY_RESPONSE');
}
verifyAudit() {
const res = runPython(CHAT_CLI, ['verify-audit']);
verifyAudit(actor: SettingsActor) {
this.requireRead(actor);
const res = runPython(CHAT_CLI, ['verify-audit'], this.tenantEnv(actor));
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 args = ['history', '--actor', actor.actor, '--tenant', actor.tenant, '--limit', String(safeLimit)];
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);
if (res.status === 0 && parsed?.ok === true) return parsed;
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_HISTORY_FAILED');
@@ -176,12 +177,12 @@ export class ChatService {
child.on('close', () => res.end());
}
replay(chatId = '', turnId = '', tenant = '') {
replay(actor: SettingsActor, chatId = '', turnId = '') {
this.requireRead(actor);
const args = ['replay'];
if (chatId) args.push('--chat-id', chatId);
if (turnId) args.push('--turn-id', turnId);
const env = tenant && tenant !== 'default' ? { CASAN_TENANT_ID: tenant } : {};
const res = runPython(REPLAY_CLI, args, env);
const res = runPython(REPLAY_CLI, args, this.tenantEnv(actor));
const parsed = parseJson<Record<string, any>>(res.stdout);
if (parsed) return { ok: res.status === 0, ...parsed };
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 } };
}
private tenantEnv(actor: SettingsActor): NodeJS.ProcessEnv {
return { CASAN_TENANT_ID: actor.tenant || 'default' };
}
private requireConnectionAdmin(actor: SettingsActor) {
if (!['project-admin', 'org-admin'].includes(actor.role)) {
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'),
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'),
evidencePacks: env('CASAN_CP_EVIDENCE_PACKS', 'docs/output/casan/evidence-packs'),
};
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 @@
// Read-only Ops Console API. Every handler returns the standard ok() envelope. No writes,
// no auth (loopback-bound, "Đọc ≠ Ghi"); management/RBAC is Plan-13 Track 2/3 (future).
import { Controller, Get, Inject, Param, Query, Req, Res } from '@nestjs/common';
import { Controller, Get, Header, Inject, Param, Query, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { ok } from '../common/api-response.js';
import { TelemetryService } from './telemetry.service.js';
@@ -16,6 +16,14 @@ export class TelemetryController {
return ok(this.svc.overview());
}
// Production reverse proxy policy must restrict this aggregate-only endpoint
// to the monitoring network / service account.
@Get('metrics')
@Header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8')
metrics() {
return this.svc.prometheusMetrics();
}
@Get('runs')
runs(@Query('limit') limit?: string) {
const n = Math.min(Math.max(Number(limit) || 50, 1), 500);
@@ -263,6 +263,51 @@ export class TelemetryService {
};
}
// Prometheus exposition is deliberately aggregate-only. Do not add trace IDs,
// actor/tenant identifiers, prompts, filenames or evidence content as labels:
// those would turn an operations endpoint into a data-exfiltration path.
prometheusMetrics() {
const overview = this.overview();
const totals = overview.totals;
const lines = [
'# HELP casan_telemetry_stale Whether the primary telemetry feed is stale (1=true).',
'# TYPE casan_telemetry_stale gauge',
`casan_telemetry_stale ${overview.stale ? 1 : 0}`,
'# HELP casan_telemetry_age_seconds Age of the primary telemetry feed in seconds.',
'# TYPE casan_telemetry_age_seconds gauge',
`casan_telemetry_age_seconds ${overview.age_s ?? -1}`,
'# HELP casan_runs_total Number of telemetry records retained by CASAN.',
'# TYPE casan_runs_total gauge',
`casan_runs_total ${totals.runs}`,
'# HELP casan_failures_total Number of retained failed telemetry records.',
'# TYPE casan_failures_total gauge',
`casan_failures_total ${totals.failures}`,
'# HELP casan_cost_usd_total Aggregate estimated CASAN execution cost in USD.',
'# TYPE casan_cost_usd_total gauge',
`casan_cost_usd_total ${totals.total_cost}`,
'# HELP casan_provider_tokens_total Aggregate provider token usage.',
'# TYPE casan_provider_tokens_total gauge',
`casan_provider_tokens_total ${totals.provider_tokens}`,
'# HELP casan_security_blocks_total H4 security verdicts blocked.',
'# TYPE casan_security_blocks_total gauge',
`casan_security_blocks_total ${overview.harness_signals['H4-security'].blocked}`,
'# HELP casan_governance_denials_total H5 governance decisions denied.',
'# TYPE casan_governance_denials_total gauge',
`casan_governance_denials_total ${overview.harness_signals['H5-governance'].denied}`,
'# HELP casan_tool_denials_total Tool registry decisions denied.',
'# TYPE casan_tool_denials_total gauge',
`casan_tool_denials_total ${totals.tool_denies}`,
'# HELP casan_action_blocks_total Governed action gate block outcomes.',
'# TYPE casan_action_blocks_total gauge',
`casan_action_blocks_total ${totals.action_blocks}`,
'# HELP casan_critical_incidents_total Retained critical incident records.',
'# TYPE casan_critical_incidents_total gauge',
`casan_critical_incidents_total ${overview.harness_signals.incidents.critical}`,
'',
];
return lines.join('\n');
}
runs(limit = 50) {
const metrics = readJsonl(PATHS.metrics);
return { ...this.freshness(), count: metrics.length, runs: recent(metrics, limit) };
@@ -1,6 +1,6 @@
import { test } from 'node:test';
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 { join } from 'node:path';
import { ChatService } from '../src/chat/chat.service.js';
@@ -20,7 +20,9 @@ function withTempChatState(fn: () => void) {
const state = mkdtempSync(join(tmpdir(), 'cp-chat-'));
process.env.CASAN_STATE_ROOT = state;
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_HEAD;
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.trace_verify.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.decision, 'MATCH');
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.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.decision, 'MATCH');
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.records, 0);
});
@@ -40,6 +40,15 @@ test('overview() returns real aggregated shape, never throws on the repo state',
assert.ok(o.audit_chain.records >= 0);
});
test('prometheusMetrics() exports aggregate-safe operational metrics only', () => {
const metrics = new TelemetryService().prometheusMetrics();
assert.match(metrics, /^# HELP casan_telemetry_stale/m);
assert.match(metrics, /^casan_runs_total \d+/m);
assert.match(metrics, /^casan_security_blocks_total \d+/m);
assert.match(metrics, /^casan_governance_denials_total \d+/m);
assert.doesNotMatch(metrics, /trace_id|tenant|prompt|evidence/i);
});
test('security()/governance()/cost() return objects with expected keys', () => {
const svc = new TelemetryService();
assert.ok(typeof (svc.security() as any).by_status === 'object');
@@ -2,6 +2,7 @@
"name": "@casan/control-panel-frontend",
"version": "1.0.0",
"private": true,
"license": "UNLICENSED",
"type": "module",
"description": "CASAN Ops Console \u2014 read-only React UI over the harness telemetry API.",
"scripts": {
@@ -12,6 +12,7 @@ import { Approvals } from './pages/Approvals';
import { CommandCenter } from './pages/CommandCenter';
import { Chat } from './pages/Chat';
import { Goals } from './pages/Goals';
import { EvidencePacks } from './pages/EvidencePacks';
export default function App() {
return (
@@ -29,6 +30,7 @@ export default function App() {
<Route path="/command" element={<CommandCenter />} />
<Route path="/chat" element={<Chat />} />
<Route path="/goals" element={<Goals />} />
<Route path="/evidence-packs" element={<EvidencePacks />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AppLayout>
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
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; }
@@ -18,6 +18,7 @@ const NAVIGATION: Array<{ label: string; items: NavItem[] }> = [
{ to: '/security', label: 'Security', icon: 'shield' },
{ to: '/incidents', label: 'Incidents', icon: 'incident' },
{ to: '/traceability', label: 'Traceability', icon: 'trace' },
{ to: '/evidence-packs', label: 'Evidence packs', icon: 'evidence' },
] },
{ label: 'Control', items: [
{ 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" /></>,
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" /></>,
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" /></>,
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" /></>,
@@ -34,6 +34,25 @@ export interface HarnessRunRecord {
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 {
totals: {
runs: number; total_cost: number; avg_latency_ms: number; failures: number;
@@ -419,6 +438,8 @@ export const api = {
traceability: () => get<Freshness & { matrix: any }>('traceability'),
cost: () => get<Freshness & { provider_tokens: number; provider_cost: number; by_provider: any[]; business_kpi: any }>('cost'),
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)),
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)),
@@ -0,0 +1,62 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { api, type EvidencePackDetail, type EvidencePackSummary } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
const HARNESSES = [
['H1', 'Context', 'h1-context-report.json'], ['H2', 'Tool', 'h2-tool-audit.json'],
['H3', 'Evaluation', 'h3-eval-scorecard.json'], ['H4', 'Security', 'h4-security-report.json'],
['H5', 'Governance', 'h5-audit-chain-proof.json'], ['H6', 'AgentOps', 'h6-cost-telemetry.json'],
['H7', 'Orchestration', 'h7-orchestration-report.json'],
] as const;
function shortHash(value: string | null) { return value ? `${value.slice(0, 12)}…${value.slice(-8)}` : 'not recorded'; }
function label(reason: string) { return reason.replaceAll('_', ' '); }
function statusFor(report: Record<string, unknown> | undefined, detail: EvidencePackDetail) {
if (!report) return 'not recorded';
if (report.chain_ok === false || report.audit_chain_ok === false || report.telemetry_ok === false) return 'failed';
if (Number(report.blocked ?? 0) > 0 || report.cost_spike_status === 'spike_detected') return 'attention';
return detail.integrity.ok ? 'recorded' : 'unverified';
}
function PackRow({ pack, selected, onSelect }: { pack: EvidencePackSummary; selected: boolean; onSelect: () => void }) {
return <button type="button" onClick={onSelect} className={`w-full rounded-xl border p-3.5 text-left transition ${selected ? 'border-indigo-300 bg-indigo-50/70 shadow-sm' : 'border-transparent hover:border-slate-200 hover:bg-slate-50'}`}>
<div className="flex items-start justify-between gap-2"><span className="font-mono text-xs font-semibold text-slate-700">{pack.run_id}</span><StatusBadge value={pack.certified ? 'certified' : 'uncertified'} /></div>
<div className="mt-2 flex items-center justify-between text-[11px] text-slate-500"><span>{new Date(pack.created_at).toLocaleString()}</span><span>{pack.file_count} files</span></div>
</button>;
}
function Detail({ pack }: { pack: EvidencePackDetail }) {
const reportEntries = Object.entries(pack.reports);
return <div className="space-y-5">
<section className={`overflow-hidden rounded-2xl border ${pack.certified ? 'border-emerald-200 bg-gradient-to-br from-emerald-50 to-white' : 'border-amber-200 bg-gradient-to-br from-amber-50 to-white'} p-5 sm:p-6`}>
<div className="flex flex-col justify-between gap-5 sm:flex-row sm:items-start">
<div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Evidence verdict</div><h1 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900">{pack.certified ? 'Certified evidence pack' : 'Evidence captured — certification withheld'}</h1><p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">Run <span className="font-mono text-xs">{pack.run_id}</span> is bound to a file manifest and verified independently before it is shown here.</p></div>
<div className="flex flex-wrap gap-2"><StatusBadge value={pack.integrity.ok ? 'manifest verified' : 'integrity failed'} /><StatusBadge value={pack.signature === 'present' ? 'signature present' : 'unsigned'} /></div>
</div>
{pack.certification_reasons.length > 0 && <div className="mt-5 rounded-xl border border-amber-200 bg-white/80 p-3.5"><div className="text-[11px] font-bold uppercase tracking-[0.13em] text-amber-700">Why certification is withheld</div><div className="mt-2 flex flex-wrap gap-2">{pack.certification_reasons.map((reason) => <span key={reason} className="rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800">{label(reason)}</span>)}</div></div>}
</section>
<Card title="H1–H7 chain of custody" right={<span className="font-mono text-[11px] text-slate-400">manifest {shortHash(pack.manifest_head)}</span>}>
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-7">{HARNESSES.map(([id, name, file]) => { const report = pack.reports[file]; const state = statusFor(report, pack); return <div key={id} className="relative rounded-xl border border-slate-200 bg-slate-50/70 p-3"><div className="flex items-center justify-between"><span className="font-mono text-xs font-bold text-indigo-700">{id}</span><span className={`h-2 w-2 rounded-full ${state === 'recorded' ? 'bg-emerald-500' : state === 'failed' ? 'bg-rose-500' : state === 'attention' ? 'bg-amber-500' : 'bg-slate-300'}`} /></div><div className="mt-2 text-sm font-semibold text-slate-800">{name}</div><div className="mt-1 text-[11px] text-slate-500">{state}</div></div>; })}</div>
</Card>
<div className="grid gap-5 xl:grid-cols-[1.1fr_0.9fr]">
<Card title="Gate reports"><div className="space-y-3">{reportEntries.map(([name, report]) => <details key={name} className="rounded-xl border border-slate-200 bg-slate-50/60 p-3.5"><summary className="cursor-pointer list-none text-sm font-semibold text-slate-700"><span className="font-mono text-xs text-indigo-700">{name.replace('-report.json', '').replace('.json', '')}</span></summary><dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 border-t border-slate-200 pt-3 text-xs">{Object.entries(report).filter(([, value]) => typeof value !== 'object').map(([key, value]) => <div key={key}><dt className="text-slate-400">{key.replaceAll('_', ' ')}</dt><dd className="mt-0.5 break-words font-medium text-slate-700">{String(value)}</dd></div>)}</dl></details>)}</div></Card>
<Card title="Evidence manifest" right={<span className="text-xs text-slate-400">{pack.files.length} bound files</span>}><div className="max-h-[430px] space-y-2 overflow-y-auto pr-1">{pack.files.map((file) => <div key={file.path} className="rounded-xl border border-slate-100 bg-slate-50/70 p-3"><div className="break-all font-mono text-xs font-medium text-slate-700">{file.path}</div><div className="mt-1.5 flex justify-between gap-3 font-mono text-[10px] text-slate-400"><span>{shortHash(file.sha256)}</span><span>{file.bytes.toLocaleString()} B</span></div></div>)}</div></Card>
</div>
<Card title="Decision log"><pre className="max-h-80 overflow-auto whitespace-pre-wrap rounded-xl bg-slate-950 p-4 font-mono text-xs leading-5 text-slate-200">{pack.decision_log ?? 'No decision log was recorded.'}</pre></Card>
</div>;
}
export function EvidencePacks() {
const { data, isLoading, isError } = useQuery({ queryKey: ['evidence-packs'], queryFn: api.evidencePacks });
const [selected, setSelected] = useState('');
const activeId = selected || data?.packs[0]?.run_id || '';
const detail = useQuery({ queryKey: ['evidence-pack', activeId], queryFn: () => api.evidencePack(activeId), enabled: Boolean(activeId) });
const heading = useMemo(() => data?.packs.length ?? 0, [data]);
if (isLoading) return <div className="text-slate-500">Loading evidence inventory…</div>;
if (isError || !data) return <div className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-rose-700">Evidence inventory could not be loaded.</div>;
if (!data.packs.length) return <Card title="Evidence packs"><div className="py-12 text-center"><div className="text-lg font-semibold text-slate-800">No evidence pack has been created yet</div><p className="mx-auto mt-2 max-w-lg text-sm leading-6 text-slate-500">Create one with <code className="rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs">casan pack &lt;run-id&gt;</code>; it will appear here only after the on-disk manifest is available.</p></div></Card>;
return <div className="space-y-5"><div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-indigo-600">Assurance workspace</div><h1 className="mt-1 text-2xl font-semibold tracking-tight text-slate-900">Evidence packs <span className="text-slate-400">({heading})</span></h1><p className="mt-1 text-sm text-slate-500">Inspect certification, provenance, gates and immutable file hashes without opening raw logs.</p></div><div className="grid gap-5 xl:grid-cols-[288px_minmax(0,1fr)]"><aside className="rounded-2xl border border-slate-200 bg-white p-2 shadow-sm"><div className="px-3 py-2 text-[11px] font-bold uppercase tracking-[0.13em] text-slate-400">Available packs</div><div className="space-y-1.5">{data.packs.map((pack) => <PackRow key={pack.run_id} pack={pack} selected={pack.run_id === activeId} onSelect={() => setSelected(pack.run_id)} />)}</div></aside><main>{detail.isLoading && <div className="text-slate-500">Verifying manifest…</div>}{detail.isError && <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">The selected evidence pack could not be read safely.</div>}{detail.data && <Detail pack={detail.data} />}</main></div></div>;
}
+12 -5
View File
@@ -19,8 +19,14 @@ Enterprise governed AI-SDLC console. Packages: `casan-enterprise`, `casan-govern
| Loop governance | Plan-17 loop primitives (`loop-*.py`) |
## Components still to build (NOT in this task)
- Governed Chat Console (Plan-18) · Prompt Mode Router · Model Provider Management
- Operator mode · Codegen mode · Agent/Skill Registry · policy-versioning UI
- Managed enterprise deployment with enterprise OIDC, CA, network policy and
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
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`.
## To implement later
Sequence: Plan-14 (RBAC console) → Plan-13 (Control Plane) → Plan-18 (Governed Chat
Console: read-only → operator → chat-as-loop → multi-tenant). Reuse the existing blocks
above instead of re-writing them.
Sequence: finish the paid-PoC evidence/Gitea flow → enterprise deployment and
operations controls → independent review → production readiness. Reuse the existing
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:
# 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_STEP_TIMEOUT_SEC default 600
# CASAN_CI_SUITE_FILTER optional regex; run matching suite names only
@@ -132,10 +133,13 @@ run "phase-sec23-state-isolation" bash "$TESTS/phase-sec23-state-isolation-tests
run "phase-sec23-rbac-tenant" bash "$TESTS/phase-sec23-rbac-tenant-tests.sh"
run "phase-sec23-scope" bash "$TESTS/phase-sec23-scope-tests.sh"
run "phase-sec23-registry-crypt" bash "$TESTS/phase-sec23-registry-crypt-tests.sh"
run "phase-tenant-isolation-review" bash "$TESTS/phase-tenant-isolation-review-tests.sh"
# Plan-16 SEC-24/25 supply-chain (offline slice)
run "phase-sec24-supplychain" bash "$TESTS/phase-sec24-tests.sh"
run "phase-sec25-attestation" bash "$TESTS/phase-sec25-tests.sh"
run "phase-production-handoff" bash "$TESTS/phase-production-preflight-tests.sh"
run "phase-state-backup" bash "$TESTS/phase-state-backup-tests.sh"
run "phase-upgrade-compatibility" bash "$TESTS/phase-upgrade-compatibility-tests.sh"
run "phase-release-provenance" bash "$TESTS/phase-release-provenance-tests.sh"
run "phase-service-desk-onboard" bash "$TESTS/phase-service-desk-onboard-tests.sh"
@@ -188,6 +192,17 @@ else
skip "frontend-vitest (CASAN_CI_RUN_FRONTEND=0)"
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 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"
@@ -267,6 +267,16 @@ def extract_patch(text: str) -> str:
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:
roots = [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])]
changed = []
@@ -640,6 +650,14 @@ def run(job_path: str) -> int:
if not allowed:
emit(goal_id, "H4-security", "blocked", "Local worker output rejected")
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)
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})
@@ -670,10 +688,23 @@ def run(job_path: str) -> int:
if not allowed:
emit(goal_id, "H4-security", "blocked", "Cloud reviewer output rejected")
raise ValueError("cloud_output_security_blocked")
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})
final_status = "completed"
metric_status = "success"
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)
emit(goal_id, "H3-eval", "pass", "Independent review incorporated", {"provider": reviewer_provider, "model": reviewer_model, **cloud_meta})
final_status = "completed"
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:
safe_result = safe_local
stage(job_path, "cloud-reviewer", "warning", cloud_reason, reviewer_provider, reviewer_model)
@@ -12,6 +12,7 @@ set -uo pipefail
# --memory/--cpus → resource abuse is bounded
# -v <ws>:/work:rw → ONLY the workspace is writable; host $HOME/.ssh is NOT mounted
# --cap-drop=ALL --security-opt=no-new-privileges → no privilege escalation
# --user=65532:65532 → workload process is never root in the container
#
# Usage:
# sandbox-container.sh --workspace <dir> [--image busybox] [--timeout 20]
@@ -22,6 +23,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE="${CASAN_SANDBOX_IMAGE:-busybox}"
WORKSPACE="$PWD"; TIMEOUT="${CASAN_SANDBOX_TIMEOUT:-20}"
MEMORY="${CASAN_SANDBOX_MEMORY:-256m}"; PIDS="${CASAN_SANDBOX_PIDS:-128}"; CPUS="${CASAN_SANDBOX_CPUS:-1}"
USER_SPEC="${CASAN_SANDBOX_USER:-65532:65532}"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--workspace) WORKSPACE="${2:-}"; shift 2 ;;
@@ -30,6 +32,7 @@ while [[ "$#" -gt 0 ]]; do
--memory) MEMORY="${2:-}"; shift 2 ;;
--pids) PIDS="${2:-}"; shift 2 ;;
--cpus) CPUS="${2:-}"; shift 2 ;;
--user) USER_SPEC="${2:-}"; shift 2 ;;
--) shift; break ;;
*) echo "sandbox-container: unknown arg $1" >&2; exit 2 ;;
esac
@@ -38,6 +41,16 @@ done
command -v docker >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_NO_DOCKER" >&2; exit 127; }
docker info >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_DOCKER_DOWN" >&2; exit 127; }
[[ "$USER_SPEC" =~ ^[0-9]+:[0-9]+$ ]] || { echo "SANDBOX_CONTAINER_BAD_USER" >&2; exit 2; }
# A non-root process is always required. Production additionally refuses a
# rootful Docker daemon because a compromised daemon socket defeats container
# isolation. Local developer/test profiles may use a rootful daemon, but cannot
# claim that configuration as a hardened production runner.
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'rootless' \
|| { echo "SANDBOX_CONTAINER_ROOTLESS_REQUIRED" >&2; exit 2; }
fi
WS_ABS="$(cd "$WORKSPACE" 2>/dev/null && pwd)" || { echo "SANDBOX_CONTAINER_BAD_WORKSPACE" >&2; exit 2; }
@@ -54,7 +67,7 @@ bash "$SCRIPT_DIR/tool-exec.sh" "$TIMEOUT" -- \
docker run --rm --init --name "$CID" \
--network=none --read-only \
--pids-limit="$PIDS" --memory="$MEMORY" --cpus="$CPUS" \
--cap-drop=ALL --security-opt=no-new-privileges \
--user "$USER_SPEC" --cap-drop=ALL --security-opt=no-new-privileges \
--tmpfs /tmp:rw,size=16m \
-v "$WS_ABS":/work:rw -w /work \
"$IMAGE" sh -c "$CMD"
@@ -103,6 +103,10 @@ PY
fi
else
# Fallback — local key (dev environment without Vault)
if [[ "${CASAN_PROFILE:-}" == "prod" ]]; then
echo "SIGN_AUDIT_HEAD_FAIL reason=vault_kms_required_in_prod" >&2
exit 1
fi
AUDIT_PRIV="$CASAN_GOVERNANCE_ROOT/audit-private.pem"
if [[ ! -f "$AUDIT_PRIV" ]]; then
echo "SIGN_AUDIT_HEAD_SKIP no private key and VAULT_ADDR not set — verify will show anchor=unsigned" >&2
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
set -euo pipefail
# CASAN state backup / restore. Backups are self-verifying and restore only
# into an explicit empty directory; production requires encryption at rest.
#
# state-backup.sh create <backup-id> [--out DIR]
# state-backup.sh verify <backup-id> [--out DIR]
# state-backup.sh restore <backup-id> --out DIR --to EMPTY_DIR
# state-backup.sh drill <backup-id> [--out DIR]
#
# Env: CASAN_BACKUP_PASSPHRASE_FILE is required in prod and optional elsewhere.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
MODE="${1:-}"; ID="${2:-}"
shift 2 2>/dev/null || true
OUT_ROOT="${CASAN_BACKUP_ROOT:-$CASAN_APP_ROOT/backups/casan-state}"
TO=""
while [[ "$#" -gt 0 ]]; do
case "$1" in
--out) OUT_ROOT="${2:-}"; shift 2 ;;
--to) TO="${2:-}"; shift 2 ;;
*) echo "STATE_BACKUP_FAIL reason=unknown_option option=$1" >&2; exit 64 ;;
esac
done
fail() { echo "STATE_BACKUP_FAIL $*" >&2; exit 1; }
[[ "$ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$ ]] || { echo "STATE_BACKUP_FAIL reason=invalid_backup_id" >&2; exit 64; }
OUT_ROOT="$(cd "$(dirname "$OUT_ROOT")" 2>/dev/null && pwd)/$(basename "$OUT_ROOT")"
PACK_DIR="$OUT_ROOT/$ID"
ARCHIVE="$PACK_DIR/state.tar.gz"
ENCRYPTED="$PACK_DIR/state.tar.gz.enc"
MANIFEST="$PACK_DIR/manifest.json"
passphrase_file() {
local key="${CASAN_BACKUP_PASSPHRASE_FILE:-}"
if [[ "${CASAN_PROFILE:-}" == "prod" && ( -z "$key" || ! -s "$key" ) ]]; then
fail "reason=prod_encryption_key_required"
fi
[[ -z "$key" || -s "$key" ]] || fail "reason=backup_passphrase_file_missing"
printf '%s' "$key"
}
write_manifest() {
local payload="$1" encrypted="$2" files="$3"
python3 - "$MANIFEST" "$ID" "$payload" "$encrypted" "$files" <<'PY'
import hashlib, json, os, sys
out, backup_id, payload, encrypted, files = sys.argv[1:]
with open(payload, 'rb') as f: digest = hashlib.sha256(f.read()).hexdigest()
data = {"schema_version":"1.0", "backup_id":backup_id, "payload":os.path.basename(payload),
"encrypted":encrypted == "true", "sha256":digest, "state_files":int(files)}
with open(out, 'w', encoding='utf8') as f: json.dump(data, f, sort_keys=True, indent=2); f.write('\n')
PY
}
verify() {
[[ -f "$MANIFEST" ]] || fail "reason=manifest_missing"
python3 - "$MANIFEST" "$PACK_DIR" <<'PY'
import hashlib, json, os, sys
manifest, root = sys.argv[1:]
data = json.load(open(manifest, encoding='utf8'))
payload = data.get('payload', '')
if not payload or os.path.basename(payload) != payload: raise SystemExit('payload_invalid')
path = os.path.join(root, payload)
if not os.path.isfile(path): raise SystemExit('payload_missing')
with open(path, 'rb') as f: actual = hashlib.sha256(f.read()).hexdigest()
if actual != data.get('sha256'): raise SystemExit('checksum_mismatch')
print(json.dumps(data, sort_keys=True))
PY
}
case "$MODE" in
create)
[[ -d "$CASAN_STATE_ROOT" ]] || fail "reason=state_root_missing"
[[ ! -e "$PACK_DIR" ]] || fail "reason=backup_already_exists"
mkdir -p "$PACK_DIR" || fail "reason=backup_dir_create_failed"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
# State is copied by relative path only. The archive cannot contain an
# arbitrary absolute filesystem path even when CASAN_STATE_ROOT is overridden.
tar -czf "$TMP/state.tar.gz" -C "$(dirname "$CASAN_STATE_ROOT")" "$(basename "$CASAN_STATE_ROOT")"
COUNT="$(tar -tzf "$TMP/state.tar.gz" | wc -l | tr -d '[:space:]')"
KEY="$(passphrase_file)"
if [[ -n "$KEY" ]]; then
command -v openssl >/dev/null 2>&1 || fail "reason=openssl_required_for_encryption"
openssl enc -aes-256-cbc -pbkdf2 -salt -in "$TMP/state.tar.gz" -out "$ENCRYPTED" -pass "file:$KEY"
write_manifest "$ENCRYPTED" true "$COUNT"
rm -f "$TMP/state.tar.gz"
else
mv "$TMP/state.tar.gz" "$ARCHIVE"
write_manifest "$ARCHIVE" false "$COUNT"
fi
chmod 700 "$PACK_DIR"; chmod 600 "$MANIFEST" "$PACK_DIR"/state.tar.gz* 2>/dev/null || true
echo "STATE_BACKUP_CREATED id=$ID files=$COUNT encrypted=$([[ -n "$KEY" ]] && echo true || echo false) dir=$PACK_DIR"
;;
verify)
RESULT="$(verify)" || fail "reason=manifest_verification_failed"
echo "STATE_BACKUP_VALID id=$ID $RESULT"
;;
restore)
[[ -n "$TO" ]] || { echo "STATE_BACKUP_FAIL reason=restore_target_required" >&2; exit 64; }
RESULT="$(verify)" || fail "reason=manifest_verification_failed"
[[ ! -e "$TO" || ( -d "$TO" && -z "$(find "$TO" -mindepth 1 -maxdepth 1 -print -quit)" ) ]] || fail "reason=restore_target_not_empty"
mkdir -p "$TO"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
ENCRYPTED_FLAG="$(python3 -c 'import json,sys; print(str(json.loads(sys.argv[1])["encrypted"]).lower())' "$RESULT")"
if [[ "$ENCRYPTED_FLAG" == true ]]; then
KEY="$(passphrase_file)"; [[ -n "$KEY" ]] || fail "reason=decrypt_key_required"
openssl enc -d -aes-256-cbc -pbkdf2 -in "$ENCRYPTED" -out "$TMP/state.tar.gz" -pass "file:$KEY" || fail "reason=decrypt_failed"
else
cp "$ARCHIVE" "$TMP/state.tar.gz"
fi
tar -xzf "$TMP/state.tar.gz" -C "$TO" --no-same-owner
echo "STATE_BACKUP_RESTORED id=$ID target=$TO"
;;
drill)
"$0" create "$ID" --out "$OUT_ROOT"
"$0" verify "$ID" --out "$OUT_ROOT"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
"$0" restore "$ID" --out "$OUT_ROOT" --to "$TMP/restore"
ORIGINAL="$(tar -tzf "${ARCHIVE:-$PACK_DIR/state.tar.gz}" 2>/dev/null || true)"
[[ -n "$ORIGINAL" || -f "$ENCRYPTED" ]] || fail "reason=drill_archive_missing"
[[ -d "$TMP/restore/$(basename "$CASAN_STATE_ROOT")" ]] || fail "reason=drill_restore_shape_invalid"
echo "STATE_BACKUP_DRILL_PASS id=$ID restored=true"
;;
*) echo "Usage: state-backup.sh {create|verify|restore|drill} <backup-id> [--out DIR] [--to EMPTY_DIR]" >&2; exit 64 ;;
esac
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Read-only tenant-isolation pre-audit for CASAN state partitions.
It validates filesystem boundaries and records concrete findings. It is not an
independent audit; use its JSON output as input to an independent review.
"""
import argparse
import hashlib
import json
import os
import re
import stat
import sys
from pathlib import Path
TENANT = re.compile(r"^[A-Za-z0-9_-]+$")
def mode(path: Path) -> int:
return stat.S_IMODE(path.lstat().st_mode)
def insecure(mode_value: int) -> bool:
return bool(mode_value & 0o077)
def review(root: Path) -> dict:
findings: list[dict] = []
tenants: list[str] = []
keys: dict[str, str] = {}
if not root.is_dir() or root.is_symlink():
findings.append({"severity": "critical", "code": "tenant_root_invalid", "path": str(root)})
return {"ok": False, "root": str(root), "tenants": tenants, "findings": findings}
if insecure(mode(root)):
findings.append({"severity": "high", "code": "tenant_root_permissions", "path": str(root), "mode": oct(mode(root))})
for candidate in sorted(root.iterdir()):
if not TENANT.fullmatch(candidate.name):
findings.append({"severity": "medium", "code": "unexpected_root_entry", "path": str(candidate)})
continue
tenants.append(candidate.name)
if candidate.is_symlink() or not candidate.is_dir():
findings.append({"severity": "critical", "code": "tenant_partition_not_directory", "tenant": candidate.name})
continue
if insecure(mode(candidate)):
findings.append({"severity": "high", "code": "tenant_permissions", "tenant": candidate.name, "mode": oct(mode(candidate))})
for current, dirs, files in os.walk(candidate, followlinks=False):
base = Path(current)
for name in dirs + files:
item = base / name
if item.is_symlink():
findings.append({"severity": "critical", "code": "tenant_symlink_forbidden", "tenant": candidate.name, "path": str(item)})
key = candidate / "keys" / "at-rest.key"
if key.exists():
if key.is_symlink() or mode(key) != 0o600:
findings.append({"severity": "high", "code": "tenant_key_permissions", "tenant": candidate.name, "path": str(key), "mode": oct(mode(key))})
else:
keys[candidate.name] = hashlib.sha256(key.read_bytes()).hexdigest()
values = list(keys.values())
if len(values) != len(set(values)):
findings.append({"severity": "critical", "code": "tenant_key_material_reused"})
return {"ok": not findings, "root": str(root), "tenants": tenants, "keyed_tenants": sorted(keys), "findings": findings}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", required=True)
parser.add_argument("--out")
args = parser.parse_args()
result = review(Path(args.root).resolve())
payload = json.dumps(result, sort_keys=True, indent=2) + "\n"
if args.out:
Path(args.out).write_text(payload, encoding="utf8")
print(payload, end="")
return 0 if result["ok"] else 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
# Fail-closed compatibility gate for an upgrade or rollback. This command does
# not deploy. It produces a machine-readable plan that a release workflow must
# verify before it changes runtime state.
#
# upgrade-compatibility.sh check --from 1.0.0 --to 1.0.1 [--matrix FILE]
# upgrade-compatibility.sh check --from 1.0.1 --to 1.0.0 --rollback [--matrix FILE]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
CMD="${1:-}"; shift || true
FROM=""; TO=""; ROLLBACK=false
MATRIX="${CASAN_COMPATIBILITY_MATRIX:-$CASAN_APP_ROOT/infra/production/compatibility-matrix.json}"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--from) FROM="${2:-}"; shift 2 ;;
--to) TO="${2:-}"; shift 2 ;;
--matrix) MATRIX="${2:-}"; shift 2 ;;
--rollback) ROLLBACK=true; shift ;;
*) echo "UPGRADE_COMPATIBILITY_FAIL reason=unknown_option option=$1" >&2; exit 64 ;;
esac
done
[[ "$CMD" == "check" && -n "$FROM" && -n "$TO" && -f "$MATRIX" ]] || {
echo "Usage: upgrade-compatibility.sh check --from X.Y.Z --to X.Y.Z [--rollback] [--matrix FILE]" >&2; exit 64; }
python3 - "$MATRIX" "$FROM" "$TO" "$ROLLBACK" <<'PY'
import json, re, sys
matrix, source, target, rollback = sys.argv[1:]
def parse(v):
m = re.fullmatch(r'(\d+)\.(\d+)\.(\d+)', v)
if not m: raise ValueError('invalid_semver')
return tuple(map(int, m.groups()))
try:
src, dst = parse(source), parse(target)
data = json.load(open(matrix, encoding='utf8'))
except Exception as e:
print(f'UPGRADE_COMPATIBILITY_FAIL reason={str(e)[:80]}', file=sys.stderr); raise SystemExit(1)
def matches(pattern, version):
m = re.fullmatch(r'(\d+)\.(\d+)\.(x|\d+)', pattern)
if not m: return False
a,b,c=m.groups()
return version[0] == int(a) and version[1] == int(b) and (c == 'x' or version[2] == int(c))
for rule in data.get('rules', []):
if matches(str(rule.get('from','')), src) and matches(str(rule.get('to','')), dst):
if rollback == 'true' and rule.get('rollback') != 'supported':
continue
print(json.dumps({'decision':'allow','from':source,'to':target,'rollback':rollback == 'true',
'policy_schema':rule.get('policy_schema'),'evidence_pack_schema':rule.get('evidence_pack_schema'),
'required_steps':['verify_release_provenance','state_backup','artifact_smoke','post_deploy_evidence_verify']}, sort_keys=True))
raise SystemExit(0)
print(json.dumps({'decision':'deny','from':source,'to':target,'rollback':rollback == 'true',
'reason':'no_approved_compatibility_rule'}, sort_keys=True), file=sys.stderr)
raise SystemExit(1)
PY
@@ -31,6 +31,7 @@ expect_nonzero() {
echo "===== C6 true isolation (container) ====="
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
chmod 777 "$WORK" # non-root container user must be able to write the explicit workspace
expect_nonzero "network egress blocked by --network=none" \
bash "$SB" --workspace "$WORK" -- 'wget -T 2 -q -O- http://1.1.1.1 || exit 7'
expect_nonzero "write outside workspace blocked by --read-only rootfs" \
@@ -42,11 +43,20 @@ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
bash "$SB" --workspace "$WORK" -- 'echo ok > proof.txt'
[[ -f "$WORK/proof.txt" ]] && pass "workspace write is visible on host (bind mount)" \
|| fail "workspace write not visible on host"
expect_rc 0 "workload runs as a non-root UID" \
bash "$SB" --workspace "$WORK" -- 'test "$(id -u)" != 0'
if docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q rootless; then
expect_rc 0 "production mode accepts a rootless Docker daemon" \
env CASAN_PROFILE=prod bash "$SB" --workspace "$WORK" -- 'true'
else
expect_nonzero "production mode rejects a rootful Docker daemon" \
env CASAN_PROFILE=prod bash "$SB" --workspace "$WORK" -- 'true'
fi
# sandbox-run.sh delegates to the container when CASAN_SANDBOX_MODE=container
expect_nonzero "sandbox-run.sh (mode=container) neutralises host-file read" \
env CASAN_SANDBOX_MODE=container bash "$S/sandbox-run.sh" --workspace "$WORK" -- 'cat ~/.ssh/id_rsa'
else
echo " SKIP container isolation (Docker not available)"; PASS=$((PASS+6))
echo " SKIP container isolation (Docker not available)"; PASS=$((PASS+8))
fi
echo ""
@@ -56,6 +56,12 @@ env CASAN_VERIFY_STRICT=1 CASAN_AUDIT_KEY_DIR="$KD2" bash "$BASH_DIR/governance-
&& pass "unsigned head from enforced run FAILS strict verify (fail-closed)" \
|| fail "unsigned head passed strict verify"
# 4) Production must never silently fall back from Vault Transit to a local key.
printf '{"timestamp":"2026-07-18T00:00:00Z","action":"test"}\n' > "$WORK/audit.jsonl"
[[ "$(rc_of env CASAN_PROFILE=prod VAULT_ADDR= VAULT_TOKEN= CASAN_GOVERNANCE_ROOT="$WORK/prod-gov" bash "$BASH_DIR/sign-audit-head.sh" "$WORK/audit.jsonl")" -ne 0 ]] \
&& pass "prod audit signer refuses local-key fallback when Vault is unavailable" \
|| fail "prod audit signer accepted a local-key fallback"
echo ""
echo "===== SEC-02 SUMMARY: PASS=$PASS FAIL=$FAIL ====="
[[ "$FAIL" -eq 0 ]] || exit 1
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
S="$SCRIPT_DIR/../scripts/bash/state-backup.sh"
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
PASS=0; FAIL=0
ok() { "$@" >/dev/null 2>&1 && { echo "PASS: $2"; PASS=$((PASS+1)); } || { echo "FAIL: $2"; FAIL=$((FAIL+1)); }; }
STATE="$WORK/state"; mkdir -p "$STATE/logs/audit"; printf 'audit\n' > "$STATE/logs/audit/audit.jsonl"; printf 'head\n' > "$STATE/logs/audit/audit-head.txt"
CASAN_APP_ROOT="$WORK/app" CASAN_STATE_ROOT="$STATE" CASAN_BACKUP_ROOT="$WORK/backups" bash "$S" create smoke >/dev/null && { echo "PASS: create plaintext backup"; PASS=$((PASS+1)); } || { echo "FAIL: create plaintext backup"; FAIL=$((FAIL+1)); }
CASAN_APP_ROOT="$WORK/app" CASAN_STATE_ROOT="$STATE" CASAN_BACKUP_ROOT="$WORK/backups" bash "$S" verify smoke >/dev/null && { echo "PASS: verify backup manifest"; PASS=$((PASS+1)); } || { echo "FAIL: verify backup manifest"; FAIL=$((FAIL+1)); }
CASAN_APP_ROOT="$WORK/app" CASAN_STATE_ROOT="$STATE" CASAN_BACKUP_ROOT="$WORK/backups" bash "$S" restore smoke --to "$WORK/restore" >/dev/null && cmp "$STATE/logs/audit/audit.jsonl" "$WORK/restore/state/logs/audit/audit.jsonl" >/dev/null && { echo "PASS: restore preserves state"; PASS=$((PASS+1)); } || { echo "FAIL: restore preserves state"; FAIL=$((FAIL+1)); }
mkdir -p "$WORK/not-empty"; touch "$WORK/not-empty/keep"
if CASAN_APP_ROOT="$WORK/app" CASAN_STATE_ROOT="$STATE" CASAN_BACKUP_ROOT="$WORK/backups" bash "$S" restore smoke --to "$WORK/not-empty" >/dev/null 2>&1; then echo "FAIL: non-empty restore target denied"; FAIL=$((FAIL+1)); else echo "PASS: non-empty restore target denied"; PASS=$((PASS+1)); fi
printf 'passphrase\n' > "$WORK/key"; chmod 600 "$WORK/key"
CASAN_PROFILE=prod CASAN_BACKUP_PASSPHRASE_FILE="$WORK/key" CASAN_APP_ROOT="$WORK/app" CASAN_STATE_ROOT="$STATE" CASAN_BACKUP_ROOT="$WORK/backups" bash "$S" create prod >/dev/null && { echo "PASS: prod backup encrypted"; PASS=$((PASS+1)); } || { echo "FAIL: prod backup encrypted"; FAIL=$((FAIL+1)); }
if CASAN_PROFILE=prod CASAN_APP_ROOT="$WORK/app" CASAN_STATE_ROOT="$STATE" CASAN_BACKUP_ROOT="$WORK/backups" bash "$S" create no-key >/dev/null 2>&1; then echo "FAIL: prod backup without key denied"; FAIL=$((FAIL+1)); else echo "PASS: prod backup without key denied"; PASS=$((PASS+1)); fi
echo "===== STATE BACKUP SUMMARY: PASS=$PASS FAIL=$FAIL ====="; [[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
R="$SCRIPT_DIR/../scripts/bash/tenant-isolation-review.py"
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
ROOT="$WORK/tenants"; mkdir -p "$ROOT/alpha/keys" "$ROOT/beta/keys"; chmod 700 "$ROOT" "$ROOT/alpha" "$ROOT/beta"
printf 'alpha-key\n' > "$ROOT/alpha/keys/at-rest.key"; printf 'beta-key\n' > "$ROOT/beta/keys/at-rest.key"; chmod 600 "$ROOT"/*/keys/at-rest.key
PASS=0; FAIL=0
pass(){ echo "PASS: $1"; PASS=$((PASS+1)); }; fail(){ echo "FAIL: $1"; FAIL=$((FAIL+1)); }
if python3 "$R" --root "$ROOT" >/dev/null; then pass "isolated tenant partitions pass review"; else fail "valid partitions rejected"; fi
chmod 755 "$ROOT/alpha"
if python3 "$R" --root "$ROOT" >/dev/null 2>&1; then fail "world-readable tenant accepted"; else pass "world-readable tenant denied"; fi
chmod 700 "$ROOT/alpha"; ln -s /tmp "$ROOT/beta/escape"
if python3 "$R" --root "$ROOT" >/dev/null 2>&1; then fail "tenant symlink accepted"; else pass "tenant symlink denied"; fi
echo "===== TENANT ISOLATION REVIEW SUMMARY: PASS=$PASS FAIL=$FAIL ====="; [[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
S="$CASAN_HARNESS_ROOT/scripts/bash/upgrade-compatibility.sh"
M="$CASAN_APP_ROOT/infra/production/compatibility-matrix.json"
PASS=0; FAIL=0
pass(){ echo "PASS: $1"; PASS=$((PASS+1)); }; fail(){ echo "FAIL: $1"; FAIL=$((FAIL+1)); }
if bash "$S" check --from 1.0.0 --to 1.0.1 --matrix "$M" | grep -q '"decision": "allow"'; then pass "patch upgrade is explicitly allowed"; else fail "patch upgrade denied"; fi
if bash "$S" check --from 1.0.1 --to 1.0.0 --rollback --matrix "$M" | grep -q '"rollback": true'; then pass "approved rollback is explicitly allowed"; else fail "approved rollback denied"; fi
if bash "$S" check --from 1.0.1 --to 2.0.0 --matrix "$M" >/dev/null 2>&1; then fail "unreviewed major upgrade accepted"; else pass "unreviewed major upgrade denied"; fi
if bash "$S" check --from invalid --to 1.0.0 --matrix "$M" >/dev/null 2>&1; then fail "invalid version accepted"; else pass "invalid version denied"; fi
echo "===== UPGRADE COMPATIBILITY SUMMARY: PASS=$PASS FAIL=$FAIL ====="; [[ "$FAIL" -eq 0 ]]
+6 -2
View File
@@ -3,7 +3,8 @@
> 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
> 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
> `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. |
| 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. |
| 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 |
| 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 |
The authoritative customer-claim boundary is the
[`EDITION_FEATURE_LIMITATION_MATRIX.md`](../../docs/packaging/EDITION_FEATURE_LIMITATION_MATRIX.md).
## Build (preview)
```bash
scripts/package-release.sh platform # → dist/casan-platform-preview-vX.Y.Z.tar.gz
+32 -7
View File
@@ -50,15 +50,37 @@
"level": 3,
"package_names": ["casan-platform", "casan-control-panel"],
"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"],
"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)"],
"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)"],
"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", "attack-battery-viewer", "gitea-webhook evidence publishing"],
"includes": [
"packages/casan-platform",
"packages/casan-control-panel",
"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",
"docker_image": "casan-platform:{VERSION} (preview)"
@@ -67,7 +89,7 @@
"level": 4,
"package_names": ["casan-enterprise", "casan-governed-console"],
"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"],
"extends": "platform",
"implemented_components": [
@@ -76,9 +98,12 @@
"approval workflow (approval-verify.sh/approval-sign.sh/approval-jwt-mint.py)",
"KMS (vault-kms.sh)",
"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"],
"artifact": "casan-enterprise-preview-v{VERSION}.tar.gz",
"docker_image": "casan-enterprise:{VERSION} (future)"
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Generate a reproducible third-party inventory from the committed npm lockfile.
// This is an inventory, not legal advice; every release must regenerate and review it.
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
const [output = 'docs/commercial/THIRD_PARTY_SOFTWARE.json'] = process.argv.slice(2);
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
const exceptionsPath = 'docs/commercial/LICENSE_EXCEPTIONS.json';
const exceptions = existsSync(exceptionsPath) ? JSON.parse(readFileSync(exceptionsPath, 'utf8')).exceptions ?? {} : {};
const workspaceLicenses = new Map(Object.entries(lock.packages ?? {})
.filter(([path, value]) => path && !path.startsWith('node_modules/') && value && typeof value === 'object' && typeof value.name === 'string')
.map(([, value]) => [value.name, typeof value.license === 'string' ? value.license : 'UNKNOWN']));
const rows = Object.entries(lock.packages ?? {})
.filter(([path, value]) => path.startsWith('node_modules/') && value && typeof value === 'object')
.map(([path, value]) => {
const name = path.slice('node_modules/'.length);
const key = `${name}@${String(value.version ?? 'unknown')}`;
const declared = typeof value.license === 'string' ? value.license : (workspaceLicenses.get(name) ?? 'UNKNOWN');
const exception = exceptions[key];
return {
name,
version: String(value.version ?? 'unknown'),
declared_license: declared,
license: typeof exception?.reviewed_license === 'string' ? exception.reviewed_license : declared,
review_evidence: typeof exception?.evidence === 'string' ? exception.evidence : null,
resolved: typeof value.resolved === 'string' ? value.resolved : null,
integrity: typeof value.integrity === 'string' ? value.integrity : null,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
const licenses = {};
for (const row of rows) licenses[row.license] = (licenses[row.license] ?? 0) + 1;
const inventory = {
schema_version: '1.0',
generated_from: 'package-lock.json',
generated_at: new Date().toISOString(),
scope: 'npm dependencies recorded in the root lockfile; Python, container base images, model and dataset licenses require separate release review.',
exceptions_source: existsSync(exceptionsPath) ? exceptionsPath : null,
package_count: rows.length,
licenses,
packages: rows,
};
const destination = resolve(output);
mkdirSync(dirname(destination), { recursive: true });
writeFileSync(destination, `${JSON.stringify(inventory, null, 2)}\n`);
console.log(`LICENSE_INVENTORY_CREATED path=${output} packages=${rows.length} licenses=${Object.keys(licenses).length}`);
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
# Smoke the release exactly as a customer receives it. This intentionally
# extracts into a fresh directory and never resolves files from the source tree.
#
# Usage: scripts/smoke-release-artifact.sh dist/casan-<bundle>-vX.Y.Z.tar.gz
# Env: CASAN_ARTIFACT_SMOKE_RUN_TESTS=1 # platform: npm ci + tests + build
ARCHIVE="${1:-}"
[[ -n "$ARCHIVE" && -f "$ARCHIVE" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=archive_missing path=${ARCHIVE:-unset}" >&2
exit 64
}
ARCHIVE="$(cd "$(dirname "$ARCHIVE")" && pwd)/$(basename "$ARCHIVE")"
WORK="$(mktemp -d)"
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT
tar -xzf "$ARCHIVE" -C "$WORK"
roots=()
while IFS= read -r directory; do
roots+=("$directory")
done < <(find "$WORK" -mindepth 1 -maxdepth 1 -type d -print)
[[ "${#roots[@]}" -eq 1 ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=unexpected_root_count count=${#roots[@]}" >&2
exit 1
}
ROOT="${roots[0]}"
required=(BUNDLE-MANIFEST.txt SHA256SUMS VERSION bin/casan packages/casan-harness)
for path in "${required[@]}"; do
[[ -e "$ROOT/$path" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=required_path_missing path=$path" >&2
exit 1
}
done
(cd "$ROOT" && shasum -a 256 -c SHA256SUMS >/dev/null)
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
CLI_VERSION="$(cd "$ROOT" && ./bin/casan version)"
[[ "$CLI_VERSION" == "casan $VERSION" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=cli_version_mismatch expected=$VERSION actual=$CLI_VERSION" >&2
exit 1
}
(cd "$ROOT" && ./bin/casan help >/dev/null)
STATUS="$(awk '/^status:/ {print $2}' "$ROOT/BUNDLE-MANIFEST.txt")"
BUNDLE="$(awk '/^bundle:/ {print $2}' "$ROOT/BUNDLE-MANIFEST.txt")"
if [[ "$BUNDLE" == "platform" ]]; then
platform_required=(
packages/casan-control-panel/backend/package.json
packages/casan-control-panel/frontend/package.json
Dockerfile.control-panel-api
Dockerfile.control-panel-ui
docker-compose.control-panel.yml
docker-compose.control-panel.local.yml
nginx/control-panel.conf
infra/production/README.md
docs/security/CONTROL_PANEL_AUTH_HARDENING.md
)
for path in "${platform_required[@]}"; do
[[ -e "$ROOT/$path" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=platform_path_missing path=$path" >&2
exit 1
}
done
[[ -f "$ROOT/PREVIEW-INCOMPLETE.txt" ]] || {
echo "ARTIFACT_SMOKE_FAIL reason=preview_marker_missing" >&2
exit 1
}
if [[ "${CASAN_ARTIFACT_SMOKE_RUN_TESTS:-0}" == "1" ]]; then
command -v npm >/dev/null 2>&1 || {
echo "ARTIFACT_SMOKE_FAIL reason=npm_missing" >&2
exit 1
}
(cd "$ROOT" && npm ci && npm run console:test && npm run console:build)
fi
fi
echo "ARTIFACT_SMOKE_PASS bundle=$BUNDLE status=$STATUS version=$VERSION extracted=true checksums=true"