feat: escalate chat approvals through inbox
This commit is contained in:
@@ -61,7 +61,7 @@
|
|||||||
| Future B1–B6 | 💤 vision | `CASAN_PLAN_FUTURE_PHASES.md` — approval workflow nâng cao · state machine · model benchmark · governed memory · auto-remediation · platform KPI. |
|
| Future B1–B6 | 💤 vision | `CASAN_PLAN_FUTURE_PHASES.md` — approval workflow nâng cao · state machine · model benchmark · governed memory · auto-remediation · platform KPI. |
|
||||||
| **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`. |
|
| **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. |
|
| **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 done+test; MVP-2 Track 4 + OPERATOR Track 5/6 + Track 8.1/8.2/8.3 done** · target arch còn planned | **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. **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 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. Test: chat suites **42/0** (`phase-chat-prompt-router` 8/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), Control Panel **26/0** + build xanh. Next: CODEGEN-as-loop + Track 8.4 approvals escalation → MVP-3 multi-tenant. |
|
| **18 Chat Console** | 🟡 **MVP-0 + MVP-1 done+test; MVP-2 Track 4 + OPERATOR Track 5/6 + Track 8.1/8.2/8.3/8.4 done** · target arch còn planned | **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. **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 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. Test: chat suites **46/0** (`phase-chat-prompt-router` 8/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), Control Panel **28/0** + build xanh. Next: CODEGEN-as-loop → MVP-3 multi-tenant. |
|
||||||
---
|
---
|
||||||
|
|
||||||
## Trần điểm & điều kiện lên "Strong (81+)"
|
## Trần điểm & điều kiện lên "Strong (81+)"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
| 15 | `CASAN_PLAN_15_RESPONSIBLE_AI_DATA_GOV.md` | Responsible AI & Data Governance (FPT §14.3–14.4) | 📋 |
|
| 15 | `CASAN_PLAN_15_RESPONSIBLE_AI_DATA_GOV.md` | Responsible AI & Data Governance (FPT §14.3–14.4) | 📋 |
|
||||||
| 16 | `CASAN_PLAN_16_SECURITY_AUDIT_REMEDIATION.md` | Security audit core harness + kế hoạch vá (tamper-evidence/injection/fail-open) | � P0/P1/P2 done |
|
| 16 | `CASAN_PLAN_16_SECURITY_AUDIT_REMEDIATION.md` | Security audit core harness + kế hoạch vá (tamper-evidence/injection/fail-open) | � P0/P1/P2 done |
|
||||||
| 17 | `CASAN_PLAN_17_LOOP_ENGINEERING.md` | Loop Engineering / Agentic Loop Governance (budget governor, convergence, verify-contract, loop trace/replay, meta-loop) | 📋 |
|
| 17 | `CASAN_PLAN_17_LOOP_ENGINEERING.md` | Loop Engineering / Agentic Loop Governance (budget governor, convergence, verify-contract, loop trace/replay, meta-loop) | 📋 |
|
||||||
| 18 | `CASAN_PLAN_18_CHAT_CONSOLE.md` | Governed Chat Console (target arch; **MVP-0 Ask CASAN + MVP-1 Operator done; MVP-2 Track 4/5/6 + Track 8.1/8.2/8.3 done** → approvals/CODEGEN → multi-tenant) | 🟡 MVP-0/1 + MVP-2 partial done |
|
| 18 | `CASAN_PLAN_18_CHAT_CONSOLE.md` | Governed Chat Console (target arch; **MVP-0 Ask CASAN + MVP-1 Operator done; MVP-2 Track 4/5/6 + Track 8.1/8.2/8.3/8.4 done** → CODEGEN → multi-tenant) | 🟡 MVP-0/1 + MVP-2 partial done |
|
||||||
| Future | `CASAN_PLAN_FUTURE_PHASES.md` | Approval workflow, state machine, benchmark, memory, remediation, KPI | 💤 vision |
|
| Future | `CASAN_PLAN_FUTURE_PHASES.md` | Approval workflow, state machine, benchmark, memory, remediation, KPI | 💤 vision |
|
||||||
|
|
||||||
> **Không có plan số 11:** số 11 được bỏ trống có chủ ý — nhánh eval/traceability đã
|
> **Không có plan số 11:** số 11 được bỏ trống có chủ ý — nhánh eval/traceability đã
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ package "DONE - Plan 13 Control Panel" #E8F5E9 {
|
|||||||
component "Local-prod deploy smoke\nTLS + oauth2-proxy + mock OIDC\nnginx auth_request" as local_prod #D5F5E3
|
component "Local-prod deploy smoke\nTLS + oauth2-proxy + mock OIDC\nnginx auth_request" as local_prod #D5F5E3
|
||||||
component "Command Center baseline\n9 evidence-backed widgets\nbriefing + ticker + evidence drawer" as command_done #D5F5E3
|
component "Command Center baseline\n9 evidence-backed widgets\nbriefing + ticker + evidence drawer" as command_done #D5F5E3
|
||||||
component "Loop/Chat widget\nbudget + replay drawer + chat ticker\nextends Command Center contract" as loop_widgets_done #D5F5E3
|
component "Loop/Chat widget\nbudget + replay drawer + chat ticker\nextends Command Center contract" as loop_widgets_done #D5F5E3
|
||||||
|
component "Approvals escalation\nturn ESCALATED -> inbox\nJWT + SoD + reason" as approvals_done #D5F5E3
|
||||||
}
|
}
|
||||||
|
|
||||||
package "NEXT - offline platform follow-ups" #FFF8E1 {
|
package "NEXT - offline platform follow-ups" #FFF8E1 {
|
||||||
component "RAI / Data Gov view\nretention + model-card + PII report" as rai #F9E79F
|
component "RAI / Data Gov view\nretention + model-card + PII report" as rai #F9E79F
|
||||||
component "Approvals escalation\nturn ESCALATED -> inbox\nJWT + SoD + reason" as approvals #F9E79F
|
|
||||||
component "CODEGEN-as-loop\nH4 guarded codegen\nloop-certified artifacts" as codegen_loop #F9E79F
|
component "CODEGEN-as-loop\nH4 guarded codegen\nloop-certified artifacts" as codegen_loop #F9E79F
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +43,10 @@ api_done --> local_prod : verified behind auth proxy
|
|||||||
api_done --> command_done : GET /api/v1/command
|
api_done --> command_done : GET /api/v1/command
|
||||||
command_done --> gov_core : provenance + audit state
|
command_done --> gov_core : provenance + audit state
|
||||||
loop_widgets_done --> command_done : specialized widget
|
loop_widgets_done --> command_done : specialized widget
|
||||||
|
approvals_done --> gov_core : proposals + SoD
|
||||||
|
|
||||||
lead --> rai : next if staying offline
|
lead --> rai : next if staying offline
|
||||||
rai --> gov_core : read RAI + policy artifacts
|
rai --> gov_core : read RAI + policy artifacts
|
||||||
approvals --> gov_core : proposals + SoD
|
|
||||||
codegen_loop --> gov_core : guarded loop-run
|
codegen_loop --> gov_core : guarded loop-run
|
||||||
|
|
||||||
managed_oidc ..> local_prod : replace mock IdP/cert
|
managed_oidc ..> local_prod : replace mock IdP/cert
|
||||||
@@ -55,7 +55,7 @@ billing ..> command_done : ground-truth FinOps
|
|||||||
|
|
||||||
note right of rai
|
note right of rai
|
||||||
Recommended next:
|
Recommended next:
|
||||||
1. If no external infra: build RAI view or Plan-18 approvals/CODEGEN slice.
|
1. If no external infra: build RAI view or Plan-18 CODEGEN slice.
|
||||||
2. If infra is available: promote local-prod OIDC to managed prod.
|
2. If infra is available: promote local-prod OIDC to managed prod.
|
||||||
3. Keep chat widgets on the existing Command envelope.
|
3. Keep chat widgets on the existing Command envelope.
|
||||||
4. Keep every metric evidence-backed with provenance.
|
4. Keep every metric evidence-backed with provenance.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# KẾ HOẠCH 18 — Governed Chat Console (Chat-as-Loop qua Control Plane)
|
# KẾ HOẠCH 18 — Governed Chat Console (Chat-as-Loop qua Control Plane)
|
||||||
|
|
||||||
> Status 2026-07-08: **🟡 MVP-0 + MVP-1 done+test; MVP-2 Track 4 + Operator Track 5/6 + Track 8.1/8.2/8.3 done — target architecture còn planned.**
|
> Status 2026-07-08: **🟡 MVP-0 + MVP-1 done+test; MVP-2 Track 4 + Operator Track 5/6 + Track 8.1/8.2/8.3/8.4 done — target architecture còn planned.**
|
||||||
> Đã implement **Ask CASAN — Read-only Evidence Assistant** qua harness + Control
|
> Đã implement **Ask CASAN — Read-only Evidence Assistant** qua harness + Control
|
||||||
> Panel (`/api/v1/chat/ask`, `/chat` UI): Prompt Router deterministic
|
> Panel (`/api/v1/chat/ask`, `/chat` UI): Prompt Router deterministic
|
||||||
> `READ_ONLY/OPERATOR/BLOCK/NOT_SUPPORTED`, context whitelist, H4 scan in/out, H5
|
> `READ_ONLY/OPERATOR/BLOCK/NOT_SUPPORTED`, context whitelist, H4 scan in/out, H5
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
> gate chọn agent, delegation hold, tool allowlist block, Control Panel agent picker.
|
> gate chọn agent, delegation hold, tool allowlist block, Control Panel agent picker.
|
||||||
> **Operator Track 5/6** đã bind mỗi side-effect turn vào Plan-17 `loop-run.sh`,
|
> **Operator Track 5/6** đã bind mỗi side-effect turn vào Plan-17 `loop-run.sh`,
|
||||||
> verify trace/replay, H4 preflight/context/tool-output scan, và chỉ release action
|
> verify trace/replay, H4 preflight/context/tool-output scan, và chỉ release action
|
||||||
> sau khi draft được certify. **Track 8.1/8.2/8.3** đã có replay/verify-chain
|
> sau khi draft được certify. **Track 8.1/8.2/8.3/8.4** đã có replay/verify-chain
|
||||||
> foundation và Command Center Chat/Loop widget đọc chat audit/replay evidence thật.
|
> foundation, Command Center Chat/Loop widget đọc chat audit/replay evidence thật,
|
||||||
|
> và escalation vào approvals inbox.
|
||||||
> Mục tiêu
|
> Mục tiêu
|
||||||
> tổng thể vẫn là thêm **cửa sổ chat** vào Control Plane (Plan-13) như một
|
> tổng thể vẫn là thêm **cửa sổ chat** vào Control Plane (Plan-13) như một
|
||||||
> **bề mặt tương tác của core harness** — mỗi lượt chat là **một loop-run được
|
> **bề mặt tương tác của core harness** — mỗi lượt chat là **một loop-run được
|
||||||
@@ -290,7 +291,7 @@ flowchart TD
|
|||||||
| 18.8.1 | ✅ **Replay** một turn (verify lại artifact đã ghi) phát hiện tamper/non-determinism | `chat-replay.py` + `loop-trace.py replay` | sửa artifact turn → replay lệch |
|
| 18.8.1 | ✅ **Replay** một turn (verify lại artifact đã ghi) phát hiện tamper/non-determinism | `chat-replay.py` + `loop-trace.py replay` | sửa artifact turn → replay lệch |
|
||||||
| 18.8.2 | ✅ `verify-chain` lịch sử chat (tamper-evidence, SEC-01/02; KMS khi TIER-2 là follow-up) | `chat-replay.py verify-chain` | sửa 1 turn → chain BREAK |
|
| 18.8.2 | ✅ `verify-chain` lịch sử chat (tamper-evidence, SEC-01/02; KMS khi TIER-2 là follow-up) | `chat-replay.py verify-chain` | sửa 1 turn → chain BREAK |
|
||||||
| 18.8.3 | ✅ Widget **Chat/Loop** trên Command Center (13 §8.6): ticker per-iteration, budget gauge, click → Evidence drawer | `GET /api/v1/command` + `/command` | số khớp fixture; click → evidence |
|
| 18.8.3 | ✅ Widget **Chat/Loop** trên Command Center (13 §8.6): ticker per-iteration, budget gauge, click → Evidence drawer | `GET /api/v1/command` + `/command` | số khớp fixture; click → evidence |
|
||||||
| 18.8.4 | **Approvals/Escalation panel**: turn `ESCALATED` vào approvals inbox (13 §3.4), duyệt/từ chối (JWT + SoD + lý do) | nối Plan-13 §3.4 | escalate → pending; JWT giả → DENY |
|
| 18.8.4 | ✅ **Approvals/Escalation panel**: turn `ESCALATED` vào approvals inbox (13 §3.4), duyệt/từ chối (JWT + SoD + lý do) | `chat-turn.py` + `approval-inbox.py` + `/approvals` | escalate → pending; JWT giả → DENY |
|
||||||
|
|
||||||
### Track 9 — Multi-tenant hardening `[MVP-3]`
|
### Track 9 — Multi-tenant hardening `[MVP-3]`
|
||||||
| Task | Việc | File | Verify (WSL) |
|
| Task | Việc | File | Verify (WSL) |
|
||||||
@@ -322,7 +323,8 @@ flowchart TD
|
|||||||
`phase-chat-readonly-tests.sh`, `phase-chat-session-audit-tests.sh`; MVP-1 —
|
`phase-chat-readonly-tests.sh`, `phase-chat-session-audit-tests.sh`; MVP-1 —
|
||||||
`phase-chat-operator-tests.sh`; MVP-2 — `phase-chat-agent-select-tests.sh`,
|
`phase-chat-operator-tests.sh`; MVP-2 — `phase-chat-agent-select-tests.sh`,
|
||||||
`phase-chat-pipeline-tests.sh`, `phase-chat-stream-hold-tests.sh`,
|
`phase-chat-pipeline-tests.sh`, `phase-chat-stream-hold-tests.sh`,
|
||||||
`phase-chat-replay-tests.sh`; MVP-3 — `phase-chat-tenant-tests.sh`. Chạy **WSL**
|
`phase-chat-replay-tests.sh`, `phase-chat-approval-tests.sh`; MVP-3 —
|
||||||
|
`phase-chat-tenant-tests.sh`. Chạy **WSL**
|
||||||
(deterministic, mock model; nhánh live cần Ollama SKIP-aware). Nối `ci-harness-gate.sh`;
|
(deterministic, mock model; nhánh live cần Ollama SKIP-aware). Nối `ci-harness-gate.sh`;
|
||||||
cập nhật tổng test ở `CASAN_HARDENING_STATUS.md`.
|
cập nhật tổng test ở `CASAN_HARDENING_STATUS.md`.
|
||||||
|
|
||||||
@@ -368,6 +370,12 @@ cập nhật tổng test ở `CASAN_HARDENING_STATUS.md`.
|
|||||||
- [x] `/command` renders the Chat/Loop card in the existing evidence drawer flow; ticker includes recent chat/loop turns.
|
- [x] `/command` renders the Chat/Loop card in the existing evidence drawer flow; ticker includes recent chat/loop turns.
|
||||||
- [x] Fixture test proves counts and budget gauge match evidence. Verify: `console:test` 26/0, `console:build` xanh, `phase-chat-replay` 4/0.
|
- [x] Fixture test proves counts and budget gauge match evidence. Verify: `console:test` 26/0, `console:build` xanh, `phase-chat-replay` 4/0.
|
||||||
|
|
||||||
|
**MVP-2 Track 8.4 (Approvals / escalation):**
|
||||||
|
- [x] Delegation escalation (`REQUIRES_APPROVAL`) from chat creates an `ESCALATED` ChatTurn and pending `chat.escalate` proposal in `approval-inbox.py`.
|
||||||
|
- [x] Approval inbox keeps SoD and reason requirements; strict mode / supplied approval JWT calls `approval-verify.sh` and fake JWT is denied before proposal mutation.
|
||||||
|
- [x] Control Panel `/approvals` can pass an approval JWT for strict review; dev mode remains backward-compatible.
|
||||||
|
- [x] Verify: `phase-chat-approval` 4/0, `phase-chat-agent-select` 8/0, `phase-chat-replay` 4/0, `console:test` 28/0, `console:build` xanh.
|
||||||
|
|
||||||
**Full (target architecture) — DoD tổng:**
|
**Full (target architecture) — DoD tổng:**
|
||||||
- [ ] Turn chạy **đúng như một loop-run Plan-17** (không định nghĩa vòng lặp riêng).
|
- [ ] Turn chạy **đúng như một loop-run Plan-17** (không định nghĩa vòng lặp riêng).
|
||||||
- [ ] **Không đường vòng:** test chứng minh bỏ bất kỳ gate nào (preflight/verify) → FAIL.
|
- [ ] **Không đường vòng:** test chứng minh bỏ bất kỳ gate nào (preflight/verify) → FAIL.
|
||||||
@@ -381,11 +389,11 @@ cập nhật tổng test ở `CASAN_HARDENING_STATUS.md`.
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 7. Ghi chú trung thực & Non-goals (không lan man)
|
## 7. Ghi chú trung thực & Non-goals (không lan man)
|
||||||
- **[MVP-0 + MVP-1 + MVP-2 Track 4 + Operator Track 5/6 + Track 8.1/8.2/8.3] — 🟡 done+test.**
|
- **[MVP-0 + MVP-1 + MVP-2 Track 4 + Operator Track 5/6 + Track 8.1/8.2/8.3/8.4] — 🟡 done+test.**
|
||||||
Ask CASAN read-only, Operator registered actions, agent/skill governance foundation,
|
Ask CASAN read-only, Operator registered actions, agent/skill governance foundation,
|
||||||
OPERATOR chat-as-loop/draft-hold, replay/verify-chain foundation, và Command Center
|
OPERATOR chat-as-loop/draft-hold, replay/verify-chain foundation, và Command Center
|
||||||
Chat/Loop widget đã có harness CLI + Control Panel API/UI. Các phase CODEGEN-as-loop /
|
Chat/Loop widget, approvals escalation đã có harness CLI + Control Panel API/UI. Các
|
||||||
approvals escalation / multi-tenant production vẫn chưa mở.
|
phase CODEGEN-as-loop / multi-tenant production vẫn chưa mở.
|
||||||
- **MVP-first (chống lan man):** **MVP-0 (Ask CASAN read-only) KHÔNG phụ thuộc Plan-17/
|
- **MVP-first (chống lan man):** **MVP-0 (Ask CASAN read-only) KHÔNG phụ thuộc Plan-17/
|
||||||
14/SEC-23** — làm được ngay trên nền H4/H5/H6 hiện có. Chỉ **MVP-2 trở đi** (chat-as-
|
14/SEC-23** — làm được ngay trên nền H4/H5/H6 hiện có. Chỉ **MVP-2 trở đi** (chat-as-
|
||||||
loop + agent) mới cần Plan-17 (T1–T3) + Plan-14 RBAC; **MVP-3** mới cần SEC-23 tenant.
|
loop + agent) mới cần Plan-17 (T1–T3) + Plan-14 RBAC; **MVP-3** mới cần SEC-23 tenant.
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ approval JWT thật ([16 SEC-07](CASAN_PLAN_16_SECURITY_AUDIT_REMEDIATION.md)),
|
|||||||
|---|---|---|:--:|:--:|---|
|
|---|---|---|:--:|:--:|---|
|
||||||
| B9 | ✅ **done** — **MVP-0 Ask CASAN read-only**: Prompt Router (Track 0) + Read-only Ask CASAN (Track 1) + session/audit (Track 2) + Control Panel API/UI (Track 7). H4 in/out, H5 audit hash-chain, H6 token, evidence sources; chat suites 14/0, Control Panel 23/0 + build xanh. Real model provider binding remains Track M follow-up when a provider is enabled. | [18 §1b, Track 0/1/2/7/M](CASAN_PLAN_18_CHAT_CONSOLE.md) | L | ✅ | done |
|
| B9 | ✅ **done** — **MVP-0 Ask CASAN read-only**: Prompt Router (Track 0) + Read-only Ask CASAN (Track 1) + session/audit (Track 2) + Control Panel API/UI (Track 7). H4 in/out, H5 audit hash-chain, H6 token, evidence sources; chat suites 14/0, Control Panel 23/0 + build xanh. Real model provider binding remains Track M follow-up when a provider is enabled. | [18 §1b, Track 0/1/2/7/M](CASAN_PLAN_18_CHAT_CONSOLE.md) | L | ✅ | done |
|
||||||
| B10 | ✅ **done** — **MVP-1 Operator mode**: registered actions `run tests` / `build pack` / `verify pack`, no free-command, all through `action-gate`, action artifacts with provenance, Control Panel quick actions. `phase-chat-operator` 8/0; total chat suites 24/0; Control Panel 24/0 + build xanh. | [18 Track 3](CASAN_PLAN_18_CHAT_CONSOLE.md) | M | ✅ | done |
|
| B10 | ✅ **done** — **MVP-1 Operator mode**: registered actions `run tests` / `build pack` / `verify pack`, no free-command, all through `action-gate`, action artifacts with provenance, Control Panel quick actions. `phase-chat-operator` 8/0; total chat suites 24/0; Control Panel 24/0 + build xanh. | [18 Track 3](CASAN_PLAN_18_CHAT_CONSOLE.md) | M | ✅ | done |
|
||||||
| B11 | 🟡 **partial done** — Track 4 Agent/Skill governance + OPERATOR Track 5/6 chat-as-loop/draft-hold + Track 8.1/8.2 replay foundation + Track 8.3 Command Center Chat/Loop widget. `chat-turn.py` certifies OPERATOR draft through Plan-17 `loop-run.sh`; `chat-replay.py` detects evidence artifact drift and chat-chain tamper; `/command` shows chat loop ticker/budget/evidence. Chat suites 42/0; Control Panel 26/0 + build xanh. **Remaining MVP-2:** CODEGEN-as-loop and Track 8.4 approvals escalation. | [18 Track 4/5/6/8](CASAN_PLAN_18_CHAT_CONSOLE.md) | L | 🔗 | B6, C7(RBAC), B10 |
|
| B11 | 🟡 **partial done** — Track 4 Agent/Skill governance + OPERATOR Track 5/6 chat-as-loop/draft-hold + Track 8.1/8.2 replay foundation + Track 8.3 Command Center Chat/Loop widget + Track 8.4 approvals escalation. `chat-turn.py` certifies OPERATOR draft through Plan-17 `loop-run.sh`; `chat-replay.py` detects evidence artifact drift and chat-chain tamper; `/command` shows chat loop ticker/budget/evidence; delegation escalation creates pending `chat.escalate` approvals. Chat suites 46/0; Control Panel 28/0 + build xanh. **Remaining MVP-2:** CODEGEN-as-loop. | [18 Track 4/5/6/8](CASAN_PLAN_18_CHAT_CONSOLE.md) | L | 🔗 | B6, C7(RBAC), B10 |
|
||||||
| B12 | ✅ **done** — Widget Loop/Chat trên Command Center (`chat_loop` widget, loop ticker, budget gauge, replay evidence drawer) | [17 (17.22)](CASAN_PLAN_17_LOOP_ENGINEERING.md) · [18 Track 8](CASAN_PLAN_18_CHAT_CONSOLE.md) | M | ✅ | C5 ✅ baseline |
|
| B12 | ✅ **done** — Widget Loop/Chat trên Command Center (`chat_loop` widget, loop ticker, budget gauge, replay evidence drawer) | [17 (17.22)](CASAN_PLAN_17_LOOP_ENGINEERING.md) · [18 Track 8](CASAN_PLAN_18_CHAT_CONSOLE.md) | M | ✅ | C5 ✅ baseline |
|
||||||
| B13 | **MVP-3** multi-tenant chat hardening (Track 9) | [18 Track 9](CASAN_PLAN_18_CHAT_CONSOLE.md) | M | 🔗 | A3(SEC-23) |
|
| B13 | **MVP-3** multi-tenant chat hardening (Track 9) | [18 Track 9](CASAN_PLAN_18_CHAT_CONSOLE.md) | M | 🔗 | A3(SEC-23) |
|
||||||
|
|
||||||
|
|||||||
@@ -53,10 +53,11 @@ Approval inbox / HITL:
|
|||||||
- `GET /api/v1/approvals?status=pending` — list proposals and oversight tail.
|
- `GET /api/v1/approvals?status=pending` — list proposals and oversight tail.
|
||||||
- `POST /api/v1/approvals/submit` — submit a governed proposal; delegation is resolved
|
- `POST /api/v1/approvals/submit` — submit a governed proposal; delegation is resolved
|
||||||
by harness `approval-inbox.py` + `delegation-policy.yaml`.
|
by harness `approval-inbox.py` + `delegation-policy.yaml`.
|
||||||
- `POST /api/v1/approvals/decide` — approve/reject with SoD and reason; approved
|
- `POST /api/v1/approvals/decide` — approve/reject with SoD and reason; strict mode
|
||||||
|
or supplied `approvalJwt` is verified by harness `approval-verify.sh`; approved
|
||||||
settings proposals apply through `control-plane-settings.py`.
|
settings proposals apply through `control-plane-settings.py`.
|
||||||
|
|
||||||
Governed Chat (Plan-18 MVP-0/1 + MVP-2 Track 4, Operator Track 5/6, and Track 8.1-8.3):
|
Governed Chat (Plan-18 MVP-0/1 + MVP-2 Track 4, Operator Track 5/6, and Track 8.1-8.4):
|
||||||
|
|
||||||
- `POST /api/v1/chat/ask` — Ask CASAN endpoint. The API only wraps harness
|
- `POST /api/v1/chat/ask` — Ask CASAN endpoint. The API only wraps harness
|
||||||
`chat-turn.py`; router verdicts, H4 input/output scan, action-gate decisions,
|
`chat-turn.py`; router verdicts, H4 input/output scan, action-gate decisions,
|
||||||
@@ -73,12 +74,15 @@ Governed Chat (Plan-18 MVP-0/1 + MVP-2 Track 4, Operator Track 5/6, and Track 8.
|
|||||||
`chat-replay.py`.
|
`chat-replay.py`.
|
||||||
- `GET /api/v1/command` — includes the `chat_loop` Command Center widget backed
|
- `GET /api/v1/command` — includes the `chat_loop` Command Center widget backed
|
||||||
by chat audit/replay evidence, loop ticker rows, and token budget gauge.
|
by chat audit/replay evidence, loop ticker rows, and token budget gauge.
|
||||||
|
- Delegation escalation returns `ESCALATED` and creates a pending `chat.escalate`
|
||||||
|
proposal in the approval inbox.
|
||||||
- `/chat` UI shows actor/role scope, `mode/risk/decision` badges, certified answer,
|
- `/chat` UI shows actor/role scope, `mode/risk/decision` badges, certified answer,
|
||||||
evidence sources, registered operator actions, agent binding, loop certification,
|
evidence sources, registered operator actions, agent binding, loop certification,
|
||||||
action-gate status, router details, and audit hash. Side-effect requests outside
|
action-gate status, router details, and audit hash. Side-effect requests outside
|
||||||
registered actions return governed `BLOCK` or `NOT_SUPPORTED` responses; operator
|
registered actions return governed `BLOCK` or `NOT_SUPPORTED` responses; operator
|
||||||
side effects are held until the loop draft is certified.
|
side effects are held until the loop draft is certified.
|
||||||
- `/command` UI renders the Chat/Loop widget in the existing evidence drawer flow.
|
- `/command` UI renders the Chat/Loop widget in the existing evidence drawer flow.
|
||||||
|
- `/approvals` UI accepts an approval JWT for strict reviewer identity checks.
|
||||||
|
|
||||||
FinOps/SLO:
|
FinOps/SLO:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { APP_ROOT } from '../common/app-root.js';
|
import { APP_ROOT } from '../common/app-root.js';
|
||||||
import type { SettingsActor } from '../settings/settings.service.js';
|
import type { SettingsActor } from '../settings/settings.service.js';
|
||||||
@@ -17,10 +19,12 @@ export interface ApprovalDecision {
|
|||||||
id: string;
|
id: string;
|
||||||
decision: 'approve' | 'reject';
|
decision: 'approve' | 'reject';
|
||||||
reason: string;
|
reason: string;
|
||||||
|
approvalJwt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HARNESS_BIN = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash');
|
const HARNESS_BIN = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash');
|
||||||
const INBOX_CLI = join(HARNESS_BIN, 'approval-inbox.py');
|
const INBOX_CLI = join(HARNESS_BIN, 'approval-inbox.py');
|
||||||
|
const APPROVAL_VERIFY = join(HARNESS_BIN, 'approval-verify.sh');
|
||||||
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
|
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
|
||||||
const CP_CLI = join(HARNESS_BIN, 'control-plane-settings.py');
|
const CP_CLI = join(HARNESS_BIN, 'control-plane-settings.py');
|
||||||
|
|
||||||
@@ -59,6 +63,16 @@ function parseJson<T>(raw: string, fallback: T): T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stableJson(value: unknown): string {
|
||||||
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const obj = value as Record<string, unknown>;
|
||||||
|
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`;
|
||||||
|
}
|
||||||
|
if (value === undefined) return 'null';
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApprovalsService {
|
export class ApprovalsService {
|
||||||
list(actor: SettingsActor, status = 'pending') {
|
list(actor: SettingsActor, status = 'pending') {
|
||||||
@@ -105,6 +119,8 @@ export class ApprovalsService {
|
|||||||
}
|
}
|
||||||
this.requireRbac(actor, 'approval', 'grant');
|
this.requireRbac(actor, 'approval', 'grant');
|
||||||
try {
|
try {
|
||||||
|
const pending = this.findProposal(input.id);
|
||||||
|
this.verifyApprovalIdentity(input, actor, pending);
|
||||||
const res = runFile('python3', [
|
const res = runFile('python3', [
|
||||||
INBOX_CLI,
|
INBOX_CLI,
|
||||||
'decide',
|
'decide',
|
||||||
@@ -121,6 +137,7 @@ export class ApprovalsService {
|
|||||||
const applied = input.decision === 'approve' ? this.applyApprovedProposal(proposal, actor) : null;
|
const applied = input.decision === 'approve' ? this.applyApprovedProposal(proposal, actor) : null;
|
||||||
return { proposal, applied, audit_verify: this.verifyAudit() };
|
return { proposal, applied, audit_verify: this.verifyAudit() };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
if (err instanceof ForbiddenException) throw err;
|
||||||
if (Number(err.status) === 3 || Number(err.status) === 1) {
|
if (Number(err.status) === 3 || Number(err.status) === 1) {
|
||||||
throw new ForbiddenException(err.stderr || err.message);
|
throw new ForbiddenException(err.stderr || err.message);
|
||||||
}
|
}
|
||||||
@@ -128,6 +145,42 @@ export class ApprovalsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private findProposal(id: string) {
|
||||||
|
const res = runFile('python3', [INBOX_CLI, 'list', '--status', 'all']);
|
||||||
|
const store = parseJson<Record<string, any>>(res.stdout, { proposals: [] });
|
||||||
|
const proposal = (store.proposals ?? []).find((p: Record<string, any>) => p.id === id);
|
||||||
|
if (!proposal) throw new ForbiddenException(`APPROVAL_DECIDE_DENY unknown_id ${id}`);
|
||||||
|
return proposal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private verifyApprovalIdentity(input: ApprovalDecision, actor: SettingsActor, proposal: Record<string, any>) {
|
||||||
|
const strict = process.env.CASAN_APPROVAL_STRICT === '1' || process.env.CASAN_PROFILE === 'prod' || Boolean(input.approvalJwt);
|
||||||
|
if (!strict) return;
|
||||||
|
const work = mkdtempSync(join(tmpdir(), 'cp-approval-verify-'));
|
||||||
|
const inputPath = join(work, 'approval-input.json');
|
||||||
|
try {
|
||||||
|
writeFileSync(inputPath, stableJson({
|
||||||
|
id: proposal.id,
|
||||||
|
action: proposal.action,
|
||||||
|
target: proposal.target,
|
||||||
|
proposer: proposal.proposer,
|
||||||
|
payload: proposal.payload ?? {},
|
||||||
|
}));
|
||||||
|
runFile('bash', [
|
||||||
|
APPROVAL_VERIFY,
|
||||||
|
String(proposal.action ?? 'default'),
|
||||||
|
String(proposal.proposer ?? ''),
|
||||||
|
inputPath,
|
||||||
|
actor.actor,
|
||||||
|
'-',
|
||||||
|
], input.approvalJwt ? { CASAN_APPROVAL_JWT: input.approvalJwt } : undefined);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new ForbiddenException(err.stderr || err.message || 'APPROVAL_DECIDE_DENY approval identity failed');
|
||||||
|
} finally {
|
||||||
|
rmSync(work, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applyApprovedProposal(proposal: Record<string, any>, actor: SettingsActor) {
|
private applyApprovedProposal(proposal: Record<string, any>, actor: SettingsActor) {
|
||||||
if (proposal.action !== 'settings.write') return null;
|
if (proposal.action !== 'settings.write') return null;
|
||||||
const key = proposal.payload?.key;
|
const key = proposal.payload?.key;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const orgAdmin = { actor: 'root', role: 'org-admin', project: 'default', tenant:
|
|||||||
|
|
||||||
function withTempGovernance(fn: (paths: { inbox: string; store: string }) => void) {
|
function withTempGovernance(fn: (paths: { inbox: string; store: string }) => void) {
|
||||||
const prevInbox = process.env.CASAN_APPROVAL_INBOX_FILE;
|
const prevInbox = process.env.CASAN_APPROVAL_INBOX_FILE;
|
||||||
|
const prevStrict = process.env.CASAN_APPROVAL_STRICT;
|
||||||
const prevStore = process.env.CASAN_CP_STORE_FILE;
|
const prevStore = process.env.CASAN_CP_STORE_FILE;
|
||||||
const prevKeyDir = process.env.CASAN_CP_KEY_DIR;
|
const prevKeyDir = process.env.CASAN_CP_KEY_DIR;
|
||||||
const prevPub = process.env.CASAN_CP_PUB;
|
const prevPub = process.env.CASAN_CP_PUB;
|
||||||
@@ -25,6 +26,8 @@ function withTempGovernance(fn: (paths: { inbox: string; store: string }) => voi
|
|||||||
} finally {
|
} finally {
|
||||||
if (prevInbox === undefined) delete process.env.CASAN_APPROVAL_INBOX_FILE;
|
if (prevInbox === undefined) delete process.env.CASAN_APPROVAL_INBOX_FILE;
|
||||||
else process.env.CASAN_APPROVAL_INBOX_FILE = prevInbox;
|
else process.env.CASAN_APPROVAL_INBOX_FILE = prevInbox;
|
||||||
|
if (prevStrict === undefined) delete process.env.CASAN_APPROVAL_STRICT;
|
||||||
|
else process.env.CASAN_APPROVAL_STRICT = prevStrict;
|
||||||
if (prevStore === undefined) delete process.env.CASAN_CP_STORE_FILE;
|
if (prevStore === undefined) delete process.env.CASAN_CP_STORE_FILE;
|
||||||
else process.env.CASAN_CP_STORE_FILE = prevStore;
|
else process.env.CASAN_CP_STORE_FILE = prevStore;
|
||||||
if (prevKeyDir === undefined) delete process.env.CASAN_CP_KEY_DIR;
|
if (prevKeyDir === undefined) delete process.env.CASAN_CP_KEY_DIR;
|
||||||
@@ -62,6 +65,28 @@ test('approval inbox submit -> approve applies governed setting and writes overs
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('approval inbox denies forged JWT in strict mode without deciding proposal', () => {
|
||||||
|
withTempGovernance(({ inbox }) => {
|
||||||
|
const svc = new ApprovalsService();
|
||||||
|
const submitted = svc.submit({
|
||||||
|
action: 'settings.write',
|
||||||
|
target: 'security.strict',
|
||||||
|
risk: 'high',
|
||||||
|
sensitive: true,
|
||||||
|
reason: 'strict approval required',
|
||||||
|
payload: { key: 'security.strict', value: true },
|
||||||
|
}, projectAdmin) as any;
|
||||||
|
process.env.CASAN_APPROVAL_STRICT = '1';
|
||||||
|
assert.throws(
|
||||||
|
() => svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'forged jwt', approvalJwt: 'fake.jwt.token' }, approver),
|
||||||
|
ForbiddenException,
|
||||||
|
);
|
||||||
|
const inboxRaw = JSON.parse(readFileSync(inbox, 'utf8'));
|
||||||
|
assert.equal(inboxRaw.proposals[0].status, 'pending');
|
||||||
|
assert.equal(inboxRaw.oversight.length, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('approval inbox enforces separation of duties', () => {
|
test('approval inbox enforces separation of duties', () => {
|
||||||
withTempGovernance(() => {
|
withTempGovernance(() => {
|
||||||
const svc = new ApprovalsService();
|
const svc = new ApprovalsService();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { mkdtempSync } from 'node:fs';
|
import { mkdtempSync, readFileSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { ChatService } from '../src/chat/chat.service.js';
|
import { ChatService } from '../src/chat/chat.service.js';
|
||||||
@@ -14,8 +14,11 @@ function withTempChatState(fn: () => void) {
|
|||||||
CASAN_CHAT_AUDIT_LOG: process.env.CASAN_CHAT_AUDIT_LOG,
|
CASAN_CHAT_AUDIT_LOG: process.env.CASAN_CHAT_AUDIT_LOG,
|
||||||
CASAN_CHAT_AUDIT_HEAD: process.env.CASAN_CHAT_AUDIT_HEAD,
|
CASAN_CHAT_AUDIT_HEAD: process.env.CASAN_CHAT_AUDIT_HEAD,
|
||||||
CASAN_CHAT_METRICS_LOG: process.env.CASAN_CHAT_METRICS_LOG,
|
CASAN_CHAT_METRICS_LOG: process.env.CASAN_CHAT_METRICS_LOG,
|
||||||
|
CASAN_APPROVAL_INBOX_FILE: process.env.CASAN_APPROVAL_INBOX_FILE,
|
||||||
};
|
};
|
||||||
process.env.CASAN_STATE_ROOT = mkdtempSync(join(tmpdir(), 'cp-chat-'));
|
const state = mkdtempSync(join(tmpdir(), 'cp-chat-'));
|
||||||
|
process.env.CASAN_STATE_ROOT = state;
|
||||||
|
process.env.CASAN_APPROVAL_INBOX_FILE = join(state, 'approval-inbox.json');
|
||||||
delete process.env.CASAN_CHAT_AUDIT_LOG;
|
delete process.env.CASAN_CHAT_AUDIT_LOG;
|
||||||
delete process.env.CASAN_CHAT_AUDIT_HEAD;
|
delete process.env.CASAN_CHAT_AUDIT_HEAD;
|
||||||
delete process.env.CASAN_CHAT_METRICS_LOG;
|
delete process.env.CASAN_CHAT_METRICS_LOG;
|
||||||
@@ -110,3 +113,25 @@ test('chat ask denies selected agent outside actor role', () => {
|
|||||||
assert.equal(res.reason, 'role_not_allowed_for_agent');
|
assert.equal(res.reason, 'role_not_allowed_for_agent');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('chat ask escalates delegation approval into approval inbox', () => {
|
||||||
|
withTempChatState(() => {
|
||||||
|
const svc = new ChatService();
|
||||||
|
const res = svc.ask({
|
||||||
|
message: 'Summarize Plan 18 MVP-2 status',
|
||||||
|
chatId: 'approval-chat',
|
||||||
|
delegationLevel: 1,
|
||||||
|
}, viewer) as any;
|
||||||
|
assert.equal(res.success, false);
|
||||||
|
assert.equal(res.decision, 'ESCALATED');
|
||||||
|
assert.equal(res.agent_binding.decision, 'REQUIRES_APPROVAL');
|
||||||
|
assert.equal(res.approval.proposal.status, 'pending');
|
||||||
|
assert.equal(res.approval.proposal.action, 'chat.escalate');
|
||||||
|
assert.equal(res.audit_verify.ok, true);
|
||||||
|
|
||||||
|
const inbox = JSON.parse(readFileSync(process.env.CASAN_APPROVAL_INBOX_FILE!, 'utf8'));
|
||||||
|
assert.equal(inbox.proposals.length, 1);
|
||||||
|
assert.equal(inbox.proposals[0].payload.chat_id, 'approval-chat');
|
||||||
|
assert.equal(inbox.proposals[0].payload.agent_binding.decision, 'REQUIRES_APPROVAL');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export const api = {
|
|||||||
approvals: (actor: SettingsActor, status = 'pending') => getWithHeaders<ApprovalsState>(`approvals?status=${status}`, actorHeaders(actor)),
|
approvals: (actor: SettingsActor, status = 'pending') => getWithHeaders<ApprovalsState>(`approvals?status=${status}`, actorHeaders(actor)),
|
||||||
submitApproval: (actor: SettingsActor, body: { action: string; target: string; risk?: string; sensitive?: boolean; reason: string; payload?: Record<string, unknown> }) =>
|
submitApproval: (actor: SettingsActor, body: { action: string; target: string; risk?: string; sensitive?: boolean; reason: string; payload?: Record<string, unknown> }) =>
|
||||||
post<{ proposal: any; audit_verify: { ok: boolean; output: string } }>('approvals/submit', body, actorHeaders(actor)),
|
post<{ proposal: any; audit_verify: { ok: boolean; output: string } }>('approvals/submit', body, actorHeaders(actor)),
|
||||||
decideApproval: (actor: SettingsActor, body: { id: string; decision: 'approve' | 'reject'; reason: string }) =>
|
decideApproval: (actor: SettingsActor, body: { id: string; decision: 'approve' | 'reject'; reason: string; approvalJwt?: string }) =>
|
||||||
post<{ proposal: any; applied: any; audit_verify: { ok: boolean; output: string } }>('approvals/decide', body, actorHeaders(actor)),
|
post<{ proposal: any; applied: any; audit_verify: { ok: boolean; output: string } }>('approvals/decide', body, actorHeaders(actor)),
|
||||||
askChat: (actor: SettingsActor, body: { message: string; chatId?: string; agentId?: string; skillId?: string; delegationLevel?: number }) =>
|
askChat: (actor: SettingsActor, body: { message: string; chatId?: string; agentId?: string; skillId?: string; delegationLevel?: number }) =>
|
||||||
post<ChatAnswer>('chat/ask', body, actorHeaders(actor)),
|
post<ChatAnswer>('chat/ask', body, actorHeaders(actor)),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function Approvals() {
|
|||||||
const [sensitive, setSensitive] = useState(true);
|
const [sensitive, setSensitive] = useState(true);
|
||||||
const [reason, setReason] = useState('review requested from Control Panel');
|
const [reason, setReason] = useState('review requested from Control Panel');
|
||||||
const [decisionReason, setDecisionReason] = useState('reviewed in approval inbox');
|
const [decisionReason, setDecisionReason] = useState('reviewed in approval inbox');
|
||||||
|
const [approvalJwt, setApprovalJwt] = useState('');
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const inbox = useQuery({
|
const inbox = useQuery({
|
||||||
@@ -49,7 +50,7 @@ export function Approvals() {
|
|||||||
|
|
||||||
const decide = useMutation({
|
const decide = useMutation({
|
||||||
mutationFn: ({ id, decision }: { id: string; decision: 'approve' | 'reject' }) =>
|
mutationFn: ({ id, decision }: { id: string; decision: 'approve' | 'reject' }) =>
|
||||||
api.decideApproval(actor, { id, decision, reason: decisionReason }),
|
api.decideApproval(actor, { id, decision, reason: decisionReason, approvalJwt: approvalJwt || undefined }),
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
setMessage(`${res.proposal.status} ${res.proposal.id}${res.applied ? ` applied v${res.applied.version}` : ''}`);
|
setMessage(`${res.proposal.status} ${res.proposal.id}${res.applied ? ` applied v${res.applied.version}` : ''}`);
|
||||||
void queryClient.invalidateQueries({ queryKey: ['approvals'] });
|
void queryClient.invalidateQueries({ queryKey: ['approvals'] });
|
||||||
@@ -129,6 +130,10 @@ export function Approvals() {
|
|||||||
<span className="text-gray-500">Decision reason</span>
|
<span className="text-gray-500">Decision reason</span>
|
||||||
<input className="w-full rounded border border-gray-300 px-3 py-2" value={decisionReason} onChange={(e) => setDecisionReason(e.target.value)} />
|
<input className="w-full rounded border border-gray-300 px-3 py-2" value={decisionReason} onChange={(e) => setDecisionReason(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
|
<label className="block space-y-1 text-sm">
|
||||||
|
<span className="text-gray-500">Approval JWT</span>
|
||||||
|
<input className="w-full rounded border border-gray-300 px-3 py-2 font-mono text-xs" value={approvalJwt} onChange={(e) => setApprovalJwt(e.target.value)} />
|
||||||
|
</label>
|
||||||
{inbox.data.proposals.map((p) => (
|
{inbox.data.proposals.map((p) => (
|
||||||
<div key={p.id} className="rounded border border-gray-200 p-4">
|
<div key={p.id} className="rounded border border-gray-200 p-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ ROUTER = os.path.join(BIN, "prompt-mode-router.py")
|
|||||||
AGENT_RESOLVER = os.path.join(BIN, "chat-agent-resolver.py")
|
AGENT_RESOLVER = os.path.join(BIN, "chat-agent-resolver.py")
|
||||||
READONLY = os.path.join(BIN, "chat-readonly.py")
|
READONLY = os.path.join(BIN, "chat-readonly.py")
|
||||||
OPERATOR = os.path.join(BIN, "chat-operator.py")
|
OPERATOR = os.path.join(BIN, "chat-operator.py")
|
||||||
|
APPROVAL_INBOX = os.path.join(BIN, "approval-inbox.py")
|
||||||
LOOP_RUN = os.path.join(BIN, "loop-run.sh")
|
LOOP_RUN = os.path.join(BIN, "loop-run.sh")
|
||||||
LOOP_TRACE = os.path.join(BIN, "loop-trace.py")
|
LOOP_TRACE = os.path.join(BIN, "loop-trace.py")
|
||||||
PREFLIGHT = os.path.join(BIN, "harness-preflight.sh")
|
PREFLIGHT = os.path.join(BIN, "harness-preflight.sh")
|
||||||
@@ -41,6 +42,14 @@ def state_root() -> str:
|
|||||||
return os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
|
return os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
|
||||||
|
|
||||||
|
|
||||||
|
def audit_path() -> str:
|
||||||
|
return os.environ.get("CASAN_CHAT_AUDIT_LOG") or os.path.join(state_root(), "logs", "chat", "chat-turns.jsonl")
|
||||||
|
|
||||||
|
|
||||||
|
def head_path() -> str:
|
||||||
|
return os.environ.get("CASAN_CHAT_AUDIT_HEAD") or os.path.join(state_root(), "logs", "chat", "chat-head.txt")
|
||||||
|
|
||||||
|
|
||||||
def sha(text: str) -> str:
|
def sha(text: str) -> str:
|
||||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
@@ -127,6 +136,32 @@ def loop_dir(run_id: str) -> str:
|
|||||||
return os.path.join(state_root(), "logs", "chat", "loop-runs", run_id)
|
return os.path.join(state_root(), "logs", "chat", "loop-runs", run_id)
|
||||||
|
|
||||||
|
|
||||||
|
def load_chat_head() -> str:
|
||||||
|
try:
|
||||||
|
return open(head_path(), encoding="utf-8").read().strip() or ("0" * 64)
|
||||||
|
except OSError:
|
||||||
|
return "0" * 64
|
||||||
|
|
||||||
|
|
||||||
|
def append_chat_turn(base):
|
||||||
|
path = audit_path()
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
seq = 1
|
||||||
|
if os.path.isfile(path):
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
seq = sum(1 for line in fh if line.strip()) + 1
|
||||||
|
prev = load_chat_head()
|
||||||
|
core = {"seq": seq, **base, "prev_hash": prev}
|
||||||
|
record_hash = sha(json.dumps(core, sort_keys=True, ensure_ascii=False))
|
||||||
|
rec = {**core, "record_hash": record_hash}
|
||||||
|
with open(path, "a", encoding="utf-8") as fh:
|
||||||
|
fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||||
|
os.makedirs(os.path.dirname(head_path()), exist_ok=True)
|
||||||
|
with open(head_path(), "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(record_hash + "\n")
|
||||||
|
return rec
|
||||||
|
|
||||||
|
|
||||||
def run_preflight_and_context(run_id: str, draft_path: str):
|
def run_preflight_and_context(run_id: str, draft_path: str):
|
||||||
preflight_out = os.path.join(loop_dir(run_id), "preflight.json")
|
preflight_out = os.path.join(loop_dir(run_id), "preflight.json")
|
||||||
r = subprocess.run(["bash", PREFLIGHT, draft_path, preflight_out, "--model", "local:chat-turn"], cwd=ROOT, capture_output=True, text=True)
|
r = subprocess.run(["bash", PREFLIGHT, draft_path, preflight_out, "--model", "local:chat-turn"], cwd=ROOT, capture_output=True, text=True)
|
||||||
@@ -257,20 +292,85 @@ def bind_agent(args, router):
|
|||||||
for tool in tools:
|
for tool in tools:
|
||||||
cmd += ["--tool", tool]
|
cmd += ["--tool", tool]
|
||||||
r = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
|
r = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
|
||||||
if r.returncode != 0:
|
try:
|
||||||
|
return r.returncode, json.loads(r.stdout)
|
||||||
|
except Exception:
|
||||||
|
if r.returncode != 0 and (r.stdout or r.stderr):
|
||||||
sys.stdout.write((r.stdout or r.stderr).strip() + "\n")
|
sys.stdout.write((r.stdout or r.stderr).strip() + "\n")
|
||||||
return r.returncode, None
|
return r.returncode, None
|
||||||
try:
|
|
||||||
return 0, json.loads(r.stdout)
|
|
||||||
except Exception:
|
|
||||||
print(json.dumps({"success": False, "decision": "DENIED", "reason": "agent_resolver_invalid_json"}, ensure_ascii=False))
|
print(json.dumps({"success": False, "decision": "DENIED", "reason": "agent_resolver_invalid_json"}, ensure_ascii=False))
|
||||||
return 2, None
|
return 2, None
|
||||||
|
|
||||||
|
|
||||||
|
def submit_escalation(args, router, binding):
|
||||||
|
turn_id = args.turn_id or str(uuid.uuid4())
|
||||||
|
payload = {
|
||||||
|
"chat_id": args.chat_id or "chat-default",
|
||||||
|
"turn_id": turn_id,
|
||||||
|
"tenant_id": args.tenant,
|
||||||
|
"actor": args.actor,
|
||||||
|
"role": args.role,
|
||||||
|
"message_ref": sha(args.message),
|
||||||
|
"message_preview": args.message[:240],
|
||||||
|
"router": router,
|
||||||
|
"agent_binding": binding,
|
||||||
|
}
|
||||||
|
reason = f"chat turn requires approval: {binding.get('reason', 'requires_approval')}"
|
||||||
|
r = subprocess.run([
|
||||||
|
"python3", APPROVAL_INBOX, "submit",
|
||||||
|
"--project", args.project,
|
||||||
|
"--action", "chat.escalate",
|
||||||
|
"--target", f"chat:{args.chat_id or 'chat-default'}:{turn_id}",
|
||||||
|
"--risk", router.get("risk", "high"),
|
||||||
|
"--sensitive",
|
||||||
|
"--proposer", args.actor,
|
||||||
|
"--reason", reason,
|
||||||
|
"--payload", json.dumps(payload, ensure_ascii=False, sort_keys=True),
|
||||||
|
], cwd=ROOT, capture_output=True, text=True)
|
||||||
|
try:
|
||||||
|
proposal = json.loads(r.stdout)
|
||||||
|
except Exception:
|
||||||
|
proposal = {"status": "failed", "error": (r.stdout + r.stderr).strip()}
|
||||||
|
rec = append_chat_turn({
|
||||||
|
"timestamp": proposal.get("created_at") or "",
|
||||||
|
"trace_id": "chat-escalation-" + sha(f"{args.chat_id}|{turn_id}|{args.message}")[:12],
|
||||||
|
"chat_id": args.chat_id or "chat-default",
|
||||||
|
"turn_id": turn_id,
|
||||||
|
"tenant_id": args.tenant,
|
||||||
|
"actor": args.actor,
|
||||||
|
"mode": router.get("mode", "READ_ONLY"),
|
||||||
|
"risk": router.get("risk", "high"),
|
||||||
|
"decision": "ESCALATED" if r.returncode == 0 else "DENIED",
|
||||||
|
"answer": "Chat turn escalated to approval inbox." if r.returncode == 0 else "Chat escalation failed closed.",
|
||||||
|
"sources": [],
|
||||||
|
"router": router,
|
||||||
|
"agent_binding": binding,
|
||||||
|
"approval": {"proposal_id": proposal.get("id"), "status": proposal.get("status"), "target": proposal.get("target")},
|
||||||
|
})
|
||||||
|
print(json.dumps({
|
||||||
|
"success": False,
|
||||||
|
"mode": router.get("mode", "READ_ONLY"),
|
||||||
|
"risk": router.get("risk", "high"),
|
||||||
|
"decision": "ESCALATED" if r.returncode == 0 else "DENIED",
|
||||||
|
"answer": "Chat turn escalated to approval inbox." if r.returncode == 0 else "Chat escalation failed closed.",
|
||||||
|
"sources": [],
|
||||||
|
"certified": False,
|
||||||
|
"audit": {"seq": rec["seq"], "record_hash": rec["record_hash"], "head": rec["record_hash"], "path": audit_path()},
|
||||||
|
"router": router,
|
||||||
|
"agent_binding": binding,
|
||||||
|
"approval": {"proposal": proposal, "submit_rc": r.returncode, "output": (r.stdout + r.stderr).strip()[:400]},
|
||||||
|
}, ensure_ascii=False))
|
||||||
|
return 3 if r.returncode == 0 else 2
|
||||||
|
|
||||||
|
|
||||||
def ask(args) -> int:
|
def ask(args) -> int:
|
||||||
router = classify(args.message)
|
router = classify(args.message)
|
||||||
bind_rc, binding = bind_agent(args, router)
|
bind_rc, binding = bind_agent(args, router)
|
||||||
if bind_rc != 0:
|
if bind_rc != 0:
|
||||||
|
if binding and binding.get("decision") == "REQUIRES_APPROVAL":
|
||||||
|
return submit_escalation(args, router, binding)
|
||||||
|
if binding:
|
||||||
|
print(json.dumps(binding, ensure_ascii=False))
|
||||||
return bind_rc
|
return bind_rc
|
||||||
common = [
|
common = [
|
||||||
"--message", args.message,
|
"--message", args.message,
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ run "phase-chat-agent-select" bash "$TESTS/phase-chat-agent-select-tests.sh"
|
|||||||
run "phase-chat-pipeline" bash "$TESTS/phase-chat-pipeline-tests.sh"
|
run "phase-chat-pipeline" bash "$TESTS/phase-chat-pipeline-tests.sh"
|
||||||
run "phase-chat-stream-hold" bash "$TESTS/phase-chat-stream-hold-tests.sh"
|
run "phase-chat-stream-hold" bash "$TESTS/phase-chat-stream-hold-tests.sh"
|
||||||
run "phase-chat-replay" bash "$TESTS/phase-chat-replay-tests.sh"
|
run "phase-chat-replay" bash "$TESTS/phase-chat-replay-tests.sh"
|
||||||
|
run "phase-chat-approval" bash "$TESTS/phase-chat-approval-tests.sh"
|
||||||
|
|
||||||
# ARCH-02: coverage cannot silently drop; ARCH-01: harness/policy cannot silently
|
# ARCH-02: coverage cannot silently drop; ARCH-01: harness/policy cannot silently
|
||||||
# drift. Both SKIP cleanly when no manifest is provisioned (non-strict dev/CI).
|
# drift. Both SKIP cleanly when no manifest is provisioned (non-strict dev/CI).
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# Plan-18 MVP-2 Track 8.4: chat turns that exceed delegated authority are
|
||||||
|
# escalated into the Plan-13 approval inbox and remain tamper-verifiable.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||||
|
TURN="$CASAN_HARNESS_ROOT/scripts/bash/chat-turn.py"
|
||||||
|
APPROVAL="$CASAN_HARNESS_ROOT/scripts/bash/approval-inbox.py"
|
||||||
|
REPLAY="$CASAN_HARNESS_ROOT/scripts/bash/chat-replay.py"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
export CASAN_STATE_ROOT="$WORK/state"
|
||||||
|
export CASAN_APPROVAL_INBOX_FILE="$WORK/approval-inbox.json"
|
||||||
|
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
echo "===== Plan-18 MVP-2 chat approvals escalation ====="
|
||||||
|
|
||||||
|
set +e
|
||||||
|
python3 "$TURN" ask \
|
||||||
|
--message "Summarize Plan 18 MVP-2 status" \
|
||||||
|
--actor alice \
|
||||||
|
--role viewer \
|
||||||
|
--chat-id approval-chat \
|
||||||
|
--tenant tenant-a \
|
||||||
|
--delegation-level 1 > "$WORK/escalated.json"
|
||||||
|
RC=$?
|
||||||
|
set -e 2>/dev/null || true
|
||||||
|
|
||||||
|
python3 - "$WORK/escalated.json" "$RC" <<'PY' \
|
||||||
|
&& pass "delegation escalation creates pending approval proposal" || fail "chat turn did not escalate to approval inbox"
|
||||||
|
import json, sys
|
||||||
|
d = json.load(open(sys.argv[1]))
|
||||||
|
assert int(sys.argv[2]) == 3
|
||||||
|
assert d["success"] is False
|
||||||
|
assert d["decision"] == "ESCALATED"
|
||||||
|
assert d["agent_binding"]["decision"] == "REQUIRES_APPROVAL"
|
||||||
|
assert d["approval"]["proposal"]["status"] == "pending"
|
||||||
|
assert d["approval"]["proposal"]["action"] == "chat.escalate"
|
||||||
|
assert d["approval"]["proposal"]["payload"]["chat_id"] == "approval-chat"
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 "$APPROVAL" list --status pending > "$WORK/pending.json"
|
||||||
|
python3 - "$WORK/pending.json" <<'PY' \
|
||||||
|
&& pass "approval inbox lists escalated chat proposal" || fail "pending chat proposal missing from inbox"
|
||||||
|
import json, sys
|
||||||
|
d = json.load(open(sys.argv[1]))
|
||||||
|
assert d["count"] == 1
|
||||||
|
p = d["proposals"][0]
|
||||||
|
assert p["status"] == "pending"
|
||||||
|
assert p["action"] == "chat.escalate"
|
||||||
|
assert p["payload"]["agent_binding"]["requires_approval"] is True
|
||||||
|
PY
|
||||||
|
|
||||||
|
python3 "$REPLAY" verify-chain > "$WORK/chat-chain.out" \
|
||||||
|
&& grep -q '"decision": "OK"' "$WORK/chat-chain.out" \
|
||||||
|
&& pass "escalated chat turn audit chain verifies" || fail "escalated chat audit chain invalid"
|
||||||
|
|
||||||
|
python3 "$APPROVAL" verify-audit > "$WORK/approval-audit.out" \
|
||||||
|
&& grep -q "APPROVAL_INBOX_AUDIT ok=true" "$WORK/approval-audit.out" \
|
||||||
|
&& pass "approval oversight audit chain verifies" || fail "approval oversight audit invalid"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===== CHAT APPROVAL SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||||
|
[[ "$FAIL" -eq 0 ]] || exit 1
|
||||||
@@ -19,7 +19,7 @@ Optional layer for teams that want UI / dashboard / visibility. Packages: `casan
|
|||||||
| RBAC + approval inbox | ✅ local-prod done | Settings + kill-switch API RBAC enforcement, approval inbox/delegation/oversight, SoD, governed setting proposal apply, and local OIDC claim→role mapping smoke are done. Enterprise IdP rollout remains production follow-up. |
|
| RBAC + approval inbox | ✅ local-prod done | Settings + kill-switch API RBAC enforcement, approval inbox/delegation/oversight, SoD, governed setting proposal apply, and local OIDC claim→role mapping smoke are done. Enterprise IdP rollout remains production follow-up. |
|
||||||
| Evidence Pack Viewer | 📋 planned | reads `docs/output/casan/evidence-packs/` |
|
| Evidence Pack Viewer | 📋 planned | reads `docs/output/casan/evidence-packs/` |
|
||||||
| Attack Battery Viewer | 📋 planned | reads red-team corpus + H4 recall results |
|
| Attack Battery Viewer | 📋 planned | reads red-team corpus + H4 recall results |
|
||||||
| Read-only Ask CASAN + Operator + agent selection + operator loop-hold + replay/widget | 🟡 MVP-0/1 done+test; MVP-2 Track 4 + OPERATOR Track 5/6 + Track 8.1/8.2/8.3 done | 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` + `/command`, backed by harness `chat-turn.py`, `chat-agent-resolver.py`, `chat-replay.py`, and Plan-17 `loop-run.sh` |
|
| Read-only Ask CASAN + Operator + agent selection + operator loop-hold + replay/widget/approvals | 🟡 MVP-0/1 done+test; MVP-2 Track 4 + OPERATOR Track 5/6 + Track 8.1/8.2/8.3/8.4 done | 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`, `/chat` + `/command`, backed by harness `chat-turn.py`, `chat-agent-resolver.py`, `chat-replay.py`, `approval-inbox.py`, and Plan-17 `loop-run.sh` |
|
||||||
| Gitea webhook integration | 📋 planned | trigger gate / publish evidence on push |
|
| Gitea webhook integration | 📋 planned | trigger gate / publish evidence on push |
|
||||||
|
|
||||||
## Build (preview)
|
## Build (preview)
|
||||||
@@ -29,6 +29,6 @@ scripts/package-release.sh platform # → dist/casan-platform-preview-vX.Y.Z
|
|||||||
The bundle includes a `PREVIEW-INCOMPLETE.txt` marker. Do not treat it as a finished product.
|
The bundle includes a `PREVIEW-INCOMPLETE.txt` marker. Do not treat it as a finished product.
|
||||||
|
|
||||||
## To implement later
|
## To implement later
|
||||||
Start from Plan-15 RAI view, Plan-18 MVP-2 Track 8.4 approvals escalation, or CODEGEN-as-loop. Keep new numbers
|
Start from Plan-15 RAI view, Plan-18 CODEGEN-as-loop, or Plan-18 Track 9 multi-tenant hardening. Keep new numbers
|
||||||
evidence-backed with provenance; any write/governed action must continue to route through
|
evidence-backed with provenance; any write/governed action must continue to route through
|
||||||
harness RBAC, approval, and audit primitives.
|
harness RBAC, approval, and audit primitives.
|
||||||
|
|||||||
Reference in New Issue
Block a user