# Step 28: Agent Process Guidelines ## Overview This document provides comprehensive guidelines for AI agents (Claude, Copilot, etc.) implementing the Stella Router. It establishes conventions, patterns, and decision frameworks to ensure consistent, high-quality implementations across all phases. ## Goals 1. Define clear coding standards and patterns for Router implementation 2. Establish decision frameworks for common scenarios 3. Provide checklists for implementation quality 4. Document testing requirements and coverage expectations 5. Define commit and PR conventions ## Implementation Standards ### Code Organization ``` src/Router/ ├── StellaOps.Router.Core/ # Core abstractions and contracts │ ├── Abstractions/ # Interfaces │ ├── Configuration/ # Config models │ ├── Extensions/ # Extension methods │ └── Primitives/ # Value types │ ├── StellaOps.Router.Gateway/ # Gateway implementation │ ├── Routing/ # Route matching │ ├── Handlers/ # Route handlers │ ├── Pipeline/ # Request pipeline │ └── Middleware/ # Gateway middleware │ ├── StellaOps.Router.Transport/ # Transport implementations │ ├── InMemory/ # In-process transport │ ├── Tcp/ # TCP transport │ └── Tls/ # TLS transport │ ├── StellaOps.Router.Microservice/ # Microservice SDK │ ├── Hosting/ # Host builder │ ├── Endpoints/ # Endpoint handling │ └── Context/ # Request context │ ├── StellaOps.Router.Security/ # Security components │ ├── Jwt/ # JWT validation │ ├── Claims/ # Claim hydration │ └── RateLimiting/ # Rate limiting │ └── StellaOps.Router.Observability/ # Observability ├── Logging/ # Structured logging ├── Metrics/ # Prometheus metrics └── Tracing/ # OpenTelemetry tracing ``` ### Naming Conventions | Element | Convention | Example | |---------|------------|---------| | Interfaces | `I` prefix, noun/adjective | `IRouteHandler`, `IConnectable` | | Classes | PascalCase, noun | `JwtValidator`, `RouteTable` | | Async methods | `Async` suffix | `ValidateTokenAsync`, `SendAsync` | | Config classes | `Options` or `Configuration` suffix | `JwtValidationOptions` | | Event handlers | `On` prefix | `OnConnectionEstablished` | | Factory methods | `Create` prefix | `CreateHandler`, `CreateConnection` | | Boolean properties | `Is`/`Has`/`Can` prefix | `IsValid`, `HasExpired`, `CanRetry` | ### File Structure ```csharp // File: StellaOps.Router.Core/Abstractions/IRouteHandler.cs // 1. License header (if required) // 2. Using statements (sorted: System, Microsoft, Third-party, Internal) using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using StellaOps.Router.Core.Configuration; // 3. Namespace (one per file, matches folder structure) namespace StellaOps.Router.Core.Abstractions; // 4. XML documentation /// /// Handles requests for a specific route type. /// /// /// Implementations must be thread-safe and support concurrent request handling. /// public interface IRouteHandler { // 5. Interface members (properties, then methods) /// /// Gets the handler type identifier. /// string HandlerType { get; } /// /// Determines if this handler can process the given route. /// bool CanHandle(RouteConfiguration route); /// /// Processes an incoming request. /// Task HandleAsync( RequestPayload request, RouteConfiguration route, CancellationToken cancellationToken = default); } ``` ### Error Handling Patterns ```csharp // Pattern 1: Result types for expected failures public readonly struct Result { public T? Value { get; } public Error? Error { get; } public bool IsSuccess => Error == null; private Result(T? value, Error? error) { Value = value; Error = error; } public static Result Success(T value) => new(value, null); public static Result Failure(Error error) => new(default, error); public Result Map(Func map) => IsSuccess ? Result.Success(map(Value!)) : Result.Failure(Error!); public async Task> MapAsync(Func> map) => IsSuccess ? Result.Success(await map(Value!)) : Result.Failure(Error!); } public record Error(string Code, string Message, Exception? Inner = null); // Usage public async Task> ValidateTokenAsync(string token) { try { var claims = await _validator.ValidateAsync(token); return Result.Success(claims); } catch (SecurityTokenExpiredException ex) { return Result.Failure(new Error("TOKEN_EXPIRED", "JWT has expired", ex)); } catch (SecurityTokenInvalidSignatureException ex) { return Result.Failure(new Error("INVALID_SIGNATURE", "JWT signature invalid", ex)); } } // Pattern 2: Exceptions for unexpected failures public class RouterException : Exception { public string ErrorCode { get; } public int StatusCode { get; } public RouterException(string errorCode, string message, int statusCode = 500) : base(message) { ErrorCode = errorCode; StatusCode = statusCode; } } public class ConfigurationException : RouterException { public ConfigurationException(string message) : base("CONFIG_ERROR", message, 500) { } } public class TransportException : RouterException { public TransportException(string message, Exception? inner = null) : base("TRANSPORT_ERROR", message, 503) { } } ``` ### Async Patterns ```csharp // Pattern 1: CancellationToken propagation public async Task HandleAsync( RequestPayload request, CancellationToken cancellationToken = default) { // Always check at start of long operations cancellationToken.ThrowIfCancellationRequested(); // Propagate to all async calls var validated = await _validator.ValidateAsync(request, cancellationToken); var enriched = await _enricher.EnrichAsync(validated, cancellationToken); var response = await _handler.ProcessAsync(enriched, cancellationToken); return response; } // Pattern 2: Timeout handling public async Task WithTimeoutAsync( Func> operation, TimeSpan timeout, CancellationToken cancellationToken = default) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(timeout); try { return await operation(cts.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException($"Operation timed out after {timeout}"); } } // Pattern 3: Fire-and-forget with logging public void FireAndForget(Func operation, ILogger logger, string operationName) { _ = Task.Run(async () => { try { await operation(); } catch (Exception ex) { logger.LogError(ex, "Fire-and-forget operation {Operation} failed", operationName); } }); } ``` ### Dependency Injection Patterns ```csharp // Pattern 1: Constructor injection with validation public class JwtValidator : IJwtValidator { private readonly JwtValidationOptions _options; private readonly IKeyProvider _keyProvider; private readonly ILogger _logger; public JwtValidator( IOptions options, IKeyProvider keyProvider, ILogger logger) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _keyProvider = keyProvider ?? throw new ArgumentNullException(nameof(keyProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); ValidateOptions(_options); } private static void ValidateOptions(JwtValidationOptions options) { if (string.IsNullOrEmpty(options.Issuer)) throw new ConfigurationException("JWT issuer is required"); if (options.ClockSkew < TimeSpan.Zero) throw new ConfigurationException("Clock skew cannot be negative"); } } // Pattern 2: Factory registration for complex objects public static class ServiceCollectionExtensions { public static IServiceCollection AddStellaRouter( this IServiceCollection services, Action configure) { services.Configure(configure); // Core services services.AddSingleton(); services.AddSingleton(); // Keyed services for handlers services.AddKeyedSingleton("microservice"); services.AddKeyedSingleton("graphql"); services.AddKeyedSingleton("proxy"); // Factory for route handler resolution services.AddSingleton(sp => new RouteHandlerFactory( sp.GetServices().ToDictionary(h => h.HandlerType))); return services; } } // Pattern 3: Scoped services for request context public static class RequestScopeExtensions { public static IServiceCollection AddRequestScope(this IServiceCollection services) { services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService().User); services.AddScoped(sp => sp.GetRequiredService().CorrelationId); return services; } } ``` ## Decision Framework ### When to Create New Types vs. Reuse | Scenario | Decision | Rationale | |----------|----------|-----------| | Similar data, different context | Create new type | Type safety, clear intent | | Same data, same context | Reuse type | DRY, reduce cognitive load | | Third-party type | Create wrapper | Abstraction, testability | | Config vs. runtime | Separate types | Immutability guarantees | ```csharp // Example: Separate types for config vs runtime public record RouteConfiguration( string Path, string Method, string HandlerType, Dictionary Metadata); public class CompiledRoute { public RouteConfiguration Config { get; } public Regex PathPattern { get; } public IRouteHandler Handler { get; } // Runtime-computed fields } ``` ### When to Use Interfaces vs. Abstract Classes | Use Interface | Use Abstract Class | |---------------|-------------------| | Multiple inheritance needed | Shared implementation | | Contract-only definition | Template method pattern | | Third-party implementation | Internal hierarchy only | | Mocking/testing priority | Code reuse priority | ### Logging Level Guidelines | Level | When to Use | Example | |-------|-------------|---------| | `Trace` | Internal flow details | `"Route matching attempt for {Path}"` | | `Debug` | Diagnostic information | `"Cache hit for key {Key}"` | | `Information` | Significant events | `"Request completed: {Method} {Path} → {Status}"` | | `Warning` | Recoverable issues | `"Rate limit approaching: {Current}/{Max}"` | | `Error` | Failures requiring attention | `"Failed to connect to Authority: {Error}"` | | `Critical` | System-wide failures | `"Configuration invalid, router cannot start"` | ```csharp // Structured logging patterns _logger.LogInformation( "Request processed: {Method} {Path} → {StatusCode} in {ElapsedMs}ms", request.Method, request.Path, response.StatusCode, stopwatch.ElapsedMilliseconds); // Use LoggerMessage for high-performance paths private static readonly Action LogRequestComplete = LoggerMessage.Define( LogLevel.Information, new EventId(1001, "RequestComplete"), "Request processed: {Method} {Path} → {StatusCode} in {ElapsedMs}ms"); // Usage LogRequestComplete(_logger, method, path, statusCode, elapsed, null); ``` ## Implementation Checklists ### Before Starting a Component - [ ] Read the step documentation thoroughly - [ ] Understand dependencies on previous steps - [ ] Review related existing code patterns - [ ] Identify configuration requirements - [ ] Plan test coverage strategy ### During Implementation - [ ] Follow naming conventions - [ ] Add XML documentation to public APIs - [ ] Implement `IDisposable`/`IAsyncDisposable` where needed - [ ] Add structured logging at appropriate levels - [ ] Handle cancellation tokens throughout - [ ] Use result types for expected failures - [ ] Validate all configuration at startup ### Before Marking Complete - [ ] All public types have XML documentation - [ ] Unit tests achieve >80% coverage - [ ] Integration tests cover happy path + error cases - [ ] No compiler warnings - [ ] Code passes all linting rules - [ ] Configuration is validated - [ ] README/documentation updated if needed ### Pull Request Checklist - [ ] PR title follows convention: `feat(router): description` - [ ] Description explains what and why - [ ] All tests pass - [ ] No unrelated changes - [ ] Breaking changes documented - [ ] Reviewable size (<500 lines preferred) ## Testing Requirements ### Unit Test Coverage Targets | Component Type | Target Coverage | |---------------|-----------------| | Core logic | 90% | | Handlers | 85% | | Middleware | 80% | | Configuration | 75% | | Extensions | 70% | ### Test Structure ```csharp // Test file naming: {ClassName}Tests.cs // Test method naming: {Method}_{Scenario}_{ExpectedResult} public class JwtValidatorTests { private readonly JwtValidator _sut; // System Under Test private readonly Mock _keyProviderMock; private readonly Mock> _loggerMock; public JwtValidatorTests() { _keyProviderMock = new Mock(); _loggerMock = new Mock>(); var options = Options.Create(new JwtValidationOptions { Issuer = "https://auth.example.com", Audience = "stella-router" }); _sut = new JwtValidator(options, _keyProviderMock.Object, _loggerMock.Object); } [Fact] public async Task ValidateAsync_ValidToken_ReturnsSuccessWithClaims() { // Arrange var token = GenerateValidToken(); _keyProviderMock .Setup(x => x.GetSigningKeyAsync(It.IsAny())) .ReturnsAsync(TestKeys.ValidKey); // Act var result = await _sut.ValidateAsync(token); // Assert Assert.True(result.IsSuccess); Assert.NotNull(result.Value); Assert.Equal("test-user", result.Value.Subject); } [Fact] public async Task ValidateAsync_ExpiredToken_ReturnsFailure() { // Arrange var token = GenerateExpiredToken(); // Act var result = await _sut.ValidateAsync(token); // Assert Assert.False(result.IsSuccess); Assert.Equal("TOKEN_EXPIRED", result.Error!.Code); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public async Task ValidateAsync_NullOrEmptyToken_ReturnsFailure(string? token) { // Act var result = await _sut.ValidateAsync(token!); // Assert Assert.False(result.IsSuccess); Assert.Equal("INVALID_TOKEN", result.Error!.Code); } } ``` ### Integration Test Patterns ```csharp public class RouterIntegrationTests : IClassFixture { private readonly RouterTestFixture _fixture; public RouterIntegrationTests(RouterTestFixture fixture) { _fixture = fixture; } [Fact] public async Task EndToEnd_AuthenticatedRequest_ReturnsSuccess() { // Arrange var client = _fixture.CreateAuthenticatedClient(claims: new() { ["sub"] = "test-user", ["role"] = "admin" }); // Act var response = await client.GetAsync("/api/users/123"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var user = await response.Content.ReadFromJsonAsync(); Assert.NotNull(user); Assert.Equal("123", user.Id); } } // Test fixture public class RouterTestFixture : IAsyncLifetime { private IHost? _gatewayHost; private IHost? _microserviceHost; public async Task InitializeAsync() { // Start microservice _microserviceHost = await CreateMicroserviceHost(); await _microserviceHost.StartAsync(); // Start gateway _gatewayHost = await CreateGatewayHost(); await _gatewayHost.StartAsync(); } public async Task DisposeAsync() { if (_gatewayHost != null) await _gatewayHost.StopAsync(); if (_microserviceHost != null) await _microserviceHost.StopAsync(); _gatewayHost?.Dispose(); _microserviceHost?.Dispose(); } public HttpClient CreateAuthenticatedClient(Dictionary claims) { var token = GenerateTestToken(claims); var client = new HttpClient { BaseAddress = new Uri("http://localhost:5000") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } } ``` ## Git and PR Conventions ### Branch Naming ``` feat/router-- fix/router- refactor/router- test/router- docs/router- ``` ### Commit Messages ``` (): [optional body] [optional footer] ``` Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore` Examples: ``` feat(router): implement JWT validation with per-endpoint keys - Add JwtValidator with configurable key sources - Support RS256 and ES256 algorithms - Add JWKS endpoint caching with TTL Closes #123 ``` ### PR Template ```markdown ## Summary Brief description of what this PR does. ## Changes - Change 1 - Change 2 - Change 3 ## Testing - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed ## Checklist - [ ] Code follows project conventions - [ ] Documentation updated - [ ] No breaking changes (or documented if any) - [ ] All tests pass ``` ## Common Pitfalls to Avoid ### Performance ```csharp // ❌ BAD: Allocating in hot path public bool MatchRoute(string path) { var parts = path.Split('/'); // Allocation // ... } // ✅ GOOD: Use Span for parsing public bool MatchRoute(ReadOnlySpan path) { // Zero-allocation parsing foreach (var segment in path.Split('/')) { // ... } } // ❌ BAD: Synchronous I/O blocking async context public async Task ProcessAsync() { var config = File.ReadAllText("config.json"); // Blocking! } // ✅ GOOD: Async all the way public async Task ProcessAsync() { var config = await File.ReadAllTextAsync("config.json"); } ``` ### Thread Safety ```csharp // ❌ BAD: Non-thread-safe collection private readonly Dictionary _routes = new(); public void AddRoute(string key, Route route) { _routes[key] = route; // Not thread-safe! } // ✅ GOOD: Thread-safe collection private readonly ConcurrentDictionary _routes = new(); public void AddRoute(string key, Route route) { _routes[key] = route; // Thread-safe } // ✅ GOOD: Immutable update private ImmutableDictionary _routes = ImmutableDictionary.Empty; public void AddRoute(string key, Route route) { ImmutableInterlocked.AddOrUpdate(ref _routes, key, route, (_, _) => route); } ``` ### Resource Management ```csharp // ❌ BAD: Not disposing resources public async Task SendAsync(byte[] data) { var client = new TcpClient(); await client.ConnectAsync("host", 9100); await client.GetStream().WriteAsync(data); // client never disposed! } // ✅ GOOD: Proper disposal public async Task SendAsync(byte[] data) { using var client = new TcpClient(); await client.ConnectAsync("host", 9100); await using var stream = client.GetStream(); await stream.WriteAsync(data); } // ✅ GOOD: Connection pooling public class ConnectionPool : IDisposable { private readonly Channel _pool; public async Task RentAsync() { if (_pool.Reader.TryRead(out var client)) return client; return await CreateNewConnectionAsync(); } public void Return(TcpClient client) { if (!_pool.Writer.TryWrite(client)) client.Dispose(); } } ``` ## Deliverables | Artifact | Purpose | |----------|---------| | This document | Agent implementation guidelines | | Code templates | Consistent starting points | | Checklists | Quality gates | | Test patterns | Consistent testing approach | ## Next Step [Step 29: Integration Testing & CI →](29-Step.md)