Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions docs/ai-defects/0240-fragmented-dependency-injection-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# CAR-0240: Fragmented dependency injection composition

## Status

Prevented — 2026-09-10

## References

- Issue: #240
- Introducing change: #30 for the duplicate Runtime execution-scope registration; broader composition growth spans multiple changes
- Corrective pull request: #241
- Related ADRs: ADR-0001, ADR-0032

## Defect

The standalone composition accumulated 154 direct service registrations in one
`AddAgentstration` method while additional registrations remained in the Web
and Workplace executable roots and in a broad Console extension.

The default container silently accepted an exact duplicate
`IRuntimeRunExecutionScope` registration. Other exactly-one services relied on
caller order to replace fallbacks, including the Model Profile reference
validator and configured GenAI observability options. This contradicted the
repository's explicit modular-monolith boundaries and made the effective graph
hard to review.

## Detection

- Stage: Code review
- Detection mechanism: issue #240 audited all production
`IServiceCollection` calls and compared exact service descriptors across the
complete host composition.
- Why it was not detected earlier: functional host tests resolved only the last
descriptor selected by the default container. They did not inspect descriptor
cardinality, so duplicate exactly-one registrations remained invisible.

## Causal analysis

### Faulty approach

AI-assisted feature changes appended registrations near the feature being
implemented without assigning durable registration ownership to a module-level
composition extension. Pull request #30 added a Runtime execution-scope
registration near the Runtime queue even though pull request #28 had already
registered the same contract near Runtime Run services.

Later changes followed the same append-to-root pattern. The application
continued to start because Microsoft.Extensions.DependencyInjection resolves
the last descriptor for a single service request.

### Contributing assumptions

- Successful host startup was treated as evidence that each exactly-one
contract had one descriptor.
- Registration order was assumed to be a sufficient replacement mechanism for
standalone fallbacks.
- Keeping registrations in one composition root was treated as equivalent to
keeping their module ownership explicit.
- Feature-level tests were assumed to cover the structure of the complete
service graph.

### Missed signals

- Existing storage, identity, Model Provider, Management, MCP, and UI
registration extensions already demonstrated cohesive ownership.
- `Agentstration.Infrastructure` is documented as composition support for
explicit module boundaries, but its public method had become a multi-module
implementation body.
- The two identical `IRuntimeRunExecutionScope` statements were visible in the
same file.
- No test enumerated `ServiceDescriptor` instances for exactly-one contracts.

### Safeguard gap

The repository validated behavior and host lifecycle but did not validate
descriptor count, lifetime, implementation selection, or representative
provider graphs with `ValidateOnBuild` and `ValidateScopes`. The default
container's last-registration-wins behavior therefore masked duplicates and
undocumented replacements.

## Resolution

The platform composition is split into focused extensions for foundation,
control-plane storage, security/bootstrap, agent runtime, Packs, Sources,
tools/Triggers, Runtime Runs, Work, and Flows. The public `AddAgentstration`
method is retained as a small façade and accepts a cohesive options object,
while its existing overload remains compatible.

Web, Console, and Workplace registrations are delegated to focused host
extensions. The duplicate Runtime execution-scope descriptor is removed.
Configured GenAI options, the Management-backed Model Profile validator, and
the server composite Flow event sink now use explicit replacement semantics.

## Prevention

Registration-contract tests now inspect the descriptor collection before
provider creation. They assert cardinality for exactly-one contracts, enumerate
intentional multi-bindings, verify Deterministic and Managed resolver
selection, and validate SQLite and PostgreSQL composition.

Architecture tests keep executable roots free of direct concrete
registrations and keep `AddAgentstration` as a façade. The durable ownership map
in `docs/architecture/dependency-injection.md` documents where new
registrations belong and when `TryAdd`, `Replace`, or repeated `Add` is valid.

## Validation

- Static diff and whitespace validation completed.
- GitHub Actions CI run 506 passed formatting verification, the complete
Agentstration build, the MSTest suite, the Source Registry package smoke
test, PostgreSQL migration/restart and concurrency validation, the container
build, and Windows host lifecycle validation.
- Dependency Review run 380, Documentation run 336, and CodeQL run 502 passed.
- The local execution environment did not provide the .NET SDK; all .NET
validation was therefore executed by GitHub Actions against pull request
#241.
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ Runtime.Storage.Sqlite -> Runtime.Abstractions + EF Core SQLite
Work.Storage.Sqlite -> Work storage abstractions + EF Core SQLite
```

The executable dependency-injection ownership map and registration semantics
are documented in
[Dependency injection composition](architecture/dependency-injection.md).

Canonical Management resources and provider-neutral ports live in `Management.Abstractions`; validation and use cases live in `Management.Core`. SQLite and EF Core are confined to module-specific storage projects. Concrete `AIAgent` types are confined to `Runtime.AgentFramework`. Foundry is absent from every central project.

`Agentstration.Resources` contains the neutral namespace, scope-reference, and address value types shared across boundaries. Management resources retain globally unique UIDs and use `(scope, namespace, kind, name)` as their exact logical identity. Canonical scope references are `/instance`, `/tenants/{tenantId}`, and `/workspaces/{workspaceId}`. Existing workspace callers implicitly use their current workspace and the `default` namespace. Relative references inherit their owner's namespace; explicit cross-namespace references retain the supplied namespace. See ADR-0035 and ADR-0079.
Expand Down
103 changes: 103 additions & 0 deletions docs/architecture/dependency-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Dependency injection composition

Agentstration uses explicit `IServiceCollection` extensions as the executable
map of its modular-monolith boundaries. Registration is intentionally not
assembly-scanned: optional providers, storage selection, security policies,
hosted services, and ordered multi-bindings must remain visible during review.

## Composition roots

| Root | Responsibility |
|---|---|
| `Agentstration.Web/Program.cs` | Resolve host configuration, invoke `AddAgentstrationWebHost`, map transports, and run the ordered startup lifecycle. |
| `Agentstration.Workplace.Web/Program.cs` | Resolve API endpoints, invoke `AddAgentstrationWorkplaceHost`, and map the standalone Workplace UI. |
| `Agentstration.AppHost/Program.cs` | Compose Aspire resources and pass configuration to the executable hosts and AEP extensions. |
| AEP extension `Program.cs` files | Register only the provider hosted by that autonomous extension process. |

Endpoint mapping, database initialization, bootstrap application, extension
discovery, and application start are lifecycle operations. They remain outside
service-registration extensions.

## Platform registration ownership

`AddAgentstration(AgentstrationServiceRegistrationOptions)` is the public
platform façade. It validates and normalizes the host inputs once, then invokes
the following Infrastructure-owned extensions in dependency order.

| Extension | Owned registrations |
|---|---|
| `AddAgentstrationFoundation` | Time, request context, Management events, AI defaults, and direct chat-client fallback. |
| `AddAgentstrationControlPlane` | Storage options, platform initializer, and the selected SQLite or PostgreSQL control-plane store. |
| `AddAgentstrationSecurityAndBootstrap` | Secret vaults, identity/authorization services, audit, topology bootstrap, and bootstrap resource handlers. |
| `AddAgentstrationAgentRuntime` | Agent compilation and resolution, MAF materialization, Runtime registry and queue, deployment provisioners, routing, and MCP tools. |
| `AddAgentstrationPacks` | Pack archive/artifact services, resource handlers, composition, authoring, and management. |
| `AddAgentstrationSources` | Manifest retrieval, verification index, snapshots, compatibility, catalogs, and Source management. |
| `AddAgentstrationToolingAndTriggers` | Tool resources, hooks, Trigger services, Quartz configuration, and optional scheduler hosted services. |
| `AddAgentstrationRuntimeRuns` | Selected Runtime Run store, Run lifecycle, Tool execution pipeline, event sinks, and audit reader. |
| `AddAgentstrationWorkPlane` | Selected Work store, artifact store, execution queue/gateway, Work Items, Workplace, and task projection. |
| `AddAgentstrationFlowPlane` | Selected Flow store, definitions, Entries, Run queue, execution scopes, expressions, orchestration, and retention. |

Storage implementation projects continue to own their provider-specific
extensions. Infrastructure selects one provider once and calls those methods;
it does not reproduce EF Core registration details.

Existing module-owned extensions remain authoritative:

| Owning project | Extensions |
|---|---|
| Management Core | `AddAgentstrationModelManagement` |
| Model Providers | `AddAgentstrationModelProviders` |
| MCP tools | `AddAgentstrationMcpTools` |
| Identity | `AddAgentstrationLocalIdentity`, `AddAgentstrationPostgreSqlIdentity` |
| Management storage | `AddSqliteControlPlane`, `AddPostgreSqlControlPlane` |
| Runtime storage | `AddSqliteRuntimeRuns`, `AddPostgreSqlRuntimeRuns` |
| Work storage | `AddSqliteWorkPlane`, `AddPostgreSqlWorkPlane` |
| Flow storage | `AddSqliteFlowStorage`, `AddPostgreSqlFlowStorage` |
| Shared Web UI | `AddAgentstrationWebComponents`, `AddAgentstrationLocalization`, `AddAgentstrationFlowDesigner` |
| Workplace client | `AddAgentstrationWorkplaceClient` |
| AEP ASP.NET Core | `AddAgentstrationAep` and its explicit contribution extensions |

The string-parameter `AddAgentstration` overload remains a compatibility
façade. New composition code should use
`AgentstrationServiceRegistrationOptions` so adding a host setting does not
extend an ordered parameter list.

## Server and Console ownership

`AddAgentstrationWebHost` composes the platform façade with focused server
registrations:

- Model Provider and Management services;
- extension discovery and AEP enrollment;
- HTTP/Razor/SignalR/MCP transport;
- the selected ASP.NET Core Identity store;
- bootstrap host services;
- realtime projections;
- Console composition;
- optional background workers and test cleanup.

`AddAgentstrationWebConsole` remains the convenient Console façade and delegates
to separate component, HTTP/realtime client, authentication, and authorization
registrations. `AddAgentstrationObservability` owns Web logging, tracing, and
metrics. Workplace follows the same pattern through
`AddAgentstrationWorkplaceHost` and
`AddAgentstrationWorkplaceObservability`.

## Registration semantics

- `TryAdd*` denotes a genuine standalone/test fallback that a fuller host may
replace.
- `Replace` denotes an intentional exactly-one production selection, including
the configured GenAI options, the Management-backed Model Profile validator,
and the server's composite Flow event sink.
- Repeated `Add*` calls for the same contract are allowed only for intentional
`IEnumerable<T>` contributions. Current examples include secret vaults,
bootstrap and Pack handlers, agent deployment provisioners, and Tool
execution event sinks.
- Service lifetimes are part of the composition contract. Refactoring a
registration into another extension must not silently change its lifetime.
- Registration-contract tests inspect descriptor cardinality before provider
construction and build representative providers with scope/build validation.

These rules prevent the default container's last-registration-wins behavior
from hiding accidental duplicates or undocumented caller-order dependencies.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Agentstration.Infrastructure.Agents;
using Agentstration.Infrastructure.Runtime;
using Agentstration.Management.Abstractions;
using Agentstration.Management.Core;
using Agentstration.ModelProviders;
using Agentstration.Runtime.Abstractions;
using Agentstration.Runtime.AgentFramework;
using Agentstration.Runtime.Local;
using Agentstration.Tools.Mcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Agentstration.Infrastructure;

internal static class AgentRuntimeServiceCollectionExtensions
{
internal static IServiceCollection AddAgentstrationAgentRuntime(
this IServiceCollection services,
AgentstrationServiceRegistrationContext context)
{
services.AddSingleton<IAgentDefinitionCompiler, AgentDefinitionCompiler>();
services.AddSingleton<IRuntimeAgentResolver, ControlPlaneRuntimeAgentResolver>();
services.TryAddSingleton<IModelProfileReferenceValidator, DeferredModelProfileReferenceValidator>();
if (!context.UseManagedProfileResolver)
services.AddSingleton<IChatClientResolver, SingleChatClientResolver>();
services.AddAgentstrationMcpTools();
services.AddSingleton<AgentRuntimeContext>();
services.AddSingleton<AgentFrameworkRuntimeFactory>();
services.AddSingleton<IAgentRuntimeFactory>(provider =>
provider.GetRequiredService<AgentFrameworkRuntimeFactory>());
services.AddSingleton<IRuntimeRegistry, RuntimeRegistry>();
services.AddSingleton<IRuntimeRunQueue, LocalRuntimeRunQueue>();
services.AddSingleton<IRuntimeRunCancellationRegistry, LocalRuntimeRunCancellationRegistry>();
services.AddSingleton<IRuntimeRunExecutionScope, WorkspaceRuntimeRunExecutionScope>();
services.AddSingleton<IAgentDeploymentProvisioner, InProcessAgentProvisioner>();
services.AddSingleton<IAgentDeploymentProvisioner, SharedHostAgentProvisioner>();
services.AddSingleton<IAgentDeploymentReconciler, LocalAgentDeploymentReconciler>();
services.AddSingleton<IAgentRouter, AgentFrameworkAgentRouter>();
services.AddSingleton(new AgentRevisionRetentionOptions());
services.AddSingleton<AgentManagementService>();
services.AddSingleton<AgentExecutionCoordinator>();
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Agentstration.Infrastructure.Agents;
using Agentstration.Infrastructure.Sources;

namespace Agentstration.Infrastructure;

public sealed record AgentstrationServiceRegistrationOptions
{
public required string DataDirectory { get; init; }
public AiProviderOptions? AiOptions { get; init; }
public string? ControlPlaneConnectionString { get; init; }
public string? WorkPlaneConnectionString { get; init; }
public string? FlowConnectionString { get; init; }
public string? RuntimeConnectionString { get; init; }
public AgentstrationStorageOptions? StorageOptions { get; init; }
public bool EnableHostedServices { get; init; } = true;
public SourceVerificationIndexOptions? SourceVerificationIndexOptions { get; init; }
}

internal sealed record AgentstrationServiceRegistrationContext(
string DataDirectory,
AiProviderOptions AiOptions,
AgentstrationStorageOptions StorageOptions,
AgentstrationStorageProvider StorageProvider,
string? ControlPlaneConnectionString,
string? WorkPlaneConnectionString,
string? FlowConnectionString,
string? RuntimeConnectionString,
string SchedulerConnectionString,
bool EnableHostedServices,
SourceVerificationIndexOptions SourceVerificationIndexOptions)
{
public bool UseManagedProfileResolver =>
string.Equals(AiOptions.Provider, "Managed", StringComparison.OrdinalIgnoreCase);

public static AgentstrationServiceRegistrationContext Create(AgentstrationServiceRegistrationOptions options)
{
ArgumentException.ThrowIfNullOrWhiteSpace(options.DataDirectory);

var aiOptions = options.AiOptions
?? new AiProviderOptions("Deterministic", new Uri("http://localhost/"), "deterministic", null);
var storageOptions = options.StorageOptions ?? new AgentstrationStorageOptions();
var storageProvider = storageOptions.GetProvider();
var schedulerConnectionString = storageProvider == AgentstrationStorageProvider.PostgreSql
? storageOptions.ConnectionString!
: $"Data Source={Path.Combine(options.DataDirectory, "scheduler.db")};Pooling=False";

return new AgentstrationServiceRegistrationContext(
options.DataDirectory,
aiOptions,
storageOptions,
storageProvider,
options.ControlPlaneConnectionString,
options.WorkPlaneConnectionString,
options.FlowConnectionString,
options.RuntimeConnectionString,
schedulerConnectionString,
options.EnableHostedServices,
options.SourceVerificationIndexOptions ?? new SourceVerificationIndexOptions());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Agentstration.Management.Abstractions;
using Agentstration.Management.Storage.PostgreSql;
using Agentstration.Management.Storage.Sqlite;
using Microsoft.Extensions.DependencyInjection;

namespace Agentstration.Infrastructure;

internal static class ControlPlaneServiceCollectionExtensions
{
internal static IServiceCollection AddAgentstrationControlPlane(
this IServiceCollection services,
AgentstrationServiceRegistrationContext context)
{
services.AddSingleton(context.StorageOptions);
if (context.StorageProvider == AgentstrationStorageProvider.PostgreSql)
{
services.AddSingleton<IAgentstrationStorageInitializer, PostgreSqlStorageInitializer>();
services.AddPostgreSqlControlPlane(context.StorageOptions.ConnectionString!);
}
else
{
services.AddSingleton<IAgentstrationStorageInitializer, SqliteStorageInitializer>();
services.AddSqliteControlPlane(
context.ControlPlaneConnectionString
?? $"Data Source={Path.Combine(context.DataDirectory, "control-plane.db")}");
}

return services;
}
}
Loading
Loading