finish 9th jan sprints

This commit is contained in:
master
2026-01-10 21:08:39 +02:00
parent 17d0631b8e
commit a3b2f30a11
9 changed files with 175 additions and 198 deletions

View File

@@ -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_

View File

@@ -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_

View File

@@ -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_

View File

@@ -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_

View File

@@ -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_