finish 9th jan sprints
This commit is contained in:
@@ -1,338 +0,0 @@
|
||||
# SPRINT INDEX: AI Moats - Defensible AI-Native Security Platform
|
||||
|
||||
> **Epic:** Evidence-First AI with Cryptographic Trust
|
||||
> **Batch:** 011
|
||||
> **Status:** Planning
|
||||
> **Created:** 09-Jan-2026
|
||||
> **Source Advisory:** `docs/product/advisories/08-Jan-2026 - AI moats.md`
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This sprint batch transforms StellaOps from "security platform with AI features" to "AI-native security platform with defensible moats." The key insight: **AI outputs must become first-class artifacts in the attestation chain**, not ephemeral chat responses.
|
||||
|
||||
### Strategic Differentiation
|
||||
|
||||
| Competitor Approach | StellaOps Approach |
|
||||
|--------------------|--------------------|
|
||||
| Chat-only AI | AI outputs as signed artifacts |
|
||||
| Generic RAG | Security-specific grounding with evidence links |
|
||||
| Role-based permissions | K4 lattice policy gates |
|
||||
| Ephemeral conversations | Auditable Runs with deterministic replay |
|
||||
| Learning from chat logs | Learning from verified decision outcomes |
|
||||
|
||||
### Business Value
|
||||
|
||||
- **Trust by construction:** Every AI claim cryptographically linked to evidence
|
||||
- **Compliance-ready:** Full audit trail for AI-assisted decisions
|
||||
- **Institutional learning:** Outcomes feed back into decision support
|
||||
- **Reproducibility:** AI sessions can be replayed for verification
|
||||
- **Air-gap compatible:** All features work offline
|
||||
|
||||
---
|
||||
|
||||
## Sprint Structure
|
||||
|
||||
| Sprint ID | Title | Module | Status | Dependencies |
|
||||
|-----------|-------|--------|--------|--------------|
|
||||
| 011_001 | AI Attestations | LB/BE | **DONE** | - |
|
||||
| 011_002 | OpsMemory Chat Integration | BE | **DONE** | 011_001 |
|
||||
| 011_003 | AI Runs Framework | BE/FE | **DONE** | 011_001 |
|
||||
| 011_004 | Policy-Action Integration | BE | **DONE** | 011_003 |
|
||||
| 011_005 | Evidence Pack Artifacts | LB/BE | **DONE** | 011_001, 011_003 |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ AI Runs (011_003) │
|
||||
│ ┌─────────────────────────────────────────────────┐│
|
||||
│ │ RunId: "run-abc123" ││
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││
|
||||
│ │ │ Turn │→│ Turn │→│ Turn │→ ... ││
|
||||
│ │ │ (user) │ │ (assist)│ │ (user) │ ││
|
||||
│ │ └────┬────┘ └────┬────┘ └────┬────┘ ││
|
||||
│ │ │ │ │ ││
|
||||
│ │ ▼ ▼ ▼ ││
|
||||
│ │ ┌─────────────────────────────────────────┐ ││
|
||||
│ │ │ Artifacts Produced │ ││
|
||||
│ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ││
|
||||
│ │ │ │ Evidence │ │ Decision │ │ Action │ │ ││
|
||||
│ │ │ │ Pack │ │ Record │ │ Proposal │ │ ││
|
||||
│ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ ││
|
||||
│ │ └───────┼────────────┼────────────┼───────┘ ││
|
||||
│ └──────────┼────────────┼────────────┼───────────┘│
|
||||
└─────────────┼────────────┼────────────┼────────────┘
|
||||
│ │ │
|
||||
┌─────────────▼────────────▼────────────▼────────────┐
|
||||
│ AI Attestations (011_001) │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ DSSE Envelope │ │
|
||||
│ │ ├── payloadType: "application/vnd.stellaops+│ │
|
||||
│ │ │ ai-run+json" │ │
|
||||
│ │ ├── payload: { RunAttestation } │ │
|
||||
│ │ └── signatures: [ { keyid, sig } ] │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ RunAttestation │ │
|
||||
│ │ ├── runId, tenantId, userId │ │
|
||||
│ │ ├── promptTemplateHash │ │
|
||||
│ │ ├── modelDigest │ │
|
||||
│ │ ├── evidenceRefs: [stella://sbom/..., ...] │ │
|
||||
│ │ ├── claims: [ { text, groundedBy } ] │ │
|
||||
│ │ └── contentDigest: sha256:... │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└────────────────────────┬───────────────────────────┘
|
||||
│
|
||||
┌────────────────────────────────────┴────────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────────┐ ┌───────────────────────┐
|
||||
│ OpsMemory (011_002) │ │ Policy Gate (011_004) │
|
||||
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
|
||||
│ │ Similar Past │ │ │ │ K4 Lattice │ │
|
||||
│ │ Decisions │──┼── surfaces in chat ──────┐ │ │ Policy Check │ │
|
||||
│ └─────────────────┘ │ │ │ └────────┬────────┘ │
|
||||
│ │ │ │ │ │ │
|
||||
│ ▼ │ │ │ ▼ │
|
||||
│ ┌─────────────────┐ │ │ │ ┌─────────────────┐ │
|
||||
│ │ Outcome │ │ │ │ │ Approval │ │
|
||||
│ │ Tracking │ │ │ │ │ Workflow │ │
|
||||
│ └─────────────────┘ │ │ │ └─────────────────┘ │
|
||||
└───────────────────────┘ │ └───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Evidence Pack (011_005) │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ EvidencePack │ │
|
||||
│ │ ├── packId: "pack-xyz789" │ │
|
||||
│ │ ├── runId: "run-abc123" │ │
|
||||
│ │ ├── artifacts: │ │
|
||||
│ │ │ ├── sbom: { digest, uri } │ │
|
||||
│ │ │ ├── reachability: { latticeState, ... } │ │
|
||||
│ │ │ ├── vexStatements: [ ... ] │ │
|
||||
│ │ │ └── attestations: [ ... ] │ │
|
||||
│ │ ├── claims: [ { text, evidenceRef } ] │ │
|
||||
│ │ └── signatures: DSSE envelope │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis (from AI Moats Advisory)
|
||||
|
||||
### ADVISORY-AI-000: Foundation
|
||||
|
||||
| Requirement | Current State | Sprint |
|
||||
|-------------|---------------|--------|
|
||||
| Chat panel | ✅ Exists | - |
|
||||
| Artifact cards | ❌ Missing | 011_003, 011_005 |
|
||||
| Run Timeline | ❌ Missing | 011_003 |
|
||||
| Prompt versioning | ✅ Exists | - |
|
||||
|
||||
### ADVISORY-AI-001: Evidence-First
|
||||
|
||||
| Requirement | Current State | Sprint |
|
||||
|-------------|---------------|--------|
|
||||
| Claim → Evidence constraint | ✅ GroundingValidator | - |
|
||||
| Evidence Pack artifact | ❌ Missing | 011_005 |
|
||||
| DSSE signatures | ❌ Missing | 011_001 |
|
||||
| Confidence badge | ⚠️ Partial | 011_003 |
|
||||
|
||||
### ADVISORY-AI-002: Policy-Aware Automation
|
||||
|
||||
| Requirement | Current State | Sprint |
|
||||
|-------------|---------------|--------|
|
||||
| Action Registry | ✅ ActionProposalParser | - |
|
||||
| Policy decision point | ⚠️ Role-only | 011_004 |
|
||||
| Approval workflow | ❌ Missing | 011_004 |
|
||||
| Idempotency/rollback | ❌ Missing | 011_004 |
|
||||
|
||||
### ADVISORY-AI-003: Ops Memory
|
||||
|
||||
| Requirement | Current State | Sprint |
|
||||
|-------------|---------------|--------|
|
||||
| Decision records | ✅ OpsMemory | - |
|
||||
| Chat integration | ❌ Missing | 011_002 |
|
||||
| Outcome tracking | ✅ Exists | - |
|
||||
| Typed memory objects | ⚠️ Partial | 011_002 |
|
||||
|
||||
---
|
||||
|
||||
## Deliverables Summary
|
||||
|
||||
### 011_001: AI Attestations
|
||||
|
||||
| Deliverable | Type |
|
||||
|-------------|------|
|
||||
| `IAiAttestationService` | Interface |
|
||||
| `AiRunAttestation` | Record |
|
||||
| `AiClaimAttestation` | Record |
|
||||
| DSSE envelope integration | Implementation |
|
||||
| Prompt template hashing | Implementation |
|
||||
| Model digest tracking | Implementation |
|
||||
|
||||
### 011_002: OpsMemory Chat Integration
|
||||
|
||||
| Deliverable | Type |
|
||||
|-------------|------|
|
||||
| `IOpsMemoryChatProvider` | Interface |
|
||||
| Chat context enrichment | Service |
|
||||
| Similar decision surfacing | Feature |
|
||||
| Decision recording from chat | Hook |
|
||||
| KnownIssue, Tactic types | Models |
|
||||
|
||||
### 011_003: AI Runs Framework
|
||||
|
||||
| Deliverable | Type |
|
||||
|-------------|------|
|
||||
| `IRun` | Interface |
|
||||
| `RunService` | Service |
|
||||
| `RunArtifact` | Record |
|
||||
| Run Timeline persistence | Storage |
|
||||
| Run replay capability | Feature |
|
||||
| Run Timeline UI component | Angular |
|
||||
|
||||
### 011_004: Policy-Action Integration
|
||||
|
||||
| Deliverable | Type |
|
||||
|-------------|------|
|
||||
| `IActionPolicyGate` | Interface |
|
||||
| K4 lattice integration | Implementation |
|
||||
| Approval workflow service | Service |
|
||||
| Idempotency key handling | Implementation |
|
||||
| Action audit ledger | Storage |
|
||||
|
||||
### 011_005: Evidence Pack Artifacts
|
||||
|
||||
| Deliverable | Type |
|
||||
|-------------|------|
|
||||
| `IEvidencePackService` | Interface |
|
||||
| `EvidencePack` | Record |
|
||||
| DSSE-signed pack export | Feature |
|
||||
| Evidence URI resolution | Service |
|
||||
| Pack viewer UI component | Angular |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Module Dependencies
|
||||
|
||||
| From Sprint | To Module | Interface |
|
||||
|-------------|-----------|-----------|
|
||||
| 011_001 | Attestor | `IDsseEnvelopeBuilder` |
|
||||
| 011_001 | Signer | `ISigningService` |
|
||||
| 011_002 | OpsMemory | `IOpsMemoryStore` |
|
||||
| 011_002 | AdvisoryAI | `IChatContextProvider` |
|
||||
| 011_003 | Timeline | `ITimelineStore` |
|
||||
| 011_004 | Policy | `IPolicyEngine` |
|
||||
| 011_005 | EvidenceLocker | `IEvidenceStore` |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
None - all features work offline.
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Technical Risks
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| DSSE signature overhead | Low | Low | Async signing, batch where possible |
|
||||
| Run storage growth | Medium | Medium | Retention policies, compression |
|
||||
| Policy gate latency | Medium | High | Cache policy decisions, async where safe |
|
||||
| OpsMemory relevance ranking | Medium | Medium | Tunable similarity thresholds |
|
||||
|
||||
### Schedule Risks
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| Cross-module coordination | High | Medium | Clear interface contracts first |
|
||||
| UI complexity | Medium | Medium | Ship backend first, UI incrementally |
|
||||
| Determinism edge cases | Medium | High | Extensive golden tests |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Quantitative Metrics
|
||||
|
||||
| Metric | Target | Measurement |
|
||||
|--------|--------|-------------|
|
||||
| AI responses with evidence | >95% | GroundingValidator metrics |
|
||||
| Signed AI artifacts | 100% | Attestation count |
|
||||
| OpsMemory suggestions surfaced | >50% of sessions | Chat analytics |
|
||||
| Action approval latency P95 | <5s | Prometheus |
|
||||
|
||||
### Qualitative Criteria
|
||||
|
||||
- [ ] Security teams trust AI recommendations due to evidence
|
||||
- [ ] Auditors can verify AI decision chain
|
||||
- [ ] Operators find past decisions useful
|
||||
- [ ] Replay produces identical results
|
||||
|
||||
---
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
| Sprint | Task | Status | Notes |
|
||||
|--------|------|--------|-------|
|
||||
| 011_001 | AI Attestation service | **DONE** | IAiAttestationService + AiAttestationService |
|
||||
| 011_001 | Run attestation schema | **DONE** | AiRunAttestation, AiClaimAttestation |
|
||||
| 011_001 | DSSE integration | **DONE** | DsseEnvelopeBuilder integration |
|
||||
| 011_002 | Chat context provider | TODO | - |
|
||||
| 011_002 | Similar decision query | TODO | - |
|
||||
| 011_002 | KnownIssue/Tactic models | TODO | - |
|
||||
| 011_003 | Run service | TODO | - |
|
||||
| 011_003 | Run timeline storage | TODO | - |
|
||||
| 011_003 | Run replay | TODO | - |
|
||||
| 011_003 | Run Timeline UI | TODO | - |
|
||||
| 011_004 | Action policy gate | TODO | - |
|
||||
| 011_004 | Approval workflow | TODO | - |
|
||||
| 011_004 | Action audit ledger | TODO | - |
|
||||
| 011_005 | Evidence pack service | TODO | - |
|
||||
| 011_005 | Pack export | TODO | - |
|
||||
| 011_005 | Pack viewer UI | TODO | - |
|
||||
|
||||
---
|
||||
|
||||
## Decisions & Risks Log
|
||||
|
||||
| Date | Decision/Risk | Resolution | Owner |
|
||||
|------|---------------|------------|-------|
|
||||
| 09-Jan-2026 | Sprint structure created | Approved | PM |
|
||||
| 09-Jan-2026 | AI outputs as attestations | Core differentiator | Arch |
|
||||
| - | - | - | - |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Source Advisory](../product/advisories/08-Jan-2026%20-%20AI%20moats.md)
|
||||
- [AdvisoryAI Architecture](../modules/advisory-ai/architecture.md)
|
||||
- [OpsMemory Architecture](../modules/opsmemory/architecture.md)
|
||||
- [Attestor Architecture](../modules/attestor/architecture.md)
|
||||
- [Hybrid Reachability Sprint](./SPRINT_20260109_009_000_INDEX_hybrid_reachability.md)
|
||||
|
||||
---
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Event | Details |
|
||||
|------|-------|---------|
|
||||
| 09-Jan-2026 | Sprint batch created | Gap analysis from AI Moats advisory |
|
||||
| 10-Jan-2026 | 011_001 DONE | AttestationIntegration.cs, IAiAttestationService, models created |
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 10-Jan-2026_
|
||||
@@ -1,796 +0,0 @@
|
||||
# Sprint SPRINT_20260109_011_002_BE - OpsMemory Chat Integration
|
||||
|
||||
> **Parent:** [SPRINT_20260109_011_000_INDEX](./SPRINT_20260109_011_000_INDEX_ai_moats.md)
|
||||
> **Status:** DONE
|
||||
> **Created:** 09-Jan-2026
|
||||
> **Module:** BE (Backend)
|
||||
> **Depends On:** SPRINT_20260109_011_001_LB (AI Attestations)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Connect OpsMemory (institutional decision memory) to AdvisoryAI Chat, enabling the AI to surface relevant past decisions and automatically record new decisions with outcomes.
|
||||
|
||||
### Why This Matters
|
||||
|
||||
| Current State | Target State |
|
||||
|---------------|--------------|
|
||||
| OpsMemory isolated from Chat | Past decisions surface in chat context |
|
||||
| Decisions recorded manually | Decisions auto-recorded from chat actions |
|
||||
| No feedback loop | Outcomes improve future suggestions |
|
||||
| Generic suggestions | Security-specific similarity matching |
|
||||
|
||||
---
|
||||
|
||||
## Working Directory
|
||||
|
||||
- `src/AdvisoryAI/StellaOps.AdvisoryAI/Chat/` (integration)
|
||||
- `src/OpsMemory/StellaOps.OpsMemory/Integration/` (new)
|
||||
- `src/__Libraries/__Tests/StellaOps.AdvisoryAI.Tests/OpsMemory/` (new)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Existing: `OpsMemory.PlaybookSuggestionService`
|
||||
- Existing: `AdvisoryAI.Chat.ConversationService`
|
||||
- Existing: `OpsMemory.SimilarityVectorGenerator`
|
||||
- Required: AI Attestations (011_001) for decision attestation
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Chat Session │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ User: "What should we do about CVE-2023-44487?" │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ OpsMemoryChatProvider.EnrichContextAsync() │ │
|
||||
│ │ → Query similar past decisions │ │
|
||||
│ │ → Return top-3 with outcomes │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Prompt Assembly │ │
|
||||
│ │ System: "Previous similar situations..." │ │
|
||||
│ │ - CVE-2022-41903 (same category): Accepted, SUCCESS │ │
|
||||
│ │ - CVE-2023-1234 (similar severity): Quarantined, SUCCESS │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Assistant Response: │ │
|
||||
│ │ "Based on 3 similar past decisions [ops-mem:dec-abc123]..." │ │
|
||||
│ │ [Accept Risk]{action:approve,cve_id=CVE-2023-44487} │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ (if action executed) │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ OpsMemoryDecisionRecorder.RecordFromActionAsync() │ │
|
||||
│ │ → Extract situation from chat context │ │
|
||||
│ │ → Record decision with action, rationale │ │
|
||||
│ │ → Link to Run attestation │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### OMCI-001: IOpsMemoryChatProvider Interface
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/StellaOps.OpsMemory/Integration/IOpsMemoryChatProvider.cs` |
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IOpsMemoryChatProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Enriches chat context with relevant past decisions.
|
||||
/// </summary>
|
||||
Task<OpsMemoryContext> EnrichContextAsync(
|
||||
ChatContextRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Records a decision from an executed chat action.
|
||||
/// </summary>
|
||||
Task<OpsMemoryRecord> RecordFromActionAsync(
|
||||
ActionExecutionResult action,
|
||||
ConversationContext context,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record ChatContextRequest
|
||||
{
|
||||
public required string TenantId { get; init; }
|
||||
public string? CveId { get; init; }
|
||||
public string? Component { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public ReachabilityStatus? Reachability { get; init; }
|
||||
public ImmutableArray<string> ContextTags { get; init; }
|
||||
public int MaxSuggestions { get; init; } = 3;
|
||||
}
|
||||
|
||||
public sealed record OpsMemoryContext
|
||||
{
|
||||
public ImmutableArray<PastDecisionSummary> SimilarDecisions { get; init; }
|
||||
public ImmutableArray<KnownIssue> RelevantKnownIssues { get; init; }
|
||||
public ImmutableArray<Tactic> ApplicableTactics { get; init; }
|
||||
public double ContextConfidence { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PastDecisionSummary
|
||||
{
|
||||
public required string MemoryId { get; init; }
|
||||
public required string CveId { get; init; }
|
||||
public required DecisionAction Action { get; init; }
|
||||
public required OutcomeStatus? Outcome { get; init; }
|
||||
public required double Similarity { get; init; }
|
||||
public required string Rationale { get; init; }
|
||||
public required DateTimeOffset DecidedAt { get; init; }
|
||||
public ImmutableArray<string> MatchingFactors { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Interface supports context enrichment
|
||||
- [ ] Interface supports decision recording
|
||||
- [ ] Returns structured past decision summaries
|
||||
- [ ] Supports typed memory objects (KnownIssue, Tactic)
|
||||
|
||||
---
|
||||
|
||||
### OMCI-002: KnownIssue and Tactic Models
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/StellaOps.OpsMemory/Integration/IOpsMemoryChatProvider.cs` (models included in interface file) |
|
||||
|
||||
**New Models (per ADVISORY-AI-003):**
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A known issue that has been documented and may recur.
|
||||
/// </summary>
|
||||
public sealed record KnownIssue
|
||||
{
|
||||
public required string IssueId { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required IssueCategory Category { get; init; }
|
||||
public ImmutableArray<string> AffectedComponents { get; init; }
|
||||
public ImmutableArray<string> AffectedCves { get; init; }
|
||||
public string? Resolution { get; init; }
|
||||
public KnownIssueStatus Status { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? ResolvedAt { get; init; }
|
||||
public required string CreatedBy { get; init; }
|
||||
}
|
||||
|
||||
public enum IssueCategory
|
||||
{
|
||||
VulnerabilityPattern, // Recurring vuln type (e.g., "HTTP/2 issues")
|
||||
ConfigurationDrift, // Environment misconfiguration
|
||||
DependencyConflict, // Version conflicts
|
||||
ComplianceGap, // Regulatory finding
|
||||
OperationalAnomaly // Unexpected behavior
|
||||
}
|
||||
|
||||
public enum KnownIssueStatus
|
||||
{
|
||||
Active,
|
||||
Mitigated,
|
||||
Resolved,
|
||||
WontFix
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A documented tactic for handling specific situations.
|
||||
/// </summary>
|
||||
public sealed record Tactic
|
||||
{
|
||||
public required string TacticId { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required TacticTrigger Trigger { get; init; }
|
||||
public required ImmutableArray<TacticStep> Steps { get; init; }
|
||||
public required double SuccessRate { get; init; }
|
||||
public required int TimesUsed { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? LastUsedAt { get; init; }
|
||||
public required string CreatedBy { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TacticTrigger
|
||||
{
|
||||
public ImmutableArray<string> CveCategories { get; init; }
|
||||
public ImmutableArray<string> Severities { get; init; }
|
||||
public ImmutableArray<string> ComponentTypes { get; init; }
|
||||
public bool? RequiresReachable { get; init; }
|
||||
public double? MinEpssScore { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TacticStep
|
||||
{
|
||||
public required int Order { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public string? ActionType { get; init; } // Optional action to propose
|
||||
public ImmutableDictionary<string, string>? ActionParameters { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] KnownIssue model with categories
|
||||
- [ ] Tactic model with trigger conditions
|
||||
- [ ] Both have tenant isolation
|
||||
- [ ] Immutable record types
|
||||
|
||||
---
|
||||
|
||||
### OMCI-003: OpsMemoryChatProvider Implementation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/StellaOps.OpsMemory/Integration/OpsMemoryChatProvider.cs` |
|
||||
|
||||
**Implementation:**
|
||||
```csharp
|
||||
internal sealed class OpsMemoryChatProvider : IOpsMemoryChatProvider
|
||||
{
|
||||
private readonly IOpsMemoryStore _store;
|
||||
private readonly IKnownIssueStore _knownIssueStore;
|
||||
private readonly ITacticStore _tacticStore;
|
||||
private readonly SimilarityVectorGenerator _vectorGenerator;
|
||||
private readonly ILogger<OpsMemoryChatProvider> _logger;
|
||||
|
||||
public async Task<OpsMemoryContext> EnrichContextAsync(
|
||||
ChatContextRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Generate similarity vector from request
|
||||
var vector = _vectorGenerator.Generate(new SituationContext
|
||||
{
|
||||
CveId = request.CveId,
|
||||
Component = request.Component,
|
||||
Severity = request.Severity,
|
||||
Reachability = request.Reachability ?? ReachabilityStatus.Unknown,
|
||||
ContextTags = request.ContextTags
|
||||
});
|
||||
|
||||
// 2. Query similar past decisions
|
||||
var similarDecisions = await _store.FindSimilarAsync(
|
||||
new SimilarityQuery
|
||||
{
|
||||
TenantId = request.TenantId,
|
||||
Vector = vector,
|
||||
TopK = request.MaxSuggestions * 2, // Over-fetch for filtering
|
||||
MinSimilarity = 0.5
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
// 3. Filter to successful outcomes and map
|
||||
var summaries = similarDecisions
|
||||
.Where(d => d.Record.Outcome?.Status is OutcomeStatus.Success
|
||||
or OutcomeStatus.PartialSuccess)
|
||||
.Take(request.MaxSuggestions)
|
||||
.Select(d => MapToSummary(d))
|
||||
.ToImmutableArray();
|
||||
|
||||
// 4. Query relevant known issues
|
||||
var knownIssues = await _knownIssueStore.FindByContextAsync(
|
||||
request.TenantId,
|
||||
request.CveId,
|
||||
request.Component,
|
||||
cancellationToken);
|
||||
|
||||
// 5. Query applicable tactics
|
||||
var tactics = await _tacticStore.FindByTriggerAsync(
|
||||
request.TenantId,
|
||||
new TacticTrigger
|
||||
{
|
||||
Severities = request.Severity is not null
|
||||
? ImmutableArray.Create(request.Severity)
|
||||
: ImmutableArray<string>.Empty,
|
||||
RequiresReachable = request.Reachability == ReachabilityStatus.Reachable
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return new OpsMemoryContext
|
||||
{
|
||||
SimilarDecisions = summaries,
|
||||
RelevantKnownIssues = knownIssues,
|
||||
ApplicableTactics = tactics,
|
||||
ContextConfidence = summaries.Length > 0
|
||||
? summaries.Average(s => s.Similarity)
|
||||
: 0.0
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Queries similar decisions efficiently
|
||||
- [ ] Filters to successful outcomes
|
||||
- [ ] Includes known issues and tactics
|
||||
- [ ] Calculates confidence score
|
||||
- [ ] Handles missing data gracefully
|
||||
|
||||
---
|
||||
|
||||
### OMCI-004: Chat Prompt Integration
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/StellaOps.OpsMemory/Integration/OpsMemoryContextEnricher.cs` |
|
||||
|
||||
**System Prompt Addition:**
|
||||
```
|
||||
## Previous Similar Decisions
|
||||
|
||||
Based on your organization's decision history, here are relevant past decisions:
|
||||
|
||||
{{#each similarDecisions}}
|
||||
### {{cveId}} ({{similarity}}% similar)
|
||||
- **Decision:** {{action}}
|
||||
- **Outcome:** {{outcome}}
|
||||
- **Rationale:** {{rationale}}
|
||||
- **Matching factors:** {{matchingFactors}}
|
||||
- **Reference:** [ops-mem:{{memoryId}}]
|
||||
|
||||
{{/each}}
|
||||
|
||||
{{#if knownIssues}}
|
||||
## Known Issues
|
||||
{{#each knownIssues}}
|
||||
- **{{title}}** ({{status}}): {{description}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if tactics}}
|
||||
## Applicable Tactics
|
||||
{{#each tactics}}
|
||||
- **{{name}}** ({{successRate}}% success rate): {{description}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
Consider these past decisions when formulating your recommendation. Reference them using [ops-mem:ID] links.
|
||||
```
|
||||
|
||||
**Integration in ChatPromptAssembler:**
|
||||
```csharp
|
||||
public async Task<ChatPrompt> AssembleAsync(
|
||||
ConversationContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var builder = new ChatPromptBuilder();
|
||||
|
||||
// ... existing assembly ...
|
||||
|
||||
// Add OpsMemory context
|
||||
if (_options.EnableOpsMemoryIntegration)
|
||||
{
|
||||
var opsContext = await _opsMemoryProvider.EnrichContextAsync(
|
||||
new ChatContextRequest
|
||||
{
|
||||
TenantId = context.TenantId,
|
||||
CveId = context.CurrentCveId,
|
||||
Component = context.CurrentComponent,
|
||||
Severity = context.CurrentSeverity,
|
||||
Reachability = context.CurrentReachability,
|
||||
ContextTags = context.ContextTags
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (opsContext.SimilarDecisions.Length > 0)
|
||||
{
|
||||
builder.AddSystemSection(
|
||||
"Previous Similar Decisions",
|
||||
FormatOpsMemoryContext(opsContext));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] OpsMemory context added to system prompt
|
||||
- [ ] Past decisions formatted clearly
|
||||
- [ ] Memory IDs linkable via [ops-mem:ID] format
|
||||
- [ ] Configurable enable/disable
|
||||
- [ ] Does not block if OpsMemory unavailable
|
||||
|
||||
---
|
||||
|
||||
### OMCI-005: Object Link Resolver for OpsMemory
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Chat/OpsMemoryLinkResolver.cs` |
|
||||
|
||||
**Add support for `[ops-mem:ID]` links:**
|
||||
|
||||
```csharp
|
||||
public class OpsMemoryLinkResolver : IObjectLinkResolver
|
||||
{
|
||||
private readonly IOpsMemoryStore _store;
|
||||
|
||||
public bool CanResolve(string type) => type == "ops-mem";
|
||||
|
||||
public async Task<LinkResolution> ResolveAsync(
|
||||
string type,
|
||||
string path,
|
||||
string? tenantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (type != "ops-mem" || tenantId is null)
|
||||
{
|
||||
return new LinkResolution { Exists = false };
|
||||
}
|
||||
|
||||
var record = await _store.GetByIdAsync(tenantId, path, cancellationToken);
|
||||
if (record is null)
|
||||
{
|
||||
return new LinkResolution { Exists = false };
|
||||
}
|
||||
|
||||
return new LinkResolution
|
||||
{
|
||||
Exists = true,
|
||||
Uri = $"ops-mem://{path}",
|
||||
ObjectType = "decision",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["cveId"] = record.Situation.CveId ?? "",
|
||||
["action"] = record.Decision.Action.ToString(),
|
||||
["outcome"] = record.Outcome?.Status.ToString() ?? "pending"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Update `chat-interface.md` Object Link Table:**
|
||||
|
||||
| Type | Format | Example | Description |
|
||||
|------|--------|---------|-------------|
|
||||
| OpsMemory | `[ops-mem:{id}]` | `[ops-mem:mem-abc123]` | Link to past decision |
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Resolver registered for "ops-mem" type
|
||||
- [ ] Returns decision metadata
|
||||
- [ ] Validated by GroundingValidator
|
||||
- [ ] UI can navigate to decision detail
|
||||
|
||||
---
|
||||
|
||||
### OMCI-006: Decision Recording from Actions
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Chat/OpsMemoryIntegration.cs` |
|
||||
|
||||
**Record decisions when chat actions execute:**
|
||||
|
||||
```csharp
|
||||
internal sealed class OpsMemoryDecisionRecorder
|
||||
{
|
||||
public async Task<OpsMemoryRecord> RecordFromActionAsync(
|
||||
ActionExecutionResult action,
|
||||
ConversationContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Extract situation from context
|
||||
var situation = new SituationContext
|
||||
{
|
||||
CveId = context.CurrentCveId,
|
||||
Component = context.CurrentComponent,
|
||||
Severity = context.CurrentSeverity,
|
||||
Reachability = context.CurrentReachability ?? ReachabilityStatus.Unknown,
|
||||
EpssScore = context.EpssScore,
|
||||
CvssScore = context.CvssScore,
|
||||
IsKev = context.IsKev,
|
||||
ContextTags = context.ContextTags
|
||||
};
|
||||
|
||||
// Map action to decision
|
||||
var decision = new DecisionRecord
|
||||
{
|
||||
Action = MapActionType(action.ActionType),
|
||||
Rationale = action.Parameters.GetValueOrDefault("rationale")
|
||||
?? $"Decision via AI chat: {action.ActionType}",
|
||||
DecidedBy = context.UserId,
|
||||
DecidedAt = _timeProvider.GetUtcNow(),
|
||||
PolicyReference = action.PolicyGateUsed
|
||||
};
|
||||
|
||||
// Record
|
||||
var record = new OpsMemoryRecord
|
||||
{
|
||||
MemoryId = _guidGenerator.NewGuid().ToString(),
|
||||
TenantId = context.TenantId,
|
||||
RecordedAt = _timeProvider.GetUtcNow(),
|
||||
Situation = situation,
|
||||
Decision = decision,
|
||||
Outcome = null, // Outcome recorded later
|
||||
SimilarityVector = _vectorGenerator.Generate(situation)
|
||||
};
|
||||
|
||||
await _store.RecordDecisionAsync(record, cancellationToken);
|
||||
|
||||
// Link to AI attestation if available
|
||||
if (action.RunId is not null)
|
||||
{
|
||||
await _store.LinkToAttestationAsync(
|
||||
record.MemoryId,
|
||||
action.RunId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private static DecisionAction MapActionType(string actionType) => actionType switch
|
||||
{
|
||||
"approve" => DecisionAction.Accept,
|
||||
"quarantine" => DecisionAction.Quarantine,
|
||||
"defer" => DecisionAction.Defer,
|
||||
"create_vex" => DecisionAction.Accept, // VEX creation implies acceptance
|
||||
_ => DecisionAction.Other
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Integration Point:**
|
||||
```csharp
|
||||
// In ActionExecutor.ExecuteAsync()
|
||||
var result = await ExecuteActionCoreAsync(proposal, context, cancellationToken);
|
||||
|
||||
if (result.Success && _options.RecordToOpsMemory)
|
||||
{
|
||||
await _decisionRecorder.RecordFromActionAsync(result, context, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Decisions recorded when actions execute
|
||||
- [ ] Situation extracted from chat context
|
||||
- [ ] Rationale captured from action parameters
|
||||
- [ ] Linked to AI attestation
|
||||
- [ ] Fire-and-forget (doesn't block action)
|
||||
|
||||
---
|
||||
|
||||
### OMCI-007: Storage for Typed Memory
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/StellaOps.OpsMemory/Storage/` |
|
||||
|
||||
**New Interfaces:**
|
||||
```csharp
|
||||
public interface IKnownIssueStore
|
||||
{
|
||||
Task<KnownIssue> CreateAsync(KnownIssue issue, CancellationToken ct);
|
||||
Task<KnownIssue?> GetByIdAsync(string tenantId, string issueId, CancellationToken ct);
|
||||
Task<ImmutableArray<KnownIssue>> FindByContextAsync(
|
||||
string tenantId, string? cveId, string? component, CancellationToken ct);
|
||||
Task UpdateStatusAsync(string tenantId, string issueId, KnownIssueStatus status, CancellationToken ct);
|
||||
}
|
||||
|
||||
public interface ITacticStore
|
||||
{
|
||||
Task<Tactic> CreateAsync(Tactic tactic, CancellationToken ct);
|
||||
Task<Tactic?> GetByIdAsync(string tenantId, string tacticId, CancellationToken ct);
|
||||
Task<ImmutableArray<Tactic>> FindByTriggerAsync(
|
||||
string tenantId, TacticTrigger trigger, CancellationToken ct);
|
||||
Task IncrementUsageAsync(string tenantId, string tacticId, bool success, CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
**Database Schema:**
|
||||
```sql
|
||||
-- Known Issues
|
||||
CREATE TABLE opsmemory.known_issues (
|
||||
issue_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
affected_components TEXT[],
|
||||
affected_cves TEXT[],
|
||||
resolution TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ,
|
||||
created_by TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_known_issues_tenant ON opsmemory.known_issues(tenant_id);
|
||||
CREATE INDEX idx_known_issues_cves ON opsmemory.known_issues USING gin(affected_cves);
|
||||
CREATE INDEX idx_known_issues_status ON opsmemory.known_issues(status);
|
||||
|
||||
-- Tactics
|
||||
CREATE TABLE opsmemory.tactics (
|
||||
tactic_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
trigger JSONB NOT NULL,
|
||||
steps JSONB NOT NULL,
|
||||
success_rate DECIMAL(5,4) NOT NULL DEFAULT 0,
|
||||
times_used INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
created_by TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tactics_tenant ON opsmemory.tactics(tenant_id);
|
||||
CREATE INDEX idx_tactics_trigger ON opsmemory.tactics USING gin(trigger);
|
||||
|
||||
-- Attestation links
|
||||
ALTER TABLE opsmemory.decisions
|
||||
ADD COLUMN attestation_run_id TEXT;
|
||||
|
||||
CREATE INDEX idx_decisions_attestation ON opsmemory.decisions(attestation_run_id)
|
||||
WHERE attestation_run_id IS NOT NULL;
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] PostgreSQL stores for KnownIssue and Tactic
|
||||
- [ ] GIN indexes for efficient trigger matching
|
||||
- [ ] Attestation link column added
|
||||
- [ ] All stores use tenant isolation
|
||||
|
||||
---
|
||||
|
||||
### OMCI-008: Unit Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/__Tests/StellaOps.OpsMemory.Tests/Unit/` |
|
||||
|
||||
**Test Classes:**
|
||||
1. `OpsMemoryChatProviderTests`
|
||||
- [ ] EnrichContext with matching decisions
|
||||
- [ ] EnrichContext with no matches
|
||||
- [ ] EnrichContext filters to successful outcomes
|
||||
- [ ] EnrichContext includes known issues
|
||||
- [ ] EnrichContext includes tactics
|
||||
|
||||
2. `OpsMemoryDecisionRecorderTests`
|
||||
- [ ] Records decision from approve action
|
||||
- [ ] Records decision from quarantine action
|
||||
- [ ] Extracts situation from context
|
||||
- [ ] Links to attestation
|
||||
|
||||
3. `OpsMemoryLinkResolverTests`
|
||||
- [ ] Resolves valid memory ID
|
||||
- [ ] Returns false for invalid ID
|
||||
- [ ] Returns metadata
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] >90% code coverage
|
||||
- [ ] All tests `[Trait("Category", "Unit")]`
|
||||
- [ ] Tests use mock stores
|
||||
|
||||
---
|
||||
|
||||
### OMCI-009: Integration Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/OpsMemory/__Tests/StellaOps.OpsMemory.Tests/Integration/OpsMemoryChatProviderIntegrationTests.cs` |
|
||||
|
||||
**Test Scenarios:**
|
||||
- [ ] Full flow: Chat → Action → OpsMemory record
|
||||
- [ ] Context enrichment with real PostgreSQL
|
||||
- [ ] Known issue and tactic queries
|
||||
- [ ] Attestation linking
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Uses Testcontainers PostgreSQL
|
||||
- [ ] All tests `[Trait("Category", "Integration")]`
|
||||
|
||||
---
|
||||
|
||||
### OMCI-010: Documentation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `docs/modules/opsmemory/chat-integration.md` |
|
||||
|
||||
**Content:**
|
||||
- [x] Architecture diagram
|
||||
- [x] Configuration options
|
||||
- [x] Object link format
|
||||
- [x] Known issue and tactic management
|
||||
- [x] Examples
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
AdvisoryAI:
|
||||
Chat:
|
||||
OpsMemory:
|
||||
Enabled: true
|
||||
MaxSuggestions: 3
|
||||
MinSimilarity: 0.5
|
||||
IncludeKnownIssues: true
|
||||
IncludeTactics: true
|
||||
RecordDecisions: true
|
||||
|
||||
OpsMemory:
|
||||
Integration:
|
||||
AttestationLinking: true
|
||||
FireAndForget: true # Don't block on recording
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions & Risks
|
||||
|
||||
| Decision/Risk | Notes |
|
||||
|---------------|-------|
|
||||
| Fire-and-forget recording | May lose records on crash; acceptable for UX |
|
||||
| Similarity threshold | 0.5 may be too low; tune based on feedback |
|
||||
| Tactic trigger matching | JSON query may be slow; consider materialized columns |
|
||||
|
||||
---
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Task | Action |
|
||||
|------|------|--------|
|
||||
| 09-Jan-2026 | Sprint | Created sprint definition file |
|
||||
| 10-Jan-2026 | OMCI-001 | Created IOpsMemoryChatProvider interface with models |
|
||||
| 10-Jan-2026 | OMCI-002 | Implemented OpsMemoryChatProvider |
|
||||
| 10-Jan-2026 | OMCI-003 | Created IPlaybookSuggestionService interface |
|
||||
| 10-Jan-2026 | OMCI-003 | Implemented OpsMemoryContextEnricher |
|
||||
| 10-Jan-2026 | OMCI-004 | Created OpsMemoryIntegration for AdvisoryAI |
|
||||
| 10-Jan-2026 | OMCI-008 | Created unit tests for OpsMemoryChatProvider and OpsMemoryContextEnricher |
|
||||
| 10-Jan-2026 | OMCI-005 | Created OpsMemoryLinkResolver + CompositeObjectLinkResolver |
|
||||
| 10-Jan-2026 | OMCI-007 | Created IKnownIssueStore and ITacticStore interfaces |
|
||||
| 10-Jan-2026 | OMCI-009 | Created OpsMemoryChatProviderIntegrationTests with 6 tests |
|
||||
| 10-Jan-2026 | OMCI-010 | Created docs/modules/opsmemory/chat-integration.md |
|
||||
| 10-Jan-2026 | Sprint | All tasks completed |
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [x] All 10 tasks complete
|
||||
- [x] Past decisions surface in chat
|
||||
- [x] Decisions auto-recorded from actions
|
||||
- [x] Object links resolve correctly
|
||||
- [x] All tests passing
|
||||
- [x] Documentation complete
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 10-Jan-2026_
|
||||
@@ -1,782 +0,0 @@
|
||||
# Sprint SPRINT_20260109_011_003_BE - AI Runs Framework
|
||||
|
||||
> **Parent:** [SPRINT_20260109_011_000_INDEX](./SPRINT_20260109_011_000_INDEX_ai_moats.md)
|
||||
> **Status:** DONE
|
||||
> **Created:** 09-Jan-2026
|
||||
> **Module:** BE (Backend) + FE (Frontend)
|
||||
> **Depends On:** SPRINT_20260109_011_001_LB (AI Attestations)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the "Run" concept - an auditable container for AI interactions that captures the complete lifecycle from initial query through tool calls, artifact generation, and approvals.
|
||||
|
||||
### Why This Matters (from ADVISORY-AI-000)
|
||||
|
||||
> "Chat is not auditable, repeatable, actionable with guardrails, or collaborative."
|
||||
|
||||
The Run concept transforms ephemeral chat into:
|
||||
- **Auditable:** Every interaction logged with timestamps
|
||||
- **Repeatable:** Deterministic replay possible
|
||||
- **Actionable:** Artifacts produced (not just text)
|
||||
- **Collaborative:** Handoffs, approvals, shared context
|
||||
|
||||
---
|
||||
|
||||
## Working Directory
|
||||
|
||||
- `src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/` (new)
|
||||
- `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs` (new)
|
||||
- `src/Web/StellaOps.Web/src/app/features/advisory-ai/runs/` (new)
|
||||
- `src/__Libraries/__Tests/StellaOps.AdvisoryAI.Tests/Runs/` (new)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Existing: `ConversationService` - Chat infrastructure
|
||||
- Existing: `ActionProposalParser` - Action extraction
|
||||
- Existing: `GroundingValidator` - Evidence validation
|
||||
- Required: AI Attestations (011_001) for Run attestation
|
||||
|
||||
---
|
||||
|
||||
## Run Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Run Lifecycle │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Created │ → │ Active │ → │ Pending │ → │ Complete │ │
|
||||
│ │ │ │ │ │ Approval │ │ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Run Timeline │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │ Created │→│ User │→│Assistant│→│ Action │→│Approval │→ ... │ │
|
||||
│ │ │ Event │ │ Turn │ │ Turn │ │Proposed │ │ Request │ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Artifacts Produced │ │
|
||||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||||
│ │ │ Evidence │ │ Decision │ │ Action │ │ VEX │ │ │
|
||||
│ │ │ Pack │ │ Record │ │ Result │ │ Statement │ │ │
|
||||
│ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Run Attestation (DSSE) │ │
|
||||
│ │ • Content digest of all turns │ │
|
||||
│ │ • Evidence references │ │
|
||||
│ │ • Artifact digests │ │
|
||||
│ │ • Signed by platform key │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### RUN-001: Run Domain Model
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/Models/` |
|
||||
|
||||
**Models:**
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// An auditable container for an AI-assisted investigation session.
|
||||
/// </summary>
|
||||
public sealed record Run
|
||||
{
|
||||
public required string RunId { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string UserId { get; init; }
|
||||
public required string ConversationId { get; init; }
|
||||
public required RunStatus Status { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
|
||||
// Context
|
||||
public required RunContext Context { get; init; }
|
||||
|
||||
// Timeline
|
||||
public ImmutableArray<RunTimelineEvent> Timeline { get; init; }
|
||||
|
||||
// Artifacts
|
||||
public ImmutableArray<RunArtifact> Artifacts { get; init; }
|
||||
|
||||
// Attestation (set on completion)
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public enum RunStatus
|
||||
{
|
||||
Created,
|
||||
Active,
|
||||
PendingApproval,
|
||||
Completed,
|
||||
Cancelled,
|
||||
Failed
|
||||
}
|
||||
|
||||
public sealed record RunContext
|
||||
{
|
||||
public string? FindingId { get; init; }
|
||||
public string? CveId { get; init; }
|
||||
public string? Component { get; init; }
|
||||
public string? ScanId { get; init; }
|
||||
public string? SbomId { get; init; }
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RunTimelineEvent
|
||||
{
|
||||
public required string EventId { get; init; }
|
||||
public required RunEventType EventType { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public required string Actor { get; init; } // "user:X", "assistant", "system"
|
||||
public required string Summary { get; init; }
|
||||
public ImmutableDictionary<string, object>? Details { get; init; }
|
||||
public string? RelatedTurnId { get; init; }
|
||||
public string? RelatedArtifactId { get; init; }
|
||||
}
|
||||
|
||||
public enum RunEventType
|
||||
{
|
||||
RunCreated,
|
||||
UserTurn,
|
||||
AssistantTurn,
|
||||
ToolCall,
|
||||
ActionProposed,
|
||||
ApprovalRequested,
|
||||
ApprovalGranted,
|
||||
ApprovalDenied,
|
||||
ActionExecuted,
|
||||
ActionFailed,
|
||||
ArtifactCreated,
|
||||
RunCompleted,
|
||||
RunCancelled,
|
||||
RunFailed
|
||||
}
|
||||
|
||||
public sealed record RunArtifact
|
||||
{
|
||||
public required string ArtifactId { get; init; }
|
||||
public required RunArtifactType Type { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string ContentDigest { get; init; }
|
||||
public required string Uri { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
public enum RunArtifactType
|
||||
{
|
||||
EvidencePack,
|
||||
DecisionRecord,
|
||||
VexStatement,
|
||||
ActionResult,
|
||||
Explanation,
|
||||
Report
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] All models are immutable records
|
||||
- [ ] Timeline captures full event history
|
||||
- [ ] Artifacts linked by URI and digest
|
||||
- [ ] Status machine is well-defined
|
||||
|
||||
---
|
||||
|
||||
### RUN-002: IRunService Interface
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/IRunService.cs` |
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IRunService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new Run from a conversation.
|
||||
/// </summary>
|
||||
Task<Run> CreateRunAsync(
|
||||
string conversationId,
|
||||
RunContext context,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Run by ID.
|
||||
/// </summary>
|
||||
Task<Run?> GetRunAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an event to the Run timeline.
|
||||
/// </summary>
|
||||
Task AddTimelineEventAsync(
|
||||
string runId,
|
||||
RunTimelineEvent @event,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches an artifact to the Run.
|
||||
/// </summary>
|
||||
Task AttachArtifactAsync(
|
||||
string runId,
|
||||
RunArtifact artifact,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Completes a Run and generates attestation.
|
||||
/// </summary>
|
||||
Task<Run> CompleteRunAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a Run.
|
||||
/// </summary>
|
||||
Task CancelRunAsync(
|
||||
string runId,
|
||||
string reason,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lists Runs for a tenant.
|
||||
/// </summary>
|
||||
Task<PagedResult<Run>> ListRunsAsync(
|
||||
string tenantId,
|
||||
RunQuery query,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Replays a Run for verification.
|
||||
/// </summary>
|
||||
Task<RunReplayResult> ReplayRunAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record RunQuery
|
||||
{
|
||||
public string? UserId { get; init; }
|
||||
public string? FindingId { get; init; }
|
||||
public RunStatus? Status { get; init; }
|
||||
public DateTimeOffset? Since { get; init; }
|
||||
public DateTimeOffset? Until { get; init; }
|
||||
public int Limit { get; init; } = 50;
|
||||
public string? Cursor { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RunReplayResult
|
||||
{
|
||||
public required bool Deterministic { get; init; }
|
||||
public required string OriginalDigest { get; init; }
|
||||
public required string ReplayDigest { get; init; }
|
||||
public ImmutableArray<string> Differences { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] CRUD operations for Runs
|
||||
- [ ] Timeline event streaming
|
||||
- [ ] Artifact attachment
|
||||
- [ ] Completion with attestation
|
||||
- [ ] Replay capability
|
||||
|
||||
---
|
||||
|
||||
### RUN-003: RunService Implementation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/RunService.cs` |
|
||||
|
||||
**Key Implementation:**
|
||||
```csharp
|
||||
internal sealed class RunService : IRunService
|
||||
{
|
||||
private readonly IRunStore _store;
|
||||
private readonly IConversationService _conversationService;
|
||||
private readonly IAiAttestationService _attestationService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
|
||||
public async Task<Run> CompleteRunAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var run = await _store.GetByIdAsync(runId, cancellationToken)
|
||||
?? throw new RunNotFoundException(runId);
|
||||
|
||||
// Add completion event
|
||||
var completionEvent = new RunTimelineEvent
|
||||
{
|
||||
EventId = _guidGenerator.NewGuid().ToString(),
|
||||
EventType = RunEventType.RunCompleted,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
Actor = "system",
|
||||
Summary = "Run completed"
|
||||
};
|
||||
|
||||
await _store.AddTimelineEventAsync(runId, completionEvent, cancellationToken);
|
||||
|
||||
// Create attestation
|
||||
var attestation = await _attestationService.CreateRunAttestationAsync(
|
||||
run, cancellationToken);
|
||||
var envelope = await _attestationService.SignAttestationAsync(
|
||||
attestation, cancellationToken);
|
||||
|
||||
// Update run with attestation
|
||||
var completedRun = run with
|
||||
{
|
||||
Status = RunStatus.Completed,
|
||||
CompletedAt = _timeProvider.GetUtcNow(),
|
||||
AttestationDigest = attestation.ContentDigest
|
||||
};
|
||||
|
||||
await _store.UpdateAsync(completedRun, cancellationToken);
|
||||
await _store.StoreAttestationAsync(runId, envelope, cancellationToken);
|
||||
|
||||
return completedRun;
|
||||
}
|
||||
|
||||
public async Task<RunReplayResult> ReplayRunAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var run = await _store.GetByIdAsync(runId, cancellationToken)
|
||||
?? throw new RunNotFoundException(runId);
|
||||
|
||||
// Replay each user turn through the pipeline
|
||||
var replayDigests = new List<string>();
|
||||
|
||||
foreach (var @event in run.Timeline.Where(e => e.EventType == RunEventType.UserTurn))
|
||||
{
|
||||
var turn = await _conversationService.GetTurnAsync(
|
||||
@event.RelatedTurnId!, cancellationToken);
|
||||
|
||||
// Re-run through prompt assembly + inference (with deterministic mode)
|
||||
var replayResult = await _conversationService.ReplayTurnAsync(
|
||||
turn, cancellationToken);
|
||||
|
||||
replayDigests.Add(replayResult.ContentDigest);
|
||||
}
|
||||
|
||||
// Compare digests
|
||||
var originalDigests = run.Timeline
|
||||
.Where(e => e.EventType == RunEventType.AssistantTurn)
|
||||
.Select(e => e.Details?["contentDigest"]?.ToString() ?? "")
|
||||
.ToList();
|
||||
|
||||
var differences = new List<string>();
|
||||
for (var i = 0; i < Math.Min(replayDigests.Count, originalDigests.Count); i++)
|
||||
{
|
||||
if (replayDigests[i] != originalDigests[i])
|
||||
{
|
||||
differences.Add($"Turn {i}: original={originalDigests[i]}, replay={replayDigests[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
return new RunReplayResult
|
||||
{
|
||||
Deterministic = differences.Count == 0,
|
||||
OriginalDigest = run.AttestationDigest ?? "",
|
||||
ReplayDigest = ComputeDigest(replayDigests),
|
||||
Differences = differences.ToImmutableArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Creates Runs from conversations
|
||||
- [ ] Manages timeline events
|
||||
- [ ] Generates attestation on completion
|
||||
- [ ] Replay produces determinism report
|
||||
- [ ] All operations use injected TimeProvider
|
||||
|
||||
---
|
||||
|
||||
### RUN-004: Run Storage
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/Storage/` |
|
||||
|
||||
**PostgreSQL Schema:**
|
||||
```sql
|
||||
CREATE TABLE advisoryai.runs (
|
||||
run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
conversation_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'Created',
|
||||
context JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
attestation_digest TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_runs_tenant ON advisoryai.runs(tenant_id);
|
||||
CREATE INDEX idx_runs_user ON advisoryai.runs(tenant_id, user_id);
|
||||
CREATE INDEX idx_runs_status ON advisoryai.runs(status);
|
||||
CREATE INDEX idx_runs_conversation ON advisoryai.runs(conversation_id);
|
||||
|
||||
CREATE TABLE advisoryai.run_timeline (
|
||||
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
run_id UUID NOT NULL REFERENCES advisoryai.runs(run_id),
|
||||
event_type TEXT NOT NULL,
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
details JSONB,
|
||||
related_turn_id TEXT,
|
||||
related_artifact_id TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_timeline_run ON advisoryai.run_timeline(run_id);
|
||||
CREATE INDEX idx_timeline_timestamp ON advisoryai.run_timeline(run_id, timestamp);
|
||||
|
||||
CREATE TABLE advisoryai.run_artifacts (
|
||||
artifact_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
run_id UUID NOT NULL REFERENCES advisoryai.runs(run_id),
|
||||
artifact_type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content_digest TEXT NOT NULL,
|
||||
uri TEXT NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_artifacts_run ON advisoryai.run_artifacts(run_id);
|
||||
CREATE INDEX idx_artifacts_type ON advisoryai.run_artifacts(run_id, artifact_type);
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] PostgreSQL store implementation
|
||||
- [ ] Timeline events append-only
|
||||
- [ ] Artifacts linked to runs
|
||||
- [ ] Efficient queries by tenant/user/status
|
||||
|
||||
---
|
||||
|
||||
### RUN-005: Chat Integration
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Chat/RunIntegration.cs` |
|
||||
|
||||
**Auto-create Run from conversation:**
|
||||
```csharp
|
||||
// In ConversationService.AddTurnAsync()
|
||||
if (_options.AutoCreateRuns && conversation.TurnCount == 1)
|
||||
{
|
||||
var run = await _runService.CreateRunAsync(
|
||||
conversation.ConversationId,
|
||||
ExtractContext(conversation),
|
||||
cancellationToken);
|
||||
|
||||
conversation = conversation with { RunId = run.RunId };
|
||||
}
|
||||
|
||||
// Log turn to timeline
|
||||
if (conversation.RunId is not null)
|
||||
{
|
||||
await _runService.AddTimelineEventAsync(
|
||||
conversation.RunId,
|
||||
new RunTimelineEvent
|
||||
{
|
||||
EventId = turn.TurnId,
|
||||
EventType = turn.Role == Role.User
|
||||
? RunEventType.UserTurn
|
||||
: RunEventType.AssistantTurn,
|
||||
Timestamp = turn.Timestamp,
|
||||
Actor = turn.Role == Role.User
|
||||
? $"user:{conversation.UserId}"
|
||||
: "assistant",
|
||||
Summary = TruncateSummary(turn.Content),
|
||||
Details = new Dictionary<string, object>
|
||||
{
|
||||
["contentDigest"] = ComputeDigest(turn.Content),
|
||||
["groundingScore"] = groundingResult.GroundingScore
|
||||
}.ToImmutableDictionary(),
|
||||
RelatedTurnId = turn.TurnId
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Runs auto-created from first turn
|
||||
- [ ] All turns logged to timeline
|
||||
- [ ] Content digest captured for replay
|
||||
- [ ] Grounding score included
|
||||
|
||||
---
|
||||
|
||||
### RUN-006: API Endpoints
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs` |
|
||||
|
||||
**Endpoints:**
|
||||
```http
|
||||
POST /api/v1/advisory-ai/runs
|
||||
Body: { conversationId, context }
|
||||
→ Creates new Run
|
||||
|
||||
GET /api/v1/advisory-ai/runs/{runId}
|
||||
→ Returns Run with timeline and artifacts
|
||||
|
||||
GET /api/v1/advisory-ai/runs/{runId}/timeline
|
||||
→ Returns timeline events (supports pagination)
|
||||
|
||||
GET /api/v1/advisory-ai/runs/{runId}/artifacts
|
||||
→ Returns artifacts list
|
||||
|
||||
POST /api/v1/advisory-ai/runs/{runId}/complete
|
||||
→ Completes Run and generates attestation
|
||||
|
||||
POST /api/v1/advisory-ai/runs/{runId}/cancel
|
||||
Body: { reason }
|
||||
→ Cancels Run
|
||||
|
||||
POST /api/v1/advisory-ai/runs/{runId}/replay
|
||||
→ Replays Run for verification
|
||||
|
||||
GET /api/v1/advisory-ai/runs
|
||||
Query: tenantId, userId, status, since, until, limit, cursor
|
||||
→ Lists Runs with pagination
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] All endpoints require authentication
|
||||
- [ ] Tenant isolation enforced
|
||||
- [ ] Pagination for timeline and lists
|
||||
- [ ] Replay endpoint returns determinism report
|
||||
|
||||
---
|
||||
|
||||
### RUN-007: Run Timeline UI Component
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/Web/StellaOps.Web/src/app/features/ai-runs/` |
|
||||
|
||||
**Components:**
|
||||
```typescript
|
||||
// run-timeline.component.ts
|
||||
@Component({
|
||||
selector: 'stella-run-timeline',
|
||||
template: `
|
||||
<div class="run-timeline">
|
||||
<div class="run-header">
|
||||
<h2>Run: {{ run.runId }}</h2>
|
||||
<stella-run-status-badge [status]="run.status" />
|
||||
<span class="run-meta">
|
||||
Started {{ run.createdAt | date:'short' }}
|
||||
<ng-container *ngIf="run.completedAt">
|
||||
| Completed {{ run.completedAt | date:'short' }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="timeline-events">
|
||||
<div *ngFor="let event of run.timeline" class="timeline-event"
|
||||
[ngClass]="event.eventType">
|
||||
<div class="event-marker">
|
||||
<stella-event-icon [type]="event.eventType" />
|
||||
</div>
|
||||
<div class="event-content">
|
||||
<div class="event-header">
|
||||
<span class="event-actor">{{ event.actor }}</span>
|
||||
<span class="event-time">{{ event.timestamp | date:'HH:mm:ss' }}</span>
|
||||
</div>
|
||||
<div class="event-summary">{{ event.summary }}</div>
|
||||
<div *ngIf="event.details" class="event-details">
|
||||
<ng-container [ngSwitch]="event.eventType">
|
||||
<stella-turn-detail *ngSwitchCase="'AssistantTurn'"
|
||||
[details]="event.details" />
|
||||
<stella-action-detail *ngSwitchCase="'ActionProposed'"
|
||||
[details]="event.details" />
|
||||
<stella-artifact-link *ngSwitchCase="'ArtifactCreated'"
|
||||
[details]="event.details" />
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="run-artifacts" *ngIf="run.artifacts.length > 0">
|
||||
<h3>Artifacts</h3>
|
||||
<div class="artifact-grid">
|
||||
<stella-artifact-card *ngFor="let artifact of run.artifacts"
|
||||
[artifact]="artifact" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="run-attestation" *ngIf="run.attestationDigest">
|
||||
<stella-attestation-badge [digest]="run.attestationDigest" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class RunTimelineComponent {
|
||||
@Input() run: Run;
|
||||
}
|
||||
```
|
||||
|
||||
**Additional Components:**
|
||||
- `run-status-badge.component.ts` - Status visualization
|
||||
- `event-icon.component.ts` - Timeline markers
|
||||
- `artifact-card.component.ts` - Artifact cards
|
||||
- `run-list.component.ts` - Run listing
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Timeline visualizes all events
|
||||
- [x] Event types have distinct icons
|
||||
- [x] Artifacts displayed as cards
|
||||
- [x] Attestation badge shows verification status
|
||||
- [x] Responsive design
|
||||
|
||||
---
|
||||
|
||||
### RUN-008: Unit Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/__Tests/StellaOps.AdvisoryAI.Tests/Runs/` |
|
||||
|
||||
**Test Classes:**
|
||||
1. `RunServiceTests`
|
||||
- [ ] Create Run from conversation
|
||||
- [ ] Add timeline events
|
||||
- [ ] Attach artifacts
|
||||
- [ ] Complete Run generates attestation
|
||||
- [ ] Cancel Run sets status
|
||||
|
||||
2. `RunReplayTests`
|
||||
- [ ] Replay deterministic run
|
||||
- [ ] Detect non-deterministic differences
|
||||
- [ ] Handle missing turns gracefully
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] >90% code coverage
|
||||
- [ ] All tests `[Trait("Category", "Unit")]`
|
||||
|
||||
---
|
||||
|
||||
### RUN-009: Integration Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Runs/Integration/` |
|
||||
|
||||
**Test Scenarios:**
|
||||
- [x] Full conversation -> Run -> attestation flow
|
||||
- [x] Timeline persistence
|
||||
- [x] Artifact storage and retrieval
|
||||
- [x] Run replay verification
|
||||
|
||||
---
|
||||
|
||||
### RUN-010: Documentation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `docs/modules/advisory-ai/runs.md` |
|
||||
|
||||
**Content:**
|
||||
- [x] Run concept and lifecycle
|
||||
- [x] API reference
|
||||
- [x] Timeline event types
|
||||
- [x] Artifact types
|
||||
- [x] Replay verification
|
||||
- [x] UI guide
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
AdvisoryAI:
|
||||
Runs:
|
||||
Enabled: true
|
||||
AutoCreate: true # Auto-create from first conversation turn
|
||||
RetentionDays: 90
|
||||
AttestOnComplete: true
|
||||
ReplayEnabled: true
|
||||
|
||||
Timeline:
|
||||
MaxEventsPerRun: 1000
|
||||
ContentDigestAlgorithm: sha256
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions & Risks
|
||||
|
||||
| Decision/Risk | Notes |
|
||||
|---------------|-------|
|
||||
| Auto-create vs explicit | Auto-create reduces friction but may create many short-lived Runs |
|
||||
| Timeline event storage | Append-only for audit; may grow large |
|
||||
| Replay determinism | LLM responses vary; capture digest, not expect exact match |
|
||||
| Run retention | Need retention policy to manage storage |
|
||||
|
||||
---
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Task | Action |
|
||||
|------|------|--------|
|
||||
| 09-Jan-2026 | Sprint | Created sprint definition file |
|
||||
| 10-Jan-2026 | RUN-001 to RUN-008 | Completed domain models, services, storage, chat integration, API endpoints, unit tests |
|
||||
| 10-Jan-2026 | RUN-009 | Completed integration tests for Run service |
|
||||
| 10-Jan-2026 | RUN-010 | Created comprehensive documentation at docs/modules/advisory-ai/runs.md |
|
||||
| 10-Jan-2026 | Sprint | All backend tasks complete (RUN-007 FE blocked) |
|
||||
| 10-Jan-2026 | RUN-007 | Created AI Runs UI components (viewer, list) in features/ai-runs/ |
|
||||
| 10-Jan-2026 | RUN-007 | Registered AI Runs API client in app.config.ts |
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [x] All 10 tasks complete
|
||||
- [x] Runs capture full interaction history
|
||||
- [x] Timeline shows all events
|
||||
- [x] Attestation generated on completion
|
||||
- [x] Replay reports determinism
|
||||
- [x] All tests passing
|
||||
- [x] Documentation complete
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 10-Jan-2026_
|
||||
@@ -1,843 +0,0 @@
|
||||
# Sprint SPRINT_20260109_011_004_BE - Policy-Action Integration
|
||||
|
||||
> **Parent:** [SPRINT_20260109_011_000_INDEX](./SPRINT_20260109_011_000_INDEX_ai_moats.md)
|
||||
> **Status:** DONE
|
||||
> **Created:** 09-Jan-2026
|
||||
> **Module:** BE (Backend)
|
||||
> **Depends On:** SPRINT_20260109_011_003_BE (AI Runs Framework)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Connect AI-proposed actions to the Policy Engine's K4 lattice for governance-aware automation. Move beyond simple role checks to VEX-aware policy gates with approval workflows.
|
||||
|
||||
### Why This Matters (from ADVISORY-AI-002)
|
||||
|
||||
> "The main blocker to 'AI that acts' is governance: wrong environment, insufficient permission, missing approvals, non-idempotent actions, unclear accountability."
|
||||
|
||||
Current state: ActionProposalParser checks roles.
|
||||
Target state: Full policy evaluation with:
|
||||
- K4 lattice integration for VEX-aware decisions
|
||||
- Approval workflows for high-risk actions
|
||||
- Idempotency tracking
|
||||
- Action audit ledger
|
||||
|
||||
---
|
||||
|
||||
## Working Directory
|
||||
|
||||
- `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/` (new subdirectory)
|
||||
- `src/Policy/StellaOps.Policy.Engine/Actions/` (integration)
|
||||
- `src/__Libraries/__Tests/StellaOps.AdvisoryAI.Tests/Actions/` (new)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Existing: `ActionProposalParser` - Action extraction
|
||||
- Existing: `Policy.Engine` - K4 lattice logic
|
||||
- Existing: `Policy.ReviewWorkflowService` - Approval workflows
|
||||
- Required: AI Runs (011_003) for action attachment
|
||||
|
||||
---
|
||||
|
||||
## Action Flow Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Action Execution Flow │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. AI Proposes Action │ │
|
||||
│ │ [Accept Risk]{action:approve,cve_id=CVE-2023-44487} │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 2. Policy Gate Evaluation │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Role Check │→ │ K4 Lattice │→ │ Environment │ │ │
|
||||
│ │ │ (existing) │ │ Query │ │ Check │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ PolicyDecision │ │ │
|
||||
│ │ │ • Allow → Execute immediately │ │ │
|
||||
│ │ │ • AllowWithApproval → Route to approval workflow │ │ │
|
||||
│ │ │ • Deny → Reject with explanation │ │ │
|
||||
│ │ │ • DenyWithOverride → Reject but allow admin override│ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────┼───────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ 3a. Execute │ │ 3b. Approval │ │ 3c. Deny │ │
|
||||
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
|
||||
│ │ │Idempotency│ │ │ │ Request │ │ │ │ Explain │ │ │
|
||||
│ │ │ Check │ │ │ │ Created │ │ │ │ Why │ │ │
|
||||
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │ └──────────┘ │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │ ▼ │ │ ▼ │ │ │ │
|
||||
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ │ │
|
||||
│ │ │ Execute │ │ │ │ Wait for │ │ │ │ │
|
||||
│ │ │ Action │ │ │ │ Approval │ │ │ │ │
|
||||
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │ │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │ ▼ │ │ ▼ │ │ │ │
|
||||
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ │ │
|
||||
│ │ │ Record │ │ │ │ Execute │ │ │ │ │
|
||||
│ │ │ to Ledger│ │ │ │on Approve│ │ │ │ │
|
||||
│ │ └──────────┘ │ │ └──────────┘ │ │ │ │
|
||||
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### PACT-001: IActionPolicyGate Interface
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/IActionPolicyGate.cs` |
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IActionPolicyGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates whether an action is allowed by policy.
|
||||
/// </summary>
|
||||
Task<ActionPolicyDecision> EvaluateAsync(
|
||||
ActionProposal proposal,
|
||||
ActionContext context,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable explanation for a policy decision.
|
||||
/// </summary>
|
||||
Task<PolicyExplanation> ExplainAsync(
|
||||
ActionPolicyDecision decision,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record ActionContext
|
||||
{
|
||||
public required string TenantId { get; init; }
|
||||
public required string UserId { get; init; }
|
||||
public required ImmutableArray<string> UserRoles { get; init; }
|
||||
public required string Environment { get; init; } // "production", "staging", etc.
|
||||
public string? RunId { get; init; }
|
||||
public string? FindingId { get; init; }
|
||||
public string? CveId { get; init; }
|
||||
|
||||
// For K4 lattice queries
|
||||
public LatticeState? ReachabilityState { get; init; }
|
||||
public double? EpssScore { get; init; }
|
||||
public bool? IsKev { get; init; }
|
||||
public string? VexStatus { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ActionPolicyDecision
|
||||
{
|
||||
public required ActionPolicyResult Result { get; init; }
|
||||
public required string PolicyId { get; init; }
|
||||
public required string PolicyVersion { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public string? ApprovalWorkflowId { get; init; }
|
||||
public ImmutableArray<string> RequiredApprovers { get; init; }
|
||||
public TimeSpan? ApprovalTimeout { get; init; }
|
||||
public bool AllowOverride { get; init; }
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
public enum ActionPolicyResult
|
||||
{
|
||||
Allow,
|
||||
AllowWithApproval,
|
||||
Deny,
|
||||
DenyWithOverride
|
||||
}
|
||||
|
||||
public sealed record PolicyExplanation
|
||||
{
|
||||
public required string Summary { get; init; }
|
||||
public ImmutableArray<PolicyFactor> Factors { get; init; }
|
||||
public string? SuggestedAlternative { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PolicyFactor
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string Value { get; init; }
|
||||
public required PolicyFactorWeight Weight { get; init; }
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
public enum PolicyFactorWeight
|
||||
{
|
||||
Allow,
|
||||
Neutral,
|
||||
Caution,
|
||||
Block
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Interface supports full policy evaluation
|
||||
- [x] Context includes K4-relevant fields
|
||||
- [x] Decision includes approval workflow info
|
||||
- [x] Explanation is human-readable
|
||||
|
||||
---
|
||||
|
||||
### PACT-002: ActionPolicyGate Implementation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/ActionPolicyGate.cs` |
|
||||
|
||||
**Implementation:**
|
||||
```csharp
|
||||
internal sealed class ActionPolicyGate : IActionPolicyGate
|
||||
{
|
||||
private readonly IPolicyEngine _policyEngine;
|
||||
private readonly IActionRegistry _actionRegistry;
|
||||
private readonly ILogger<ActionPolicyGate> _logger;
|
||||
|
||||
public async Task<ActionPolicyDecision> EvaluateAsync(
|
||||
ActionProposal proposal,
|
||||
ActionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Get action definition
|
||||
var actionDef = _actionRegistry.GetAction(proposal.ActionType)
|
||||
?? throw new UnknownActionTypeException(proposal.ActionType);
|
||||
|
||||
// 2. Check basic role requirement
|
||||
if (!HasRequiredRole(context.UserRoles, actionDef.RequiredRole))
|
||||
{
|
||||
return new ActionPolicyDecision
|
||||
{
|
||||
Result = ActionPolicyResult.Deny,
|
||||
PolicyId = "role-check",
|
||||
PolicyVersion = "1.0",
|
||||
Reason = $"Requires role '{actionDef.RequiredRole}'",
|
||||
AllowOverride = false
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Query K4 lattice for VEX-aware evaluation
|
||||
var k4Query = new K4PolicyQuery
|
||||
{
|
||||
ActionType = proposal.ActionType,
|
||||
TenantId = context.TenantId,
|
||||
Environment = context.Environment,
|
||||
CveId = context.CveId,
|
||||
ReachabilityState = context.ReachabilityState,
|
||||
EpssScore = context.EpssScore,
|
||||
IsKev = context.IsKev,
|
||||
VexStatus = context.VexStatus
|
||||
};
|
||||
|
||||
var k4Result = await _policyEngine.EvaluateK4Async(k4Query, cancellationToken);
|
||||
|
||||
// 4. Map K4 result to action decision
|
||||
return k4Result.Verdict switch
|
||||
{
|
||||
K4Verdict.Allow => CreateAllowDecision(k4Result),
|
||||
K4Verdict.AllowWithReview => CreateApprovalDecision(k4Result, actionDef),
|
||||
K4Verdict.Deny => CreateDenyDecision(k4Result),
|
||||
K4Verdict.DenyOverridable => CreateDenyWithOverrideDecision(k4Result),
|
||||
_ => throw new InvalidOperationException($"Unknown K4 verdict: {k4Result.Verdict}")
|
||||
};
|
||||
}
|
||||
|
||||
private ActionPolicyDecision CreateApprovalDecision(
|
||||
K4PolicyResult k4Result,
|
||||
ActionDefinition actionDef)
|
||||
{
|
||||
// Determine approvers based on action risk level
|
||||
var approvers = actionDef.RiskLevel switch
|
||||
{
|
||||
RiskLevel.Critical => ImmutableArray.Create("security-lead", "ciso"),
|
||||
RiskLevel.High => ImmutableArray.Create("security-lead"),
|
||||
RiskLevel.Medium => ImmutableArray.Create("team-lead"),
|
||||
_ => ImmutableArray.Create("any-approver")
|
||||
};
|
||||
|
||||
return new ActionPolicyDecision
|
||||
{
|
||||
Result = ActionPolicyResult.AllowWithApproval,
|
||||
PolicyId = k4Result.PolicyId,
|
||||
PolicyVersion = k4Result.PolicyVersion,
|
||||
Reason = k4Result.Reason,
|
||||
ApprovalWorkflowId = $"action-approval-{actionDef.RiskLevel}",
|
||||
RequiredApprovers = approvers,
|
||||
ApprovalTimeout = actionDef.RiskLevel == RiskLevel.Critical
|
||||
? TimeSpan.FromHours(24)
|
||||
: TimeSpan.FromHours(4),
|
||||
AllowOverride = false,
|
||||
Metadata = k4Result.Metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Integrates with existing Policy.Engine
|
||||
- [x] Uses K4 lattice for VEX-aware decisions
|
||||
- [x] Maps risk levels to approval requirements
|
||||
- [x] Includes timeout for approvals
|
||||
|
||||
---
|
||||
|
||||
### PACT-003: Action Registry Enhancement
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/ActionRegistry.cs` |
|
||||
|
||||
**Enhanced Action Definitions:**
|
||||
```csharp
|
||||
public sealed record ActionDefinition
|
||||
{
|
||||
public required string ActionType { get; init; }
|
||||
public required string DisplayName { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string RequiredRole { get; init; }
|
||||
public required RiskLevel RiskLevel { get; init; }
|
||||
public required bool IsIdempotent { get; init; }
|
||||
public required bool HasCompensation { get; init; }
|
||||
public ImmutableArray<ActionParameter> Parameters { get; init; }
|
||||
public ImmutableArray<string> AffectedEnvironments { get; init; }
|
||||
public string? CompensationActionType { get; init; }
|
||||
}
|
||||
|
||||
public enum RiskLevel
|
||||
{
|
||||
Low, // Read-only, informational
|
||||
Medium, // Creates records, sends notifications
|
||||
High, // Modifies security posture
|
||||
Critical // Production blockers, quarantine
|
||||
}
|
||||
|
||||
public sealed record ActionParameter
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required bool Required { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? DefaultValue { get; init; }
|
||||
public string? ValidationRegex { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Built-in Actions with Risk Levels:**
|
||||
|
||||
| Action | Risk Level | Idempotent | Compensation |
|
||||
|--------|------------|------------|--------------|
|
||||
| approve | High | Yes | revoke_approval |
|
||||
| quarantine | Critical | Yes | release_quarantine |
|
||||
| defer | Low | Yes | undefer |
|
||||
| create_vex | Medium | No | - |
|
||||
| generate_manifest | Low | Yes | - |
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Actions have risk levels
|
||||
- [x] Idempotency flag per action
|
||||
- [x] Compensation actions defined
|
||||
- [x] Parameter validation
|
||||
|
||||
---
|
||||
|
||||
### PACT-004: Approval Workflow Integration
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/ApprovalWorkflowAdapter.cs` |
|
||||
|
||||
**Integration with existing ReviewWorkflowService:**
|
||||
```csharp
|
||||
internal sealed class ApprovalWorkflowAdapter : IApprovalWorkflowAdapter
|
||||
{
|
||||
private readonly IReviewWorkflowService _reviewService;
|
||||
private readonly IRunService _runService;
|
||||
|
||||
public async Task<ApprovalRequest> CreateApprovalRequestAsync(
|
||||
ActionProposal proposal,
|
||||
ActionPolicyDecision decision,
|
||||
ActionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new ApprovalRequest
|
||||
{
|
||||
RequestId = Guid.NewGuid().ToString(),
|
||||
WorkflowId = decision.ApprovalWorkflowId!,
|
||||
TenantId = context.TenantId,
|
||||
RequesterId = context.UserId,
|
||||
RequiredApprovers = decision.RequiredApprovers,
|
||||
Timeout = decision.ApprovalTimeout ?? TimeSpan.FromHours(4),
|
||||
Payload = new ApprovalPayload
|
||||
{
|
||||
ActionType = proposal.ActionType,
|
||||
ActionLabel = proposal.Label,
|
||||
Parameters = proposal.Parameters,
|
||||
RunId = context.RunId,
|
||||
FindingId = context.FindingId,
|
||||
PolicyReason = decision.Reason
|
||||
},
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
// Create in ReviewWorkflowService
|
||||
await _reviewService.CreateReviewAsync(
|
||||
MapToReviewRequest(request),
|
||||
cancellationToken);
|
||||
|
||||
// Add to Run timeline if in a Run
|
||||
if (context.RunId is not null)
|
||||
{
|
||||
await _runService.AddTimelineEventAsync(
|
||||
context.RunId,
|
||||
new RunTimelineEvent
|
||||
{
|
||||
EventId = request.RequestId,
|
||||
EventType = RunEventType.ApprovalRequested,
|
||||
Timestamp = request.CreatedAt,
|
||||
Actor = $"user:{context.UserId}",
|
||||
Summary = $"Approval requested for {proposal.Label}",
|
||||
Details = new Dictionary<string, object>
|
||||
{
|
||||
["actionType"] = proposal.ActionType,
|
||||
["requiredApprovers"] = decision.RequiredApprovers,
|
||||
["timeout"] = decision.ApprovalTimeout?.ToString() ?? ""
|
||||
}.ToImmutableDictionary()
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
public async Task<ApprovalResult> WaitForApprovalAsync(
|
||||
string requestId,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(timeout);
|
||||
|
||||
try
|
||||
{
|
||||
var review = await _reviewService.WaitForDecisionAsync(requestId, cts.Token);
|
||||
return new ApprovalResult
|
||||
{
|
||||
Approved = review.Decision == ReviewDecision.Approved,
|
||||
ApproverId = review.DecidedBy,
|
||||
DecidedAt = review.DecidedAt,
|
||||
Comments = review.Comments
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
return new ApprovalResult
|
||||
{
|
||||
Approved = false,
|
||||
TimedOut = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Creates approval requests via ReviewWorkflowService
|
||||
- [x] Logs to Run timeline
|
||||
- [x] Supports timeout
|
||||
- [x] Returns approval result
|
||||
|
||||
---
|
||||
|
||||
### PACT-005: Idempotency Handler
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/IdempotencyHandler.cs` |
|
||||
|
||||
**Implementation:**
|
||||
```csharp
|
||||
public interface IIdempotencyHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates an idempotency key for an action.
|
||||
/// </summary>
|
||||
string GenerateKey(ActionProposal proposal, ActionContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an action was already executed.
|
||||
/// </summary>
|
||||
Task<IdempotencyCheckResult> CheckAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Records action execution for idempotency.
|
||||
/// </summary>
|
||||
Task RecordExecutionAsync(
|
||||
string key,
|
||||
ActionExecutionResult result,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record IdempotencyCheckResult
|
||||
{
|
||||
public required bool AlreadyExecuted { get; init; }
|
||||
public ActionExecutionResult? PreviousResult { get; init; }
|
||||
public DateTimeOffset? ExecutedAt { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class IdempotencyHandler : IIdempotencyHandler
|
||||
{
|
||||
public string GenerateKey(ActionProposal proposal, ActionContext context)
|
||||
{
|
||||
// Key components: tenant, action type, target (CVE/component/image)
|
||||
var components = new List<string>
|
||||
{
|
||||
context.TenantId,
|
||||
proposal.ActionType
|
||||
};
|
||||
|
||||
// Add target-specific components
|
||||
if (proposal.Parameters.TryGetValue("cve_id", out var cveId))
|
||||
components.Add($"cve:{cveId}");
|
||||
if (proposal.Parameters.TryGetValue("image_digest", out var digest))
|
||||
components.Add($"image:{digest}");
|
||||
if (proposal.Parameters.TryGetValue("component", out var component))
|
||||
components.Add($"component:{component}");
|
||||
|
||||
var content = string.Join("|", components);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Database:**
|
||||
```sql
|
||||
CREATE TABLE advisoryai.action_executions (
|
||||
idempotency_key TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
parameters JSONB NOT NULL,
|
||||
result JSONB NOT NULL,
|
||||
executed_at TIMESTAMPTZ NOT NULL,
|
||||
executed_by TEXT NOT NULL,
|
||||
run_id TEXT,
|
||||
ttl TIMESTAMPTZ NOT NULL -- For cleanup
|
||||
);
|
||||
|
||||
CREATE INDEX idx_executions_tenant ON advisoryai.action_executions(tenant_id);
|
||||
CREATE INDEX idx_executions_ttl ON advisoryai.action_executions(ttl);
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Generates deterministic keys
|
||||
- [x] Checks before execution
|
||||
- [x] Records execution result
|
||||
- [x] TTL for cleanup
|
||||
|
||||
---
|
||||
|
||||
### PACT-006: Action Audit Ledger
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/ActionAuditLedger.cs` |
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IActionAuditLedger
|
||||
{
|
||||
Task RecordAsync(ActionAuditEntry entry, CancellationToken cancellationToken);
|
||||
Task<ImmutableArray<ActionAuditEntry>> QueryAsync(
|
||||
ActionAuditQuery query, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record ActionAuditEntry
|
||||
{
|
||||
public required string EntryId { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public required string ActionType { get; init; }
|
||||
public required string Actor { get; init; }
|
||||
public required ActionAuditOutcome Outcome { get; init; }
|
||||
|
||||
// Context
|
||||
public string? RunId { get; init; }
|
||||
public string? FindingId { get; init; }
|
||||
public string? CveId { get; init; }
|
||||
|
||||
// Policy decision
|
||||
public string? PolicyId { get; init; }
|
||||
public ActionPolicyResult? PolicyResult { get; init; }
|
||||
public string? ApprovalRequestId { get; init; }
|
||||
public string? ApproverId { get; init; }
|
||||
|
||||
// Execution
|
||||
public ImmutableDictionary<string, string>? Parameters { get; init; }
|
||||
public string? ResultDigest { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
// Attestation
|
||||
public string? AttestationDigest { get; init; }
|
||||
}
|
||||
|
||||
public enum ActionAuditOutcome
|
||||
{
|
||||
Executed,
|
||||
DeniedByPolicy,
|
||||
ApprovalRequested,
|
||||
Approved,
|
||||
ApprovalDenied,
|
||||
ApprovalTimedOut,
|
||||
ExecutionFailed,
|
||||
IdempotentSkipped
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Records all action attempts
|
||||
- [x] Includes policy decision details
|
||||
- [x] Links to attestation
|
||||
- [x] Supports audit queries
|
||||
|
||||
---
|
||||
|
||||
### PACT-007: Action Executor Enhancement
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Actions/ActionExecutor.cs` |
|
||||
|
||||
**Enhanced Execution Flow:**
|
||||
```csharp
|
||||
internal sealed class ActionExecutor : IActionExecutor
|
||||
{
|
||||
public async Task<ActionExecutionResult> ExecuteAsync(
|
||||
ActionProposal proposal,
|
||||
ActionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Check idempotency
|
||||
var idempotencyKey = _idempotencyHandler.GenerateKey(proposal, context);
|
||||
var idempotencyCheck = await _idempotencyHandler.CheckAsync(
|
||||
idempotencyKey, cancellationToken);
|
||||
|
||||
if (idempotencyCheck.AlreadyExecuted)
|
||||
{
|
||||
await _auditLedger.RecordAsync(new ActionAuditEntry
|
||||
{
|
||||
EntryId = _guidGenerator.NewGuid().ToString(),
|
||||
TenantId = context.TenantId,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
ActionType = proposal.ActionType,
|
||||
Actor = context.UserId,
|
||||
Outcome = ActionAuditOutcome.IdempotentSkipped,
|
||||
RunId = context.RunId,
|
||||
Parameters = proposal.Parameters.ToImmutableDictionary()
|
||||
}, cancellationToken);
|
||||
|
||||
return idempotencyCheck.PreviousResult!;
|
||||
}
|
||||
|
||||
// 2. Evaluate policy
|
||||
var policyDecision = await _policyGate.EvaluateAsync(
|
||||
proposal, context, cancellationToken);
|
||||
|
||||
// 3. Handle based on decision
|
||||
var result = policyDecision.Result switch
|
||||
{
|
||||
ActionPolicyResult.Allow =>
|
||||
await ExecuteImmediatelyAsync(proposal, context, policyDecision, cancellationToken),
|
||||
ActionPolicyResult.AllowWithApproval =>
|
||||
await ExecuteWithApprovalAsync(proposal, context, policyDecision, cancellationToken),
|
||||
ActionPolicyResult.Deny =>
|
||||
CreateDeniedResult(proposal, policyDecision),
|
||||
ActionPolicyResult.DenyWithOverride =>
|
||||
CreateDeniedWithOverrideResult(proposal, policyDecision),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
// 4. Record to idempotency store if successful
|
||||
if (result.Success)
|
||||
{
|
||||
await _idempotencyHandler.RecordExecutionAsync(
|
||||
idempotencyKey, result, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<ActionExecutionResult> ExecuteWithApprovalAsync(
|
||||
ActionProposal proposal,
|
||||
ActionContext context,
|
||||
ActionPolicyDecision decision,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Create approval request
|
||||
var request = await _approvalAdapter.CreateApprovalRequestAsync(
|
||||
proposal, decision, context, cancellationToken);
|
||||
|
||||
// Record audit entry
|
||||
await _auditLedger.RecordAsync(new ActionAuditEntry
|
||||
{
|
||||
EntryId = _guidGenerator.NewGuid().ToString(),
|
||||
TenantId = context.TenantId,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
ActionType = proposal.ActionType,
|
||||
Actor = context.UserId,
|
||||
Outcome = ActionAuditOutcome.ApprovalRequested,
|
||||
RunId = context.RunId,
|
||||
PolicyId = decision.PolicyId,
|
||||
PolicyResult = decision.Result,
|
||||
ApprovalRequestId = request.RequestId,
|
||||
Parameters = proposal.Parameters.ToImmutableDictionary()
|
||||
}, cancellationToken);
|
||||
|
||||
// Return pending result (execution happens on approval)
|
||||
return new ActionExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
PendingApproval = true,
|
||||
ApprovalRequestId = request.RequestId,
|
||||
PolicyDecision = decision
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Full policy gate integration
|
||||
- [x] Idempotency checking
|
||||
- [x] Approval workflow routing
|
||||
- [x] Comprehensive audit logging
|
||||
|
||||
---
|
||||
|
||||
### PACT-008: Unit Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Actions/` |
|
||||
|
||||
**Test Classes:**
|
||||
1. `ActionPolicyGateTests`
|
||||
- [x] Allow for low-risk actions
|
||||
- [x] Require approval for high-risk
|
||||
- [x] Deny for missing role
|
||||
- [x] K4 lattice integration
|
||||
|
||||
2. `IdempotencyHandlerTests`
|
||||
- [x] Key generation determinism
|
||||
- [x] Check returns previous result
|
||||
- [x] Different targets = different keys
|
||||
|
||||
3. `ActionExecutorTests`
|
||||
- [x] Execute allowed action
|
||||
- [x] Route to approval
|
||||
- [x] Skip idempotent re-execution
|
||||
- [x] Record audit entries
|
||||
|
||||
---
|
||||
|
||||
### PACT-009: Integration Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Actions/` |
|
||||
|
||||
**Test Scenarios:**
|
||||
- [x] Full approval workflow
|
||||
- [x] Policy engine integration
|
||||
- [x] Audit ledger persistence
|
||||
|
||||
---
|
||||
|
||||
### PACT-010: Documentation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `docs/modules/advisory-ai/policy-integration.md` |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
AdvisoryAI:
|
||||
Actions:
|
||||
PolicyIntegration:
|
||||
Enabled: true
|
||||
DefaultTimeoutHours: 4
|
||||
CriticalTimeoutHours: 24
|
||||
|
||||
Idempotency:
|
||||
Enabled: true
|
||||
TtlDays: 30
|
||||
|
||||
Audit:
|
||||
Enabled: true
|
||||
RetentionDays: 365
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions & Risks
|
||||
|
||||
| Decision/Risk | Notes |
|
||||
|---------------|-------|
|
||||
| K4 lattice coupling | Requires Policy.Engine availability |
|
||||
| Approval timeout | Actions may expire; need notification |
|
||||
| Idempotency key collisions | Low probability with SHA-256 |
|
||||
| Audit storage growth | Need retention policy |
|
||||
|
||||
---
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Task | Action |
|
||||
|------|------|--------|
|
||||
| 09-Jan-2026 | Sprint | Created sprint definition file |
|
||||
| - | - | - |
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] All 10 tasks complete
|
||||
- [ ] Actions routed through K4 policy gate
|
||||
- [ ] Approvals work end-to-end
|
||||
- [ ] Idempotency prevents duplicates
|
||||
- [ ] Full audit trail
|
||||
- [ ] All tests passing
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 09-Jan-2026_
|
||||
@@ -1,989 +0,0 @@
|
||||
# Sprint SPRINT_20260109_011_005_LB - Evidence Pack Artifacts
|
||||
|
||||
> **Parent:** [SPRINT_20260109_011_000_INDEX](./SPRINT_20260109_011_000_INDEX_ai_moats.md)
|
||||
> **Status:** DONE
|
||||
> **Created:** 09-Jan-2026
|
||||
> **Module:** LB (Library) + BE (Backend)
|
||||
> **Depends On:** SPRINT_20260109_011_001_LB (AI Attestations), SPRINT_20260109_011_003_BE (AI Runs)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Create the Evidence Pack as a first-class artifact - a shareable, DSSE-signed bundle of evidence supporting an AI recommendation or security decision.
|
||||
|
||||
### Why This Matters (from ADVISORY-AI-001)
|
||||
|
||||
> "An answer without evidence is a liability. LLMs are persuasive even when wrong."
|
||||
|
||||
Evidence Packs transform ephemeral AI responses into:
|
||||
- **Shareable:** Export for audit, compliance, incident response
|
||||
- **Verifiable:** DSSE-signed with content digests
|
||||
- **Linked:** All evidence URIs resolvable
|
||||
- **Complete:** Contains everything needed to verify a claim
|
||||
|
||||
---
|
||||
|
||||
## Working Directory
|
||||
|
||||
- `src/__Libraries/StellaOps.Evidence.Pack/` (new)
|
||||
- `src/AdvisoryAI/StellaOps.AdvisoryAI/Evidence/` (integration)
|
||||
- `src/Web/StellaOps.Web/src/app/features/evidence-pack/` (new)
|
||||
- `src/__Libraries/__Tests/StellaOps.Evidence.Pack.Tests/` (new)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Existing: `EvidenceLocker` - Evidence storage
|
||||
- Existing: `GroundingValidator` - Evidence extraction from AI responses
|
||||
- Required: AI Attestations (011_001) for signing
|
||||
- Required: AI Runs (011_003) for attachment
|
||||
|
||||
---
|
||||
|
||||
## Evidence Pack Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"packId": "pack-xyz789",
|
||||
"version": "1.0",
|
||||
"createdAt": "2026-01-09T12:05:00Z",
|
||||
"tenantId": "tenant-123",
|
||||
|
||||
"subject": {
|
||||
"type": "finding",
|
||||
"findingId": "finding-456",
|
||||
"cveId": "CVE-2023-44487",
|
||||
"component": "pkg:npm/http2@1.0.0"
|
||||
},
|
||||
|
||||
"claims": [
|
||||
{
|
||||
"claimId": "claim-001",
|
||||
"text": "This component is affected by CVE-2023-44487",
|
||||
"type": "vulnerability_status",
|
||||
"status": "affected",
|
||||
"confidence": 0.92,
|
||||
"evidence": ["ev-001", "ev-002"]
|
||||
},
|
||||
{
|
||||
"claimId": "claim-002",
|
||||
"text": "The vulnerable function is reachable from api-gateway",
|
||||
"type": "reachability",
|
||||
"status": "reachable",
|
||||
"confidence": 0.88,
|
||||
"evidence": ["ev-003"]
|
||||
}
|
||||
],
|
||||
|
||||
"evidence": [
|
||||
{
|
||||
"evidenceId": "ev-001",
|
||||
"type": "sbom",
|
||||
"uri": "stella://sbom/scan-2026-01-09-abc123",
|
||||
"digest": "sha256:abc...",
|
||||
"snapshot": {
|
||||
"component": "pkg:npm/http2@1.0.0",
|
||||
"version": "1.0.0",
|
||||
"foundAt": "2026-01-09T10:00:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"evidenceId": "ev-002",
|
||||
"type": "vex",
|
||||
"uri": "stella://vex/nvd:CVE-2023-44487",
|
||||
"digest": "sha256:def...",
|
||||
"snapshot": {
|
||||
"status": "affected",
|
||||
"issuer": "nvd",
|
||||
"issuedAt": "2023-10-10T00:00:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"evidenceId": "ev-003",
|
||||
"type": "reachability",
|
||||
"uri": "stella://reach/api-gateway:grpc.Server",
|
||||
"digest": "sha256:ghi...",
|
||||
"snapshot": {
|
||||
"latticeState": "ConfirmedReachable",
|
||||
"staticPath": ["entrypoint", "handler", "grpc.Server"],
|
||||
"runtimeObserved": true,
|
||||
"confidence": 0.88
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"context": {
|
||||
"runId": "run-abc123",
|
||||
"conversationId": "conv-456",
|
||||
"userId": "user:alice@example.com",
|
||||
"generatedBy": "AdvisoryAI v2.1"
|
||||
},
|
||||
|
||||
"signatures": {
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"payloadType": "application/vnd.stellaops.evidence-pack+json",
|
||||
"payload": "<base64-encoded-pack>",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "evidence-pack-signing-key",
|
||||
"sig": "<base64-signature>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### EVPK-001: Evidence Pack Models
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/StellaOps.Evidence.Pack/Models/` |
|
||||
|
||||
**Models:**
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A shareable, signed bundle of evidence supporting claims.
|
||||
/// </summary>
|
||||
public sealed record EvidencePack
|
||||
{
|
||||
public required string PackId { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
// What this pack is about
|
||||
public required EvidenceSubject Subject { get; init; }
|
||||
|
||||
// Claims made
|
||||
public required ImmutableArray<EvidenceClaim> Claims { get; init; }
|
||||
|
||||
// Evidence supporting claims
|
||||
public required ImmutableArray<EvidenceItem> Evidence { get; init; }
|
||||
|
||||
// Context (optional)
|
||||
public EvidencePackContext? Context { get; init; }
|
||||
|
||||
// Computed
|
||||
public string ContentDigest => ComputeContentDigest();
|
||||
}
|
||||
|
||||
public sealed record EvidenceSubject
|
||||
{
|
||||
public required EvidenceSubjectType Type { get; init; }
|
||||
public string? FindingId { get; init; }
|
||||
public string? CveId { get; init; }
|
||||
public string? Component { get; init; }
|
||||
public string? ImageDigest { get; init; }
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
public enum EvidenceSubjectType
|
||||
{
|
||||
Finding,
|
||||
Cve,
|
||||
Component,
|
||||
Image,
|
||||
Policy,
|
||||
Custom
|
||||
}
|
||||
|
||||
public sealed record EvidenceClaim
|
||||
{
|
||||
public required string ClaimId { get; init; }
|
||||
public required string Text { get; init; }
|
||||
public required ClaimType Type { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public required ImmutableArray<string> EvidenceIds { get; init; }
|
||||
public string? Source { get; init; } // "ai", "human", "system"
|
||||
}
|
||||
|
||||
public enum ClaimType
|
||||
{
|
||||
VulnerabilityStatus,
|
||||
Reachability,
|
||||
FixAvailability,
|
||||
Severity,
|
||||
Exploitability,
|
||||
Compliance,
|
||||
Custom
|
||||
}
|
||||
|
||||
public sealed record EvidenceItem
|
||||
{
|
||||
public required string EvidenceId { get; init; }
|
||||
public required EvidenceType Type { get; init; }
|
||||
public required string Uri { get; init; }
|
||||
public required string Digest { get; init; }
|
||||
public required DateTimeOffset CollectedAt { get; init; }
|
||||
public required EvidenceSnapshot Snapshot { get; init; }
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
public enum EvidenceType
|
||||
{
|
||||
Sbom,
|
||||
Vex,
|
||||
Reachability,
|
||||
Runtime,
|
||||
Attestation,
|
||||
Advisory,
|
||||
Patch,
|
||||
Policy,
|
||||
OpsMemory,
|
||||
Custom
|
||||
}
|
||||
|
||||
public sealed record EvidenceSnapshot
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required ImmutableDictionary<string, object> Data { get; init; }
|
||||
}
|
||||
|
||||
public sealed record EvidencePackContext
|
||||
{
|
||||
public string? RunId { get; init; }
|
||||
public string? ConversationId { get; init; }
|
||||
public string? UserId { get; init; }
|
||||
public string? GeneratedBy { get; init; }
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] All models are immutable records
|
||||
- [ ] Claims linked to evidence by ID
|
||||
- [ ] Content digest computed deterministically
|
||||
- [ ] Supports multiple evidence types
|
||||
|
||||
---
|
||||
|
||||
### EVPK-002: IEvidencePackService Interface
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/StellaOps.Evidence.Pack/IEvidencePackService.cs` |
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IEvidencePackService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an Evidence Pack from grounding validation results.
|
||||
/// </summary>
|
||||
Task<EvidencePack> CreateFromGroundingAsync(
|
||||
GroundingValidationResult grounding,
|
||||
EvidenceSubject subject,
|
||||
EvidencePackContext? context,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Evidence Pack from a Run's artifacts.
|
||||
/// </summary>
|
||||
Task<EvidencePack> CreateFromRunAsync(
|
||||
string runId,
|
||||
EvidenceSubject subject,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds evidence items to an existing pack (creates new version).
|
||||
/// </summary>
|
||||
Task<EvidencePack> AddEvidenceAsync(
|
||||
string packId,
|
||||
IEnumerable<EvidenceItem> items,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Signs an Evidence Pack with DSSE.
|
||||
/// </summary>
|
||||
Task<SignedEvidencePack> SignAsync(
|
||||
EvidencePack pack,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a signed Evidence Pack.
|
||||
/// </summary>
|
||||
Task<EvidencePackVerificationResult> VerifyAsync(
|
||||
SignedEvidencePack signedPack,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Exports a pack to various formats.
|
||||
/// </summary>
|
||||
Task<EvidencePackExport> ExportAsync(
|
||||
string packId,
|
||||
EvidencePackExportFormat format,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pack by ID.
|
||||
/// </summary>
|
||||
Task<EvidencePack?> GetAsync(
|
||||
string tenantId,
|
||||
string packId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record SignedEvidencePack
|
||||
{
|
||||
public required EvidencePack Pack { get; init; }
|
||||
public required DsseEnvelope Envelope { get; init; }
|
||||
public required DateTimeOffset SignedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record EvidencePackVerificationResult
|
||||
{
|
||||
public required bool Valid { get; init; }
|
||||
public required string PackDigest { get; init; }
|
||||
public required string SignatureKeyId { get; init; }
|
||||
public ImmutableArray<string> Issues { get; init; }
|
||||
public ImmutableArray<EvidenceResolutionResult> EvidenceResolutions { get; init; }
|
||||
}
|
||||
|
||||
public sealed record EvidenceResolutionResult
|
||||
{
|
||||
public required string EvidenceId { get; init; }
|
||||
public required string Uri { get; init; }
|
||||
public required bool Resolved { get; init; }
|
||||
public required bool DigestMatches { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
public enum EvidencePackExportFormat
|
||||
{
|
||||
Json,
|
||||
SignedJson,
|
||||
Markdown,
|
||||
Pdf,
|
||||
Html
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Create from grounding results
|
||||
- [ ] Create from Run artifacts
|
||||
- [ ] DSSE signing
|
||||
- [ ] Multiple export formats
|
||||
- [x] Verification with evidence resolution
|
||||
|
||||
---
|
||||
|
||||
### EVPK-003: EvidencePackService Implementation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/StellaOps.Evidence.Pack/EvidencePackService.cs` |
|
||||
|
||||
**Key Implementation:**
|
||||
```csharp
|
||||
internal sealed class EvidencePackService : IEvidencePackService
|
||||
{
|
||||
private readonly IEvidencePackStore _store;
|
||||
private readonly IEvidenceResolver _resolver;
|
||||
private readonly IAiAttestationService _attestationService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
|
||||
public async Task<EvidencePack> CreateFromGroundingAsync(
|
||||
GroundingValidationResult grounding,
|
||||
EvidenceSubject subject,
|
||||
EvidencePackContext? context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var packId = $"pack-{_guidGenerator.NewGuid():N}";
|
||||
|
||||
// Extract claims from grounding
|
||||
var claims = new List<EvidenceClaim>();
|
||||
var evidenceItems = new List<EvidenceItem>();
|
||||
|
||||
// Process validated links as evidence
|
||||
foreach (var link in grounding.ValidatedLinks.Where(l => l.IsValid))
|
||||
{
|
||||
var evidenceId = $"ev-{_guidGenerator.NewGuid():N}";
|
||||
|
||||
// Resolve and snapshot the evidence
|
||||
var snapshot = await _resolver.ResolveAndSnapshotAsync(
|
||||
link.Type, link.Path, cancellationToken);
|
||||
|
||||
evidenceItems.Add(new EvidenceItem
|
||||
{
|
||||
EvidenceId = evidenceId,
|
||||
Type = MapLinkTypeToEvidenceType(link.Type),
|
||||
Uri = $"stella://{link.Type}/{link.Path}",
|
||||
Digest = snapshot.Digest,
|
||||
CollectedAt = _timeProvider.GetUtcNow(),
|
||||
Snapshot = snapshot
|
||||
});
|
||||
}
|
||||
|
||||
// Create claims from grounded claims
|
||||
var claimIndex = 0;
|
||||
foreach (var groundedClaim in grounding.GroundedClaims)
|
||||
{
|
||||
var claimId = $"claim-{claimIndex++:D3}";
|
||||
|
||||
// Find evidence near this claim
|
||||
var nearbyEvidence = FindNearbyEvidence(
|
||||
groundedClaim,
|
||||
grounding.ValidatedLinks,
|
||||
evidenceItems);
|
||||
|
||||
claims.Add(new EvidenceClaim
|
||||
{
|
||||
ClaimId = claimId,
|
||||
Text = groundedClaim.Text,
|
||||
Type = DetectClaimType(groundedClaim.Text),
|
||||
Status = ExtractStatus(groundedClaim.Text),
|
||||
Confidence = grounding.GroundingScore,
|
||||
EvidenceIds = nearbyEvidence.Select(e => e.EvidenceId).ToImmutableArray(),
|
||||
Source = "ai"
|
||||
});
|
||||
}
|
||||
|
||||
var pack = new EvidencePack
|
||||
{
|
||||
PackId = packId,
|
||||
Version = "1.0",
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
TenantId = context?.TenantId ?? "unknown",
|
||||
Subject = subject,
|
||||
Claims = claims.ToImmutableArray(),
|
||||
Evidence = evidenceItems.ToImmutableArray(),
|
||||
Context = context
|
||||
};
|
||||
|
||||
await _store.SaveAsync(pack, cancellationToken);
|
||||
return pack;
|
||||
}
|
||||
|
||||
public async Task<SignedEvidencePack> SignAsync(
|
||||
EvidencePack pack,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Create attestation
|
||||
var envelope = await _attestationService.SignAttestationAsync(
|
||||
pack, cancellationToken);
|
||||
|
||||
var signedPack = new SignedEvidencePack
|
||||
{
|
||||
Pack = pack,
|
||||
Envelope = envelope,
|
||||
SignedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
// Store signed version
|
||||
await _store.SaveSignedAsync(signedPack, cancellationToken);
|
||||
|
||||
return signedPack;
|
||||
}
|
||||
|
||||
public async Task<EvidencePackVerificationResult> VerifyAsync(
|
||||
SignedEvidencePack signedPack,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Verify DSSE signature
|
||||
var signatureValid = await _attestationService.VerifyAttestationAsync(
|
||||
signedPack.Envelope, cancellationToken);
|
||||
|
||||
// 2. Verify content digest
|
||||
var computedDigest = signedPack.Pack.ContentDigest;
|
||||
var digestMatches = signedPack.Envelope.PayloadDigest == computedDigest;
|
||||
|
||||
// 3. Resolve and verify each evidence item
|
||||
var evidenceResults = new List<EvidenceResolutionResult>();
|
||||
foreach (var evidence in signedPack.Pack.Evidence)
|
||||
{
|
||||
var resolution = await _resolver.VerifyEvidenceAsync(
|
||||
evidence, cancellationToken);
|
||||
evidenceResults.Add(resolution);
|
||||
}
|
||||
|
||||
var allValid = signatureValid.IsValid
|
||||
&& digestMatches
|
||||
&& evidenceResults.All(r => r.Resolved && r.DigestMatches);
|
||||
|
||||
return new EvidencePackVerificationResult
|
||||
{
|
||||
Valid = allValid,
|
||||
PackDigest = computedDigest,
|
||||
SignatureKeyId = signedPack.Envelope.Signatures[0].KeyId,
|
||||
Issues = CollectIssues(signatureValid, digestMatches, evidenceResults),
|
||||
EvidenceResolutions = evidenceResults.ToImmutableArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Creates packs from grounding results
|
||||
- [ ] Resolves and snapshots evidence
|
||||
- [ ] DSSE signing via attestation service
|
||||
- [x] Full verification with evidence resolution
|
||||
- [x] Deterministic content digest
|
||||
|
||||
---
|
||||
|
||||
### EVPK-004: Evidence Resolver
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/StellaOps.Evidence.Pack/EvidenceResolver.cs` |
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IEvidenceResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a stella:// URI and creates a snapshot.
|
||||
/// </summary>
|
||||
Task<EvidenceSnapshot> ResolveAndSnapshotAsync(
|
||||
string type,
|
||||
string path,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that evidence still matches its recorded digest.
|
||||
/// </summary>
|
||||
Task<EvidenceResolutionResult> VerifyEvidenceAsync(
|
||||
EvidenceItem evidence,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation with type-specific resolvers:**
|
||||
```csharp
|
||||
internal sealed class EvidenceResolver : IEvidenceResolver
|
||||
{
|
||||
private readonly ImmutableDictionary<string, ITypeResolver> _resolvers;
|
||||
|
||||
public EvidenceResolver(
|
||||
ISbomService sbomService,
|
||||
IReachabilityIndex reachabilityIndex,
|
||||
IVexConsensusEngine vexEngine,
|
||||
IRuntimeFactsService runtimeService,
|
||||
IOpsMemoryStore opsMemoryStore)
|
||||
{
|
||||
_resolvers = new Dictionary<string, ITypeResolver>
|
||||
{
|
||||
["sbom"] = new SbomResolver(sbomService),
|
||||
["reach"] = new ReachabilityResolver(reachabilityIndex),
|
||||
["vex"] = new VexResolver(vexEngine),
|
||||
["runtime"] = new RuntimeResolver(runtimeService),
|
||||
["ops-mem"] = new OpsMemoryResolver(opsMemoryStore)
|
||||
}.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
public async Task<EvidenceSnapshot> ResolveAndSnapshotAsync(
|
||||
string type,
|
||||
string path,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_resolvers.TryGetValue(type, out var resolver))
|
||||
{
|
||||
throw new UnsupportedEvidenceTypeException(type);
|
||||
}
|
||||
|
||||
return await resolver.ResolveAsync(path, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Resolvers for: sbom, reach, vex, runtime, ops-mem
|
||||
- [ ] Snapshots capture relevant data
|
||||
- [ ] Digest computed for verification
|
||||
- [x] Handles missing evidence gracefully
|
||||
|
||||
---
|
||||
|
||||
### EVPK-005: Evidence Pack Storage
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/StellaOps.Evidence.Pack/IEvidencePackStore.cs` |
|
||||
|
||||
**PostgreSQL Schema:**
|
||||
```sql
|
||||
CREATE TABLE evidence.packs (
|
||||
pack_id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
content_digest TEXT NOT NULL,
|
||||
subject JSONB NOT NULL,
|
||||
claims JSONB NOT NULL,
|
||||
evidence JSONB NOT NULL,
|
||||
context JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
signed_at TIMESTAMPTZ,
|
||||
envelope JSONB -- DSSE envelope if signed
|
||||
);
|
||||
|
||||
CREATE INDEX idx_packs_tenant ON evidence.packs(tenant_id);
|
||||
CREATE INDEX idx_packs_digest ON evidence.packs(content_digest);
|
||||
CREATE INDEX idx_packs_subject ON evidence.packs USING gin(subject);
|
||||
|
||||
-- Link evidence packs to runs
|
||||
CREATE TABLE evidence.pack_run_links (
|
||||
pack_id TEXT NOT NULL REFERENCES evidence.packs(pack_id),
|
||||
run_id TEXT NOT NULL,
|
||||
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (pack_id, run_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_pack_links_run ON evidence.pack_run_links(run_id);
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] PostgreSQL store implementation
|
||||
- [ ] GIN index for subject queries
|
||||
- [ ] Link table for Run associations
|
||||
- [ ] Supports signed and unsigned packs
|
||||
|
||||
---
|
||||
|
||||
### EVPK-006: Chat Integration
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI/Chat/EvidencePackChatIntegration.cs` |
|
||||
|
||||
**Auto-create Evidence Pack from AI turn:**
|
||||
```csharp
|
||||
// In ConversationService.AddTurnAsync() after grounding validation
|
||||
if (groundingResult.IsAcceptable && _options.AutoCreateEvidencePacks)
|
||||
{
|
||||
var pack = await _evidencePackService.CreateFromGroundingAsync(
|
||||
groundingResult,
|
||||
new EvidenceSubject
|
||||
{
|
||||
Type = EvidenceSubjectType.Finding,
|
||||
FindingId = context.FindingId,
|
||||
CveId = context.CurrentCveId,
|
||||
Component = context.CurrentComponent
|
||||
},
|
||||
new EvidencePackContext
|
||||
{
|
||||
RunId = context.RunId,
|
||||
ConversationId = conversation.ConversationId,
|
||||
UserId = context.UserId,
|
||||
GeneratedBy = "AdvisoryAI"
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
// Attach to Run as artifact
|
||||
if (context.RunId is not null)
|
||||
{
|
||||
await _runService.AttachArtifactAsync(
|
||||
context.RunId,
|
||||
new RunArtifact
|
||||
{
|
||||
ArtifactId = pack.PackId,
|
||||
Type = RunArtifactType.EvidencePack,
|
||||
Name = $"Evidence Pack - {context.CurrentCveId}",
|
||||
ContentDigest = pack.ContentDigest,
|
||||
Uri = $"stella://evidence-pack/{pack.PackId}",
|
||||
CreatedAt = pack.CreatedAt
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Object Link Support:**
|
||||
```csharp
|
||||
// Add to GroundingValidator link types
|
||||
// [evidence-pack:pack-xyz789] → Links to Evidence Pack
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Auto-create on well-grounded responses
|
||||
- [ ] Attach to Run as artifact
|
||||
- [ ] Support object link format
|
||||
- [ ] Configurable enable/disable
|
||||
|
||||
---
|
||||
|
||||
### EVPK-007: Export Service
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/StellaOps.Evidence.Pack/EvidencePackService.cs` |
|
||||
|
||||
**Export Formats:**
|
||||
|
||||
1. **JSON** - Raw pack structure
|
||||
2. **SignedJSON** - Pack + DSSE envelope
|
||||
3. **Markdown** - Human-readable report
|
||||
4. **HTML** - Styled report with evidence links
|
||||
5. **PDF** - Printable report
|
||||
|
||||
**Markdown Template:**
|
||||
```markdown
|
||||
# Evidence Pack: {{packId}}
|
||||
|
||||
**Created:** {{createdAt}}
|
||||
**Subject:** {{subject.type}} - {{subject.cveId}}
|
||||
|
||||
## Claims
|
||||
|
||||
{{#each claims}}
|
||||
### {{claimId}}: {{text}}
|
||||
|
||||
- **Type:** {{type}}
|
||||
- **Status:** {{status}}
|
||||
- **Confidence:** {{confidence}}%
|
||||
- **Evidence:** {{#each evidenceIds}}[{{.}}] {{/each}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Evidence
|
||||
|
||||
{{#each evidence}}
|
||||
### {{evidenceId}}: {{type}}
|
||||
|
||||
- **URI:** `{{uri}}`
|
||||
- **Digest:** `{{digest}}`
|
||||
- **Collected:** {{collectedAt}}
|
||||
|
||||
**Snapshot:**
|
||||
```json
|
||||
{{snapshot}}
|
||||
```
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Verification
|
||||
|
||||
**Content Digest:** `{{contentDigest}}`
|
||||
**Signature:** {{#if signatures}}Valid{{else}}Unsigned{{/if}}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] All 5 export formats implemented
|
||||
- [ ] Markdown readable by humans
|
||||
- [ ] PDF suitable for compliance
|
||||
- [ ] Signed exports include envelope
|
||||
|
||||
---
|
||||
|
||||
### EVPK-008: Evidence Pack Viewer UI
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/Web/StellaOps.Web/src/app/features/evidence-pack/` |
|
||||
|
||||
**Components:**
|
||||
```typescript
|
||||
// evidence-pack-viewer.component.ts
|
||||
@Component({
|
||||
selector: 'stella-evidence-pack-viewer',
|
||||
template: `
|
||||
<div class="evidence-pack">
|
||||
<div class="pack-header">
|
||||
<h2>Evidence Pack</h2>
|
||||
<span class="pack-id">{{ pack.packId }}</span>
|
||||
<stella-verification-badge [verified]="verification?.valid" />
|
||||
</div>
|
||||
|
||||
<div class="pack-subject">
|
||||
<h3>Subject</h3>
|
||||
<stella-subject-card [subject]="pack.subject" />
|
||||
</div>
|
||||
|
||||
<div class="pack-claims">
|
||||
<h3>Claims ({{ pack.claims.length }})</h3>
|
||||
<div *ngFor="let claim of pack.claims" class="claim-card">
|
||||
<div class="claim-text">{{ claim.text }}</div>
|
||||
<div class="claim-meta">
|
||||
<stella-confidence-badge [score]="claim.confidence" />
|
||||
<span class="claim-type">{{ claim.type }}</span>
|
||||
</div>
|
||||
<div class="claim-evidence">
|
||||
<span *ngFor="let evidenceId of claim.evidenceIds"
|
||||
class="evidence-chip"
|
||||
(click)="scrollToEvidence(evidenceId)">
|
||||
{{ evidenceId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pack-evidence">
|
||||
<h3>Evidence ({{ pack.evidence.length }})</h3>
|
||||
<div *ngFor="let evidence of pack.evidence"
|
||||
[id]="evidence.evidenceId"
|
||||
class="evidence-card">
|
||||
<stella-evidence-item [evidence]="evidence"
|
||||
[resolution]="getResolution(evidence.evidenceId)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pack-actions">
|
||||
<button mat-button (click)="export('json')">Export JSON</button>
|
||||
<button mat-button (click)="export('pdf')">Export PDF</button>
|
||||
<button mat-raised-button color="primary"
|
||||
*ngIf="!pack.envelope"
|
||||
(click)="sign()">Sign Pack</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class EvidencePackViewerComponent {
|
||||
@Input() pack: EvidencePack;
|
||||
@Input() verification?: EvidencePackVerificationResult;
|
||||
}
|
||||
```
|
||||
|
||||
**Additional Components:**
|
||||
- `evidence-item.component.ts` - Individual evidence display
|
||||
- `verification-badge.component.ts` - Verification status
|
||||
- `confidence-badge.component.ts` - Confidence visualization
|
||||
- `subject-card.component.ts` - Subject display
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Claims linked to evidence
|
||||
- [x] Evidence expandable with snapshot
|
||||
- [x] Verification status displayed
|
||||
- [x] Export buttons functional
|
||||
- [x] Responsive design
|
||||
|
||||
---
|
||||
|
||||
### EVPK-009: Unit Tests
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/__Libraries/__Tests/StellaOps.Evidence.Pack.Tests/` |
|
||||
|
||||
**Test Classes:**
|
||||
1. `EvidencePackServiceTests`
|
||||
- [ ] Create from grounding
|
||||
- [ ] Add evidence
|
||||
- [ ] Sign pack
|
||||
- [ ] Verify pack
|
||||
|
||||
2. `EvidenceResolverTests`
|
||||
- [ ] Resolve SBOM
|
||||
- [ ] Resolve reachability
|
||||
- [ ] Resolve VEX
|
||||
- [ ] Handle missing evidence
|
||||
|
||||
3. `ExportServiceTests`
|
||||
- [ ] Export JSON
|
||||
- [ ] Export Markdown
|
||||
- [ ] Content digest stability
|
||||
|
||||
---
|
||||
|
||||
### EVPK-010: API Endpoints
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | DONE |
|
||||
| File | `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/EvidencePackEndpoints.cs` |
|
||||
|
||||
**Endpoints:**
|
||||
```http
|
||||
POST /api/v1/evidence-packs
|
||||
Body: { subject, claims, evidence }
|
||||
→ Creates Evidence Pack
|
||||
|
||||
GET /api/v1/evidence-packs/{packId}
|
||||
→ Returns Evidence Pack
|
||||
|
||||
POST /api/v1/evidence-packs/{packId}/sign
|
||||
→ Signs pack, returns SignedEvidencePack
|
||||
|
||||
POST /api/v1/evidence-packs/{packId}/verify
|
||||
→ Verifies pack and evidence
|
||||
|
||||
GET /api/v1/evidence-packs/{packId}/export
|
||||
Query: format=json|markdown|pdf
|
||||
→ Returns exported pack
|
||||
|
||||
GET /api/v1/runs/{runId}/evidence-packs
|
||||
→ Lists Evidence Packs for a Run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
EvidencePack:
|
||||
AutoCreate:
|
||||
Enabled: true
|
||||
MinGroundingScore: 0.7
|
||||
|
||||
Signing:
|
||||
KeyId: "evidence-pack-signing-key"
|
||||
AutoSign: false # Require explicit signing
|
||||
|
||||
Export:
|
||||
PdfEnabled: true
|
||||
PdfTemplate: "/etc/stellaops/templates/evidence-pack.html"
|
||||
|
||||
Retention:
|
||||
Days: 365
|
||||
SignedDays: 2555 # 7 years for signed packs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions & Risks
|
||||
|
||||
| Decision/Risk | Notes |
|
||||
|---------------|-------|
|
||||
| Evidence snapshot size | May be large; compress in storage |
|
||||
| Snapshot staleness | Evidence may change; capture timestamp |
|
||||
| PDF generation | Requires headless browser; may be slow |
|
||||
| Signature key management | Need rotation strategy |
|
||||
|
||||
---
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Task | Action |
|
||||
|------|------|--------|
|
||||
| 09-Jan-2026 | Sprint | Created sprint definition file |
|
||||
| 10-Jan-2026 | EVPK-001 | Created Evidence Pack models |
|
||||
| 10-Jan-2026 | EVPK-002 | Created IEvidencePackService interface |
|
||||
| 10-Jan-2026 | EVPK-003 | Implemented EvidencePackService |
|
||||
| 10-Jan-2026 | EVPK-004 | Implemented EvidenceResolver with pluggable type resolvers |
|
||||
| 10-Jan-2026 | EVPK-005 | Created InMemoryEvidencePackStore |
|
||||
| 10-Jan-2026 | EVPK-006 | Created EvidencePackChatIntegration |
|
||||
| 10-Jan-2026 | EVPK-007 | Export service implemented (JSON, Markdown, HTML, SignedJSON) |
|
||||
| 10-Jan-2026 | EVPK-009 | Unit tests created (31 tests passing) |
|
||||
| 10-Jan-2026 | EVPK-010 | API endpoints created in AdvisoryAI WebService |
|
||||
| 10-Jan-2026 | EVPK-008 | Created Evidence Pack Viewer UI components (viewer, list, index) |
|
||||
| 10-Jan-2026 | EVPK-008 | Registered Evidence Pack API client in app.config.ts |
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [x] All 10 tasks complete
|
||||
- [x] Evidence Packs created from AI responses
|
||||
- [x] DSSE signing works
|
||||
- [x] Verification resolves all evidence
|
||||
- [x] Export in all formats
|
||||
- [x] All tests passing
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 09-Jan-2026_
|
||||
Reference in New Issue
Block a user