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:
@@ -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:
|
||||
|
||||
@@ -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: .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
CASAN Proprietary Commercial License
|
||||
|
||||
Copyright (c) CASAN copyright owner. All rights reserved.
|
||||
|
||||
This repository and its CASAN-authored source code, documentation, scripts,
|
||||
and release artifacts are proprietary. No permission is granted to use, copy,
|
||||
modify, distribute, sublicense, host, or create derivative works except under
|
||||
an executed written agreement with the copyright owner.
|
||||
|
||||
Third-party components remain subject to their own licenses. See
|
||||
docs/commercial/THIRD_PARTY_SOFTWARE.json and
|
||||
docs/commercial/LICENSE_AND_IP_DECISION.md.
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@ainative-okr/backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "prisma generate && tsc -p tsconfig.build.json",
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"purpose": "Manual license-review findings for packages whose committed lockfile does not contain license metadata.",
|
||||
"exceptions": {
|
||||
"busboy@1.6.0": {
|
||||
"reviewed_license": "MIT",
|
||||
"evidence": "https://github.com/mscdex/busboy/blob/master/LICENSE",
|
||||
"review_status": "requires-release-notice"
|
||||
},
|
||||
"streamsearch@1.1.0": {
|
||||
"reviewed_license": "MIT",
|
||||
"evidence": "https://github.com/mscdex/streamsearch/blob/master/LICENSE",
|
||||
"review_status": "requires-release-notice"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 製品概要
|
||||
|
||||
## 提供価値
|
||||
|
||||
CASAN は、既存の AI コーディングツールを置き換える製品ではありません。
|
||||
開発者、AI モデル、リポジトリ、CI の間に配置される独立した統制レイヤーです。
|
||||
AI が生成・提案した変更を、ポリシー、テスト、セキュリティ検査、承認、監査証跡で確認し、根拠を Evidence Pack として残します。
|
||||
|
||||
## PoC の対象ユースケース
|
||||
|
||||
「Claude Code、Codex、Copilot 等を利用する開発チームで、AI が関与した変更を
|
||||
merge 前に検査し、誰が何を行ったかを証明する。」
|
||||
|
||||
PoC では、1〜2 プロジェクト、1 つの Git リポジトリ連携、1 つの Domain Pack を対象とします。
|
||||
|
||||
## 提供範囲(CASAN Core + Control Center Lite)
|
||||
|
||||
- H1〜H7 のハーネス実行、アクションゲート、セキュリティ検査
|
||||
- Gitea CI からの検査起動と PASS/BLOCK 判定
|
||||
- Evidence Pack の作成、ハッシュ manifest の検証、Control Panel での閲覧
|
||||
- 監査ログ、ツール呼出記録、コスト/トークンの可視化
|
||||
- 承認が必要な変更を apply 前に停止するワークフロー
|
||||
|
||||
## PoC では提供しないもの
|
||||
|
||||
- 高可用性、RPO/RTO を伴う本番運用
|
||||
- 本番用 SSO/OIDC、顧客 CA、マネージド運用
|
||||
- KMS 必須運用、S3 Object Lock、外部ペネトレーションテスト
|
||||
- GitHub/GitLab/Jenkins の本番コネクタ保証
|
||||
- 自律エージェントによる無承認の変更適用
|
||||
|
||||
これらは Enterprise Pilot / Production の後続条件です。
|
||||
@@ -0,0 +1,30 @@
|
||||
# システム構成・データフロー
|
||||
|
||||
## 推奨配置
|
||||
|
||||
PoC では顧客管理のネットワーク内、または顧客クラウドアカウント内への self-hosted 配置を推奨します。
|
||||
|
||||
```text
|
||||
Developer / AI coding tool
|
||||
│ patch / PR
|
||||
▼
|
||||
Gitea ──► CASAN Runner ──► test / security / policy gates
|
||||
│
|
||||
├── Evidence Pack(manifest + reports)
|
||||
└── Control Panel(run、承認、証跡表示)
|
||||
```
|
||||
|
||||
## データフロー
|
||||
|
||||
1. 開発者が既存の AI ツールで変更を作成し、PR を作成します。
|
||||
2. Gitea workflow が CASAN を呼び出します。
|
||||
3. CASAN は許可されたワークスペースでポリシー、テスト、セキュリティを実行します。
|
||||
4. 判定、実行記録、ハッシュを Evidence Pack に保存します。
|
||||
5. Control Panel は Evidence Pack の manifest を再検証して表示します。
|
||||
6. PASS でない変更は、顧客定義のマージ/承認ルールに従って停止します。
|
||||
|
||||
## データの扱い
|
||||
|
||||
ソースコード、ログ、Evidence Pack は顧客環境内に保存する構成を基本とします。
|
||||
クラウドモデルへ送信するデータは、顧客が選択したモデル設定とポリシーに依存します。
|
||||
PoC 開始時に、送信対象、保存場所、保持期間、マスキング方針を Data Flow Sheet に記録します。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 導入・運用・制限事項
|
||||
|
||||
## 導入手順(PoC)
|
||||
|
||||
1. 顧客と対象リポジトリ、担当者、承認ルールを確定します。
|
||||
2. CASAN bundle を顧客環境へ配置し、Docker Compose または提供手順で起動します。
|
||||
3. Gitea workflow に CASAN gate を接続します。
|
||||
4. Domain Pack と閾値を対象プロジェクトに合わせます。
|
||||
5. テスト PR を用いて PASS/BLOCK/Evidence Pack を確認します。
|
||||
6. 運用開始後、週次で KPI と誤検知をレビューします。
|
||||
|
||||
## 日常運用
|
||||
|
||||
- 開発者: PR の CASAN verdict と必要な修正を確認します。
|
||||
- QA/セキュリティ: ブロック理由、Evidence Pack、例外承認を確認します。
|
||||
- 管理者: ポリシー変更、ロール、バックアップ、障害連絡を管理します。
|
||||
|
||||
## 既知の制限
|
||||
|
||||
- Evidence Pack の署名は、PoC 環境では鍵未設定の場合に unsigned となります。
|
||||
- テレメトリ完全性未検証またはコストスパイクがある場合、pack は certified になりません。
|
||||
- 実際の隔離強度は配置する runner/container 設定に依存します。Production 用 hardened sandbox は PoC の範囲外です。
|
||||
- Enterprise SSO、HA、DR、外部監査は PoC 完了条件には含みません。
|
||||
|
||||
## 障害時
|
||||
|
||||
モデル、runner、検査、または証跡生成が失敗した場合、CASAN は PASS を生成せず、失敗理由を記録します。
|
||||
復旧判断とマージ例外の承認は顧客の責任者が行います。
|
||||
@@ -0,0 +1,29 @@
|
||||
# PoC計画(8〜12週間)
|
||||
|
||||
## 成功条件
|
||||
|
||||
- 対象リポジトリで CASAN gate が PR ごとに実行されること
|
||||
- PASS/BLOCK の理由と Evidence Pack を担当者が確認できること
|
||||
- AI が関与した変更のテスト/セキュリティ/承認記録を追跡できること
|
||||
- 顧客と合意した KPI を最終報告書で提示できること
|
||||
|
||||
## 進め方
|
||||
|
||||
| 期間 | 内容 | 成果物 |
|
||||
|---|---|---|
|
||||
| 1〜2週 | キックオフ、対象選定、リスク確認 | PoC 設計、Data Flow Sheet |
|
||||
| 3〜4週 | 環境配置、Gitea 連携、Domain Pack | 導入記録、テスト PR |
|
||||
| 5〜8週 | 実運用、閾値調整、Evidence 確認 | 週次 KPI、課題一覧 |
|
||||
| 9〜12週 | 最終評価、次フェーズ判断 | PoC 結果報告、Pilot 提案 |
|
||||
|
||||
## KPI(開始時に基準値を合意)
|
||||
|
||||
- CASAN が実行された PR 比率
|
||||
- ブロックされたセキュリティ/ポリシー違反数
|
||||
- Evidence Pack が完全性検証に成功した比率
|
||||
- 誤検知レビュー件数と解決時間
|
||||
- AI 関連の変更に対する承認・追跡可能比率
|
||||
|
||||
## 顧客に必要な協力
|
||||
|
||||
対象リポジトリ、テスト環境、Gitea 管理者、セキュリティ/QA 窓口、承認ルール、及びクラウドモデル利用時のデータ送信判断を提供してください。
|
||||
@@ -0,0 +1,26 @@
|
||||
# セキュリティ・責任分界
|
||||
|
||||
## CASAN が提供する統制
|
||||
|
||||
- AI 出力/ツール実行に対するポリシーとセキュリティ検査
|
||||
- 実行・判定・承認の監査記録
|
||||
- Evidence Pack の SHA-256 manifest 検証
|
||||
- 失敗時に PASS を出さない fail-closed 動作
|
||||
|
||||
## 顧客の責任
|
||||
|
||||
- ネットワーク、ホスト、ID、リポジトリ権限、バックアップの管理
|
||||
- モデル provider と送信データの選定
|
||||
- 個人情報、秘密情報、保持期間、例外承認の決定
|
||||
- PoC 環境のアクセス権とログ閲覧権の管理
|
||||
|
||||
## 提供者の責任
|
||||
|
||||
- 合意した CASAN bundle、導入支援、障害一次切り分けを提供すること
|
||||
- 既知の制限、重大な脆弱性、PoC スコープ外の事項を明示すること
|
||||
- 顧客データを合意外の目的に使用しないこと
|
||||
|
||||
## 事前確認事項
|
||||
|
||||
APPI を含む適用法令、個人情報の有無、海外移転、subprocessor、データ削除/export、インシデント通知窓口は契約・Data Flow Sheet で合意します。
|
||||
この資料は法的助言ではありません。
|
||||
@@ -0,0 +1,19 @@
|
||||
# SLA / サポート条件ドラフト(PoC)
|
||||
|
||||
## 適用範囲
|
||||
|
||||
本ドラフトは PoC 期間の導入支援と障害対応の目安です。Production SLA ではありません。
|
||||
|
||||
| 区分 | 例 | 初回応答目標 |
|
||||
|---|---|---|
|
||||
| P1 | CASAN が全 PR で実行不能、証跡が作成不能 | 営業時間内 4 時間 |
|
||||
| P2 | 一部 gate/表示機能の不具合、回避策あり | 営業時間内 1 営業日 |
|
||||
| P3 | 質問、設定調整、改善要望 | 3 営業日以内 |
|
||||
|
||||
## 連絡とエスカレーション
|
||||
|
||||
顧客の PoC 責任者と提供者の技術窓口をキックオフ時に指定します。P1 の場合、run ID、発生時刻、影響範囲、表示された理由を添えて連絡します。
|
||||
|
||||
## 除外
|
||||
|
||||
顧客ネットワーク、顧客提供モデル、Gitea 自体、外部クラウド障害、未承認の構成変更は本ドラフトの保証対象外です。
|
||||
@@ -0,0 +1,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 のアクセス制御と保持期間
|
||||
@@ -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) で記録し、
|
||||
法務/プライバシー責任者の承認を得ます。
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"files": {
|
||||
"benign-fp-report.json": "52d3911e7b539f07391e187e3212f198f25e55d08396e2ab0e75d9db3178848b",
|
||||
"decision-log.md": "6975cabd29baff1fbc1c9ceca8a44323832ecc28eafe6bdd30fbe94981563d70",
|
||||
"h1-context-report.json": "64a1bbcdf38672eb1efcb8742e258850709297bd848a443fcf11b94b0f165d9d",
|
||||
"h2-tool-audit.json": "095b369ae21d47ecfcbc38387f3255118bc32a4c28327f19b27e0bfcd7c945ae",
|
||||
"h3-eval-scorecard.json": "b0e779fb219c4d9d38e55eefb15cd4370b4f0760e0a3bf70b5d22923407c8e47",
|
||||
"h4-security-report.json": "2828cf13b2f4edba852c9da77e1397b3b44933a3bd6d04c81b4ac96e31026e9d",
|
||||
"h5-audit-chain-proof.json": "f9918cc45311097be05ab852c36ed99170c65962f8c0f0e71b91d7468f9f2ba4",
|
||||
"h6-cost-telemetry.json": "e01cc551016f3d4b7d525e11cf9556481cb1376408daca6e2314aba9a3ddd088",
|
||||
"h7-orchestration-report.json": "9dd45a16004bd705c7307fae6c74975049a156a3dc1e14fc439ad816d5400066",
|
||||
"redteam-result.json": "341f7985a8cceb2cb3cc7705e644a4407ab27f7203c148abb98788dcca5e5c8e",
|
||||
"run-summary.json": "16cf96c55fa7b899e7b021b52a88cf3fa160366ab03e2361e431b0639196af29",
|
||||
"traceability-matrix.json": "011ac8ac6ab008f1048e3bce1e3f7b5b28bc969de7a1574fed86af16e259de27"
|
||||
},
|
||||
"manifest_head": "c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47"
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"generated": "deterministic (semantic OFF)",
|
||||
"policy": {
|
||||
"fp_max_pct": 3.0,
|
||||
"adv_block_min_pct": 95.0,
|
||||
"critical_block_pct": 100.0
|
||||
},
|
||||
"benign": {
|
||||
"by_lang": {
|
||||
"en": {
|
||||
"total": 32,
|
||||
"false_positives": 0
|
||||
},
|
||||
"vi": {
|
||||
"total": 32,
|
||||
"false_positives": 0
|
||||
},
|
||||
"ja": {
|
||||
"total": 31,
|
||||
"false_positives": 0
|
||||
}
|
||||
},
|
||||
"total": 95,
|
||||
"false_positives": 0,
|
||||
"false_positive_rate_pct": 0.0,
|
||||
"examples": []
|
||||
},
|
||||
"adversarial": {
|
||||
"total": 12,
|
||||
"blocked": 12,
|
||||
"block_rate_pct": 100.0,
|
||||
"missed": []
|
||||
},
|
||||
"critical": {
|
||||
"total": 4,
|
||||
"blocked": 4,
|
||||
"block_rate_pct": 100.0
|
||||
},
|
||||
"within_budget": true,
|
||||
"breaches": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# CASAN Evidence Pack — Decision Log
|
||||
|
||||
Run: `9534d91f-4518-41c5-a7c4-56e22a56c2a4`
|
||||
Certified: **False**
|
||||
Reasons: telemetry_integrity_not_verified, unresolved_cost_spike
|
||||
|
||||
## Gate outcomes
|
||||
|
||||
- H4 security: 186 records, 38 blocked
|
||||
- H5 audit chain: AUDIT_CHAIN_VALID anchor=signed last_hash=e715767a0d5ec67a8955c0e8e87262b5ca8c0f5826f1dee6583014ede8032bbe (ok=True)
|
||||
- H5 telemetry integrity: none (ok=False)
|
||||
- H6 cost: spike_detected, 56519 provider tokens
|
||||
- H2 tool audit: 24 records, chain_ok=True
|
||||
- H3 traceability: ok=True
|
||||
- Red-team: 16 vectors (block_rate=100.0%)
|
||||
|
||||
_Summaries only — no raw secret/PII content is copied into the pack._
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"harness": "H1-context",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"context_yaml": true,
|
||||
"note": "path/artifact validation performed by context-validate.sh at run time"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"harness": "H2-tool",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"records": 24,
|
||||
"denied": 6,
|
||||
"approved": 5,
|
||||
"chain_status": "TOOL_AUDIT_VALID anchor=signed last_hash=dda5685f038e171c7fdd8bb36b00793c3de7b14be7295fdfe34fff5e6a03c443",
|
||||
"chain_ok": true
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"harness": "H3-eval",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"judge_gate_tests": true,
|
||||
"note": "judge-gate fail-before/fix cycle proven by phase3-judge-gate-tests.sh"
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"harness": "H4-security",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"records": 186,
|
||||
"by_status": {
|
||||
"blocked": 38,
|
||||
"pass": 148
|
||||
},
|
||||
"blocked": 38
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"harness": "H5-governance",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"audit_chain": "AUDIT_CHAIN_VALID anchor=signed last_hash=e715767a0d5ec67a8955c0e8e87262b5ca8c0f5826f1dee6583014ede8032bbe",
|
||||
"audit_chain_ok": true,
|
||||
"telemetry_integrity": "none",
|
||||
"telemetry_ok": false
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"harness": "H6-agentops",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"provider_records": 56,
|
||||
"metric_records": 40,
|
||||
"total_provider_tokens": 56519,
|
||||
"cost_spike_rc": 2,
|
||||
"cost_spike_status": "spike_detected"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"harness": "H7-orchestration",
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"rollback_log": true,
|
||||
"note": "rollback/fallback/drift proven by adversarial + run-casan4 suites"
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
c55972046b1fe12b6bc538fde0ab31ad62e0c9e4b20003e5f1370f104d849f47
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"vectors_defined": 16,
|
||||
"critical_vectors": 4,
|
||||
"adversarial_block_rate_pct": 100.0,
|
||||
"critical_block_rate_pct": 100.0,
|
||||
"note": "block rates from benign-fp-report (deterministic layer); full suites: phase1-track-a + phase2-track-c"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"run_id": "9534d91f-4518-41c5-a7c4-56e22a56c2a4",
|
||||
"pack_version": "1.0-mvp",
|
||||
"certified": false,
|
||||
"certification_reasons": [
|
||||
"telemetry_integrity_not_verified",
|
||||
"unresolved_cost_spike"
|
||||
],
|
||||
"required_gates": [
|
||||
"H3-traceability",
|
||||
"H4-security",
|
||||
"H5-audit-chain",
|
||||
"H5-telemetry",
|
||||
"H6-cost",
|
||||
"benign-fp-budget"
|
||||
],
|
||||
"harness_reports": [
|
||||
"benign-fp-report.json",
|
||||
"h1-context-report.json",
|
||||
"h2-tool-audit.json",
|
||||
"h3-eval-scorecard.json",
|
||||
"h4-security-report.json",
|
||||
"h5-audit-chain-proof.json",
|
||||
"h6-cost-telemetry.json",
|
||||
"h7-orchestration-report.json",
|
||||
"redteam-result.json"
|
||||
]
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"generated_at": "2026-07-17T16:57:32Z",
|
||||
"requirements_source": "apps/okr/domain/input/okr-requirement.md",
|
||||
"mapping_source": "apps/okr/domain/traceability-map.json",
|
||||
"summary": {
|
||||
"requirements": 5,
|
||||
"passed": 5,
|
||||
"failed": 0,
|
||||
"symbol_refs": 9,
|
||||
"symbols_found": 9,
|
||||
"symbols_missing": 0,
|
||||
"orphan_mappings": []
|
||||
},
|
||||
"matrix": [
|
||||
{
|
||||
"id": "FR-01",
|
||||
"name": "Login",
|
||||
"status": "PASS",
|
||||
"code": [
|
||||
"apps/okr/backend/src/auth/auth.controller.ts",
|
||||
"apps/okr/backend/src/auth/auth.service.ts",
|
||||
"apps/okr/frontend/src/pages/Login.tsx",
|
||||
"apps/okr/frontend/src/hooks/useAuth.tsx"
|
||||
],
|
||||
"tests": [
|
||||
"apps/okr/backend/test/services.test.ts",
|
||||
"apps/okr/backend/test/e2e.test.ts"
|
||||
],
|
||||
"missing_code": [],
|
||||
"missing_tests": [],
|
||||
"symbol_refs": [
|
||||
{
|
||||
"file": "apps/okr/backend/src/auth/auth.service.ts",
|
||||
"symbol": "AuthService",
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"file": "apps/okr/backend/src/auth/auth.service.ts",
|
||||
"symbol": "login",
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"missing_symbols": [],
|
||||
"missing_lines": []
|
||||
},
|
||||
{
|
||||
"id": "FR-02",
|
||||
"name": "Create Objective",
|
||||
"status": "PASS",
|
||||
"code": [
|
||||
"apps/okr/backend/src/objectives/objectives.controller.ts",
|
||||
"apps/okr/backend/src/objectives/objectives.service.ts",
|
||||
"apps/okr/frontend/src/pages/CreateObjective.tsx",
|
||||
"apps/okr/frontend/src/schemas/objective.schema.ts"
|
||||
],
|
||||
"tests": [
|
||||
"apps/okr/backend/test/services.test.ts",
|
||||
"apps/okr/backend/test/e2e.test.ts",
|
||||
"apps/okr/frontend/src/__tests__/okr.test.tsx"
|
||||
],
|
||||
"missing_code": [],
|
||||
"missing_tests": [],
|
||||
"symbol_refs": [
|
||||
{
|
||||
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
|
||||
"symbol": "ObjectivesService",
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
|
||||
"symbol": "create",
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"missing_symbols": [],
|
||||
"missing_lines": []
|
||||
},
|
||||
{
|
||||
"id": "FR-03",
|
||||
"name": "Create Key Result",
|
||||
"status": "PASS",
|
||||
"code": [
|
||||
"apps/okr/backend/src/key-results/key-results.controller.ts",
|
||||
"apps/okr/backend/src/key-results/key-results.service.ts",
|
||||
"apps/okr/backend/src/key-results/dto/create-key-result.dto.ts"
|
||||
],
|
||||
"tests": [
|
||||
"apps/okr/backend/test/services.test.ts",
|
||||
"apps/okr/backend/test/e2e.test.ts"
|
||||
],
|
||||
"missing_code": [],
|
||||
"missing_tests": [],
|
||||
"symbol_refs": [
|
||||
{
|
||||
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
|
||||
"symbol": "KeyResultsService",
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
|
||||
"symbol": "create",
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"missing_symbols": [],
|
||||
"missing_lines": []
|
||||
},
|
||||
{
|
||||
"id": "FR-04",
|
||||
"name": "Update Progress",
|
||||
"status": "PASS",
|
||||
"code": [
|
||||
"apps/okr/backend/src/key-results/key-results.controller.ts",
|
||||
"apps/okr/backend/src/key-results/key-results.service.ts",
|
||||
"apps/okr/backend/src/key-results/dto/update-progress.dto.ts",
|
||||
"apps/okr/frontend/src/pages/KeyResultDetail.tsx"
|
||||
],
|
||||
"tests": [
|
||||
"apps/okr/backend/test/services.test.ts",
|
||||
"apps/okr/backend/test/e2e.test.ts",
|
||||
"apps/okr/frontend/src/__tests__/okr.test.tsx"
|
||||
],
|
||||
"missing_code": [],
|
||||
"missing_tests": [],
|
||||
"symbol_refs": [
|
||||
{
|
||||
"file": "apps/okr/backend/src/key-results/key-results.service.ts",
|
||||
"symbol": "updateProgress",
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"missing_symbols": [],
|
||||
"missing_lines": []
|
||||
},
|
||||
{
|
||||
"id": "FR-05",
|
||||
"name": "Dashboard",
|
||||
"status": "PASS",
|
||||
"code": [
|
||||
"apps/okr/backend/src/objectives/objectives.controller.ts",
|
||||
"apps/okr/backend/src/objectives/objectives.service.ts",
|
||||
"apps/okr/frontend/src/pages/Dashboard.tsx",
|
||||
"apps/okr/frontend/src/hooks/useObjectives.ts"
|
||||
],
|
||||
"tests": [
|
||||
"apps/okr/backend/test/services.test.ts",
|
||||
"apps/okr/backend/test/e2e.test.ts",
|
||||
"apps/okr/frontend/src/__tests__/okr.test.tsx"
|
||||
],
|
||||
"missing_code": [],
|
||||
"missing_tests": [],
|
||||
"symbol_refs": [
|
||||
{
|
||||
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
|
||||
"symbol": "ObjectivesService",
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"file": "apps/okr/backend/src/objectives/objectives.service.ts",
|
||||
"symbol": "list",
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"missing_symbols": [],
|
||||
"missing_lines": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,8 +11,8 @@ contents + maturity: [`packaging/levels.json`](../../packaging/levels.json).
|
||||
|---|---|:--:|---|
|
||||
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | H1–H7 harness, security + action gates, evidence pack, audit, cost/telemetry, hardening tests, policy/config defaults, `bin/casan` CLI |
|
||||
| **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.
|
||||
@@ -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+)"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
Generated
+5
@@ -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,6 +1,7 @@
|
||||
{
|
||||
"name": "ainative-okr-casan5",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"workspaces": [
|
||||
"apps/okr/backend",
|
||||
"apps/okr/frontend",
|
||||
|
||||
@@ -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 <run-id></code>; it will appear here only after the on-disk manifest is available.</p></div></Card>;
|
||||
return <div className="space-y-5"><div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-indigo-600">Assurance workspace</div><h1 className="mt-1 text-2xl font-semibold tracking-tight text-slate-900">Evidence packs <span className="text-slate-400">({heading})</span></h1><p className="mt-1 text-sm text-slate-500">Inspect certification, provenance, gates and immutable file hashes without opening raw logs.</p></div><div className="grid gap-5 xl:grid-cols-[288px_minmax(0,1fr)]"><aside className="rounded-2xl border border-slate-200 bg-white p-2 shadow-sm"><div className="px-3 py-2 text-[11px] font-bold uppercase tracking-[0.13em] text-slate-400">Available packs</div><div className="space-y-1.5">{data.packs.map((pack) => <PackRow key={pack.run_id} pack={pack} selected={pack.run_id === activeId} onSelect={() => setSelected(pack.run_id)} />)}</div></aside><main>{detail.isLoading && <div className="text-slate-500">Verifying manifest…</div>}{detail.isError && <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">The selected evidence pack could not be read safely.</div>}{detail.data && <Detail pack={detail.data} />}</main></div></div>;
|
||||
}
|
||||
@@ -19,8 +19,14 @@ Enterprise governed AI-SDLC console. Packages: `casan-enterprise`, `casan-govern
|
||||
| Loop governance | Plan-17 loop primitives (`loop-*.py`) |
|
||||
|
||||
## 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
@@ -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
|
||||
|
||||
@@ -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 ]]
|
||||
@@ -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
@@ -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)"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
// Generate a reproducible third-party inventory from the committed npm lockfile.
|
||||
// This is an inventory, not legal advice; every release must regenerate and review it.
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const [output = 'docs/commercial/THIRD_PARTY_SOFTWARE.json'] = process.argv.slice(2);
|
||||
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
|
||||
const exceptionsPath = 'docs/commercial/LICENSE_EXCEPTIONS.json';
|
||||
const exceptions = existsSync(exceptionsPath) ? JSON.parse(readFileSync(exceptionsPath, 'utf8')).exceptions ?? {} : {};
|
||||
const workspaceLicenses = new Map(Object.entries(lock.packages ?? {})
|
||||
.filter(([path, value]) => path && !path.startsWith('node_modules/') && value && typeof value === 'object' && typeof value.name === 'string')
|
||||
.map(([, value]) => [value.name, typeof value.license === 'string' ? value.license : 'UNKNOWN']));
|
||||
const rows = Object.entries(lock.packages ?? {})
|
||||
.filter(([path, value]) => path.startsWith('node_modules/') && value && typeof value === 'object')
|
||||
.map(([path, value]) => {
|
||||
const name = path.slice('node_modules/'.length);
|
||||
const key = `${name}@${String(value.version ?? 'unknown')}`;
|
||||
const declared = typeof value.license === 'string' ? value.license : (workspaceLicenses.get(name) ?? 'UNKNOWN');
|
||||
const exception = exceptions[key];
|
||||
return {
|
||||
name,
|
||||
version: String(value.version ?? 'unknown'),
|
||||
declared_license: declared,
|
||||
license: typeof exception?.reviewed_license === 'string' ? exception.reviewed_license : declared,
|
||||
review_evidence: typeof exception?.evidence === 'string' ? exception.evidence : null,
|
||||
resolved: typeof value.resolved === 'string' ? value.resolved : null,
|
||||
integrity: typeof value.integrity === 'string' ? value.integrity : null,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const licenses = {};
|
||||
for (const row of rows) licenses[row.license] = (licenses[row.license] ?? 0) + 1;
|
||||
const inventory = {
|
||||
schema_version: '1.0',
|
||||
generated_from: 'package-lock.json',
|
||||
generated_at: new Date().toISOString(),
|
||||
scope: 'npm dependencies recorded in the root lockfile; Python, container base images, model and dataset licenses require separate release review.',
|
||||
exceptions_source: existsSync(exceptionsPath) ? exceptionsPath : null,
|
||||
package_count: rows.length,
|
||||
licenses,
|
||||
packages: rows,
|
||||
};
|
||||
const destination = resolve(output);
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
writeFileSync(destination, `${JSON.stringify(inventory, null, 2)}\n`);
|
||||
console.log(`LICENSE_INVENTORY_CREATED path=${output} packages=${rows.length} licenses=${Object.keys(licenses).length}`);
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Smoke the release exactly as a customer receives it. This intentionally
|
||||
# extracts into a fresh directory and never resolves files from the source tree.
|
||||
#
|
||||
# Usage: scripts/smoke-release-artifact.sh dist/casan-<bundle>-vX.Y.Z.tar.gz
|
||||
# Env: CASAN_ARTIFACT_SMOKE_RUN_TESTS=1 # platform: npm ci + tests + build
|
||||
|
||||
ARCHIVE="${1:-}"
|
||||
[[ -n "$ARCHIVE" && -f "$ARCHIVE" ]] || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=archive_missing path=${ARCHIVE:-unset}" >&2
|
||||
exit 64
|
||||
}
|
||||
ARCHIVE="$(cd "$(dirname "$ARCHIVE")" && pwd)/$(basename "$ARCHIVE")"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
tar -xzf "$ARCHIVE" -C "$WORK"
|
||||
roots=()
|
||||
while IFS= read -r directory; do
|
||||
roots+=("$directory")
|
||||
done < <(find "$WORK" -mindepth 1 -maxdepth 1 -type d -print)
|
||||
[[ "${#roots[@]}" -eq 1 ]] || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=unexpected_root_count count=${#roots[@]}" >&2
|
||||
exit 1
|
||||
}
|
||||
ROOT="${roots[0]}"
|
||||
|
||||
required=(BUNDLE-MANIFEST.txt SHA256SUMS VERSION bin/casan packages/casan-harness)
|
||||
for path in "${required[@]}"; do
|
||||
[[ -e "$ROOT/$path" ]] || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=required_path_missing path=$path" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
(cd "$ROOT" && shasum -a 256 -c SHA256SUMS >/dev/null)
|
||||
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||
CLI_VERSION="$(cd "$ROOT" && ./bin/casan version)"
|
||||
[[ "$CLI_VERSION" == "casan $VERSION" ]] || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=cli_version_mismatch expected=$VERSION actual=$CLI_VERSION" >&2
|
||||
exit 1
|
||||
}
|
||||
(cd "$ROOT" && ./bin/casan help >/dev/null)
|
||||
|
||||
STATUS="$(awk '/^status:/ {print $2}' "$ROOT/BUNDLE-MANIFEST.txt")"
|
||||
BUNDLE="$(awk '/^bundle:/ {print $2}' "$ROOT/BUNDLE-MANIFEST.txt")"
|
||||
|
||||
if [[ "$BUNDLE" == "platform" ]]; then
|
||||
platform_required=(
|
||||
packages/casan-control-panel/backend/package.json
|
||||
packages/casan-control-panel/frontend/package.json
|
||||
Dockerfile.control-panel-api
|
||||
Dockerfile.control-panel-ui
|
||||
docker-compose.control-panel.yml
|
||||
docker-compose.control-panel.local.yml
|
||||
nginx/control-panel.conf
|
||||
infra/production/README.md
|
||||
docs/security/CONTROL_PANEL_AUTH_HARDENING.md
|
||||
)
|
||||
for path in "${platform_required[@]}"; do
|
||||
[[ -e "$ROOT/$path" ]] || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=platform_path_missing path=$path" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[[ -f "$ROOT/PREVIEW-INCOMPLETE.txt" ]] || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=preview_marker_missing" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${CASAN_ARTIFACT_SMOKE_RUN_TESTS:-0}" == "1" ]]; then
|
||||
command -v npm >/dev/null 2>&1 || {
|
||||
echo "ARTIFACT_SMOKE_FAIL reason=npm_missing" >&2
|
||||
exit 1
|
||||
}
|
||||
(cd "$ROOT" && npm ci && npm run console:test && npm run console:build)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "ARTIFACT_SMOKE_PASS bundle=$BUNDLE status=$STATUS version=$VERSION extracted=true checksums=true"
|
||||
Reference in New Issue
Block a user