14 KiB
Product Advisory: Deterministic Resolver Architecture
Date: 2025-12-24 Author: Architecture Guild Status: Approved for Implementation Sprint Epoch: 9100 Priority: P0 (Critical Path for Audit Compliance)
Executive Summary
This advisory defines the architecture for a unified deterministic resolver that evaluates vulnerability graphs using only declared evidence edges and produces cryptographically verifiable, reproducible verdicts. The resolver guarantees: same inputs → same traversal order → same per-node inputs → same verdicts → same output digest.
This is a foundational capability for:
- Auditor-friendly verification ("same inputs → same verdicts")
- Offline replay by vendors/distributors
- Single-digest comparison across runs
- CI/CD gate assertions
Problem Statement
Current implementation has strong building blocks but lacks unified orchestration:
| Component | Location | Gap |
|---|---|---|
| Topological Sort | DeterministicGraphOrderer |
Traversal only; no verdict output |
| Lattice Evaluation | TrustLatticeEngine, PolicyEvaluator |
No traversal sequence in output |
| Content Addressing | ContentAddressedIdGenerator |
Node IDs only; no edge IDs |
| Purity Analysis | ProhibitedPatternAnalyzer |
Static only; no runtime enforcement |
| Digest Computation | Various | No unified "run digest" |
Result: Cannot produce a single ResolutionResult that captures:
- The exact traversal sequence used
- Per-node verdicts with individual digests
- A composite
FinalDigestfor verification
Solution Architecture
Core Data Model
// New: src/__Libraries/StellaOps.Resolver/
/// <summary>
/// Content-addressed node identifier.
/// Format: sha256(kind || ":" || normalized-key)
/// </summary>
public sealed record NodeId(string Value) : IComparable<NodeId>
{
public int CompareTo(NodeId? other) =>
string.Compare(Value, other?.Value, StringComparison.Ordinal);
public static NodeId From(string kind, string normalizedKey) =>
new(HexSha256($"{kind}:{normalizedKey}"));
}
/// <summary>
/// Content-addressed edge identifier.
/// Format: sha256(srcID || "->" || edgeKind || "->" || dstID)
/// </summary>
public sealed record EdgeId(string Value) : IComparable<EdgeId>
{
public static EdgeId From(NodeId src, string kind, NodeId dst) =>
new(HexSha256($"{src.Value}->{kind}->{dst.Value}"));
}
/// <summary>
/// Graph edge with content-addressed identity and optional cycle-cut marker.
/// </summary>
public sealed record Edge(
EdgeId Id,
NodeId Src,
string Kind,
NodeId Dst,
JsonElement Attrs,
bool IsCycleCut = false);
/// <summary>
/// Graph node with content-addressed identity.
/// </summary>
public sealed record Node(
NodeId Id,
string Kind,
JsonElement Attrs);
/// <summary>
/// Immutable policy with content-addressed identity.
/// </summary>
public sealed record Policy(
string Version,
JsonElement Rules,
string ConstantsDigest);
/// <summary>
/// Individual verdict with its own content-addressed digest.
/// </summary>
public sealed record Verdict(
NodeId Node,
string Status,
JsonElement Evidence,
string VerdictDigest);
/// <summary>
/// Complete resolution result with all digests for verification.
/// </summary>
public sealed record ResolutionResult(
ImmutableArray<NodeId> TraversalSequence,
ImmutableArray<Verdict> Verdicts,
string GraphDigest,
string PolicyDigest,
string FinalDigest);
Resolver Algorithm
normalize(graph); // IDs, fields, ordering
validate(graph); // No implicit data, cycles declared
assert no-ambient-inputs(); // Runtime purity check
order = topoSortWithLexTieBreak(graph); // Uses only evidence edges
verdicts = []
for node in order:
inbound = gatherInboundEvidence(node) // Pure, no IO
verdict = Evaluate(node, inbound, policy) // Pure, no IO
verdicts.append(canonicalize(verdict) with VerdictDigest)
result = {
traversal: order,
verdicts: verdicts,
graphDigest: digest(canonical(graph)),
policyDigest: digest(canonical(policy))
}
result.finalDigest = digest(canonical(result))
return result
Key Invariants
- Canonical IDs:
NodeId = sha256(kind:normalized-key),EdgeId = sha256(src->kind->dst) - Canonical Serialization: Alphabetical keys, sorted arrays, fixed numerics, ISO-8601 Z timestamps
- Fixed Traversal: Kahn's algorithm with lexicographic tie-breaker (
SortedSet<NodeId>) - Explicit Cycles: Cycles require
IsCycleCut = trueedge; unmarked cycles → invalid graph - Evidence-Only Evaluation: Node verdict = f(node, inbound_edges, policy) — no ambient IO
- Stable Outputs:
(TraversalSequence, Verdicts[], GraphDigest, PolicyDigest, FinalDigest)
Gap Analysis vs Current Implementation
Already Implemented (No Work Needed)
| Requirement | Implementation | Location |
|---|---|---|
| Canonical JSON | RFC 8785 compliant | CanonicalJsonSerializer, Rfc8785JsonCanonicalizer |
| Alphabetical key sorting | StableDictionaryConverter |
StellaOps.Canonicalization |
| ISO-8601 UTC timestamps | Iso8601DateTimeConverter |
StellaOps.Canonicalization |
| Topological sort with lex tie-break | SortedSet<string> |
DeterministicGraphOrderer:134 |
| K4 Four-Valued Lattice | Complete | K4Lattice.cs |
| VEX Lattice Merging | With traces | OpenVexStatementMerger |
| Merkle Tree Proofs | SHA256-based | DeterministicMerkleTreeBuilder |
| Static Purity Analysis | 16+ patterns | ProhibitedPatternAnalyzer |
| Injected Timestamp | Context-based | PolicyEvaluationContext.Now |
| Graph Content Hash | SHA256 | DeterministicGraphOrderer.ComputeCanonicalHash() |
Gaps to Implement
| Gap | Priority | Sprint | Description |
|---|---|---|---|
| Unified Resolver | P0 | 9100.0001.0001 | Single entry point producing ResolutionResult |
| Cycle-Cut Edges | P1 | 9100.0001.0002 | IsCycleCut edge property; validation |
| EdgeId | P2 | 9100.0001.0003 | Content-addressed edge identifiers |
| FinalDigest | P1 | 9100.0002.0001 | Composite run-level digest |
| Per-Node VerdictDigest | P2 | 9100.0002.0002 | Individual verdict digests |
| Runtime Purity | P1 | 9100.0003.0001 | Runtime enforcement beyond static analysis |
| Graph Validation + NFC | P3 | 9100.0003.0002 | Pre-traversal validation; NFC normalization |
Implementation Phases
Phase 1: Core Resolver Package (Sprint 9100.0001.*)
Goal: Create StellaOps.Resolver library with unified resolver pattern.
-
Sprint 9100.0001.0001 — Core Resolver
DeterministicResolverclassResolutionResultrecord- Integration with existing
DeterministicGraphOrderer - Integration with existing
TrustLatticeEngine
-
Sprint 9100.0001.0002 — Cycle-Cut Edges
IsCycleCutproperty on edges- Cycle validation (unmarked cycles → error)
- Graph validation before traversal
-
Sprint 9100.0001.0003 — Content-Addressed EdgeId
EdgeIdrecord- Edge content addressing
- Merkle tree inclusion of edges
Phase 2: Digest Infrastructure (Sprint 9100.0002.*)
Goal: Implement comprehensive digest chain for verification.
-
Sprint 9100.0002.0001 — FinalDigest
- Composite digest computation
sha256(canonical({graphDigest, policyDigest, verdicts[]}))- Integration with attestation system
-
Sprint 9100.0002.0002 — Per-Node VerdictDigest
- Verdict-level content addressing
- Drill-down debugging ("which node changed?")
- Delta detection between runs
Phase 3: Purity & Validation (Sprint 9100.0003.*)
Goal: Harden determinism guarantees with runtime enforcement.
-
Sprint 9100.0003.0001 — Runtime Purity Enforcement
- Runtime guards beyond static analysis
- Dependency injection shims for ambient services
- Test harness for purity verification
-
Sprint 9100.0003.0002 — Graph Validation & NFC
- Pre-traversal validation ("no implicit data")
- Unicode NFC normalization for string fields
- Evidence completeness assertions
Testing Requirements
Mandatory Test Types
- Replay Test: Same input twice → identical
FinalDigest - Permutation Test: Shuffle input nodes/edges → identical outputs
- Cycle Test: Introduce cycle → fail unless
IsCycleCutedge present - Ambience Test: Forbid calls to time/env/network during evaluation
- Serialization Test: Canonicalization changes → predictable digest changes
Property-Based Tests
// Idempotency
[Property]
public void Resolver_SameInput_SameOutput(Graph graph)
{
var result1 = resolver.Run(graph);
var result2 = resolver.Run(graph);
Assert.Equal(result1.FinalDigest, result2.FinalDigest);
}
// Order Independence
[Property]
public void Resolver_ShuffledInputs_SameOutput(Graph graph, int seed)
{
var shuffled = ShuffleNodesAndEdges(graph, seed);
var result1 = resolver.Run(graph);
var result2 = resolver.Run(shuffled);
Assert.Equal(result1.FinalDigest, result2.FinalDigest);
}
// Cycle Detection
[Property]
public void Resolver_UnmarkedCycle_Throws(Graph graphWithCycle)
{
Assume.That(!graphWithCycle.Edges.Any(e => e.IsCycleCut));
Assert.Throws<InvalidGraphException>(() => resolver.Run(graphWithCycle));
}
Integration Points
Existing Components to Integrate
| Component | Integration |
|---|---|
DeterministicGraphOrderer |
Use for traversal order computation |
TrustLatticeEngine |
Use for K4 lattice evaluation |
CanonicalJsonSerializer |
Use for all serialization |
ContentAddressedIdGenerator |
Extend for EdgeId and VerdictId |
ProhibitedPatternAnalyzer |
Combine with runtime guards |
PolicyEvaluationContext |
Use for injected inputs |
New Modules
| Module | Purpose |
|---|---|
StellaOps.Resolver |
Core resolver library |
StellaOps.Resolver.Testing |
Test fixtures and harnesses |
StellaOps.Resolver.Attestation |
Integration with proof chain |
Success Criteria
- Unified API: Single
resolver.Run(graph)produces completeResolutionResult - Deterministic: 100% pass rate on replay/permutation/cycle tests
- Auditable:
FinalDigestenables single-value verification - Performant: No more than 10% overhead vs current fragmented approach
- Documented: Full API documentation and integration guide
Appendix: C# Reference Implementation
public sealed class DeterministicResolver
{
private readonly Policy _policy;
private readonly string _policyDigest;
private readonly DeterministicGraphOrderer _orderer;
private readonly TrustLatticeEngine _lattice;
private readonly CanonicalJsonSerializer _serializer;
public DeterministicResolver(
Policy policy,
DeterministicGraphOrderer orderer,
TrustLatticeEngine lattice,
CanonicalJsonSerializer serializer)
{
_policy = Canon(policy);
_policyDigest = HexSha256(serializer.SerializeToBytes(_policy));
_orderer = orderer;
_lattice = lattice;
_serializer = serializer;
}
public ResolutionResult Run(EvidenceGraph graph)
{
// 1. Canonicalize and validate
var canonical = _orderer.Canonicalize(graph);
ValidateCycles(canonical);
EnsureNoAmbientInputs();
// 2. Traverse in deterministic order
var traversal = canonical.Nodes
.Select(n => NodeId.Parse(n.Id))
.ToImmutableArray();
// 3. Evaluate each node purely
var verdicts = new List<Verdict>(traversal.Length);
foreach (var nodeId in traversal)
{
var node = GetNode(canonical, nodeId);
var inbound = GatherInboundEvidence(canonical, nodeId);
var verdict = EvaluatePure(node, inbound, _policy);
var verdictBytes = _serializer.SerializeToBytes(verdict);
verdicts.Add(verdict with { VerdictDigest = HexSha256(verdictBytes) });
}
// 4. Compute digests
var verdictsArray = verdicts.ToImmutableArray();
var resultWithoutFinal = new ResolutionResult(
traversal,
verdictsArray,
canonical.ContentHash,
_policyDigest,
"");
var finalBytes = _serializer.SerializeToBytes(resultWithoutFinal);
return resultWithoutFinal with { FinalDigest = HexSha256(finalBytes) };
}
private void ValidateCycles(CanonicalGraph graph)
{
// Detect cycles; require IsCycleCut for each
var cycles = DetectCycles(graph);
var unmarked = cycles.Where(c => !c.CutEdge.IsCycleCut).ToList();
if (unmarked.Any())
{
throw new InvalidGraphException(
$"Graph contains {unmarked.Count} unmarked cycle(s). " +
"Add IsCycleCut=true to break cycles.");
}
}
private Verdict EvaluatePure(Node node, InboundEvidence evidence, Policy policy)
{
// Pure evaluation: no IO, no ambient state
return _lattice.Evaluate(node, evidence, policy);
}
}
Related Documents
docs/modules/attestor/proof-chain-specification.mddocs/modules/policy/architecture.mddocs/testing/testing-strategy-models.mdsrc/Policy/__Libraries/StellaOps.Policy/TrustLattice/K4Lattice.cssrc/Scanner/__Libraries/StellaOps.Scanner.Reachability/Ordering/DeterministicGraphOrderer.cs
Approval
| Role | Name | Date | Decision |
|---|---|---|---|
| Architecture Lead | — | 2025-12-24 | Approved |
| Policy Guild Lead | — | 2025-12-24 | Approved |
| Scanner Guild Lead | — | 2025-12-24 | Approved |