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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`SmartHopper.ProviderSdk` package** — new MIT-licensed assembly that exposes the provider-facing surface (contracts, base classes, request/response DTOs, model capabilities, settings descriptors, streaming, tool DTOs, version attributes) so community providers can build against it without cloning SmartHopper. See [docs/Providers/ProviderSdk.md](docs/Providers/ProviderSdk.md).
- **Host abstractions** — `IProviderTrustHost`, `IProviderRegistryHost`, `IPolicyPipelineHost`, `IContextProviderHost`, `IToolRegistryHost`, `IProviderSettingsStore`, `IProviderLogger`, `IProviderHttpClientFactory`, `IProviderDiagnostics`, and the `ProviderSdkHost` composition root in the SDK; `SmartHopperProviderTrustHost` adapter in Infrastructure wires `ProviderManager` into the SDK.
- **Per-provider `AssemblyLoadContext`** (`ProviderAssemblyLoader`) — loads provider-private DLLs in isolation while delegating SDK/Infrastructure/`Newtonsoft.Json`/`System.Drawing.Common` to the default ALC. Rejects providers whose `IAIProviderFactory` type identity does not match the host.
- **SDK SemVer metadata** — `SmartHopperProviderSdkVersionAttribute`, `BuiltAgainstSdkAttribute`, `MinHostSdkAttribute`, `SmartHopperProviderIdAttribute`, and `SdkCompatibility.Check` enforce `BuiltAgainstSdk.MAJOR == HostSdk.MAJOR` and `HostSdk >= MinHostSdk` at load time.
- **`ProviderClassifier`** — cryptographic classification (strong-name + Authenticode + SHA-256 manifest) producing `Official`, `OfficialTampered`, `Community`, or `Invalid`. File names and provider ids no longer affect classification.
- **Trust settings** — `AllowCommunityProviders` and `BlockNonOfficialProviders` toggles in `SmartHopperSettings`. Structured `TrustedProviderRecords` schema with classification, hash-at-decision, signer thumbprint, and strong-name token; legacy `TrustedProviders` boolean map is migrated automatically on first load.
- **`ProviderManager` warning APIs** — `IsProviderCommunity`, `IsProviderUnsigned`, `GetProviderClassification`, `GetProviderTrustRecord`, plus existing mismatched/unknown/unavailable accessors.
- **AI component badges** — `AIRequestCall` validation now emits runtime warning messages when the selected provider is community/unsigned, in addition to the existing mismatched/unavailable/unknown warnings.
- **User-local provider directory** — `%AppData%/SmartHopper/Providers` is scanned in addition to the app-local directory. App-local providers win on duplicate-id conflicts.
- **Initialization isolation** — `InitializeProviderAsync` runs under a 30-second per-provider timeout so a hanging provider cannot block discovery.

### Changed

- Provider-facing types moved from `SmartHopper.Infrastructure.*` to `SmartHopper.ProviderSdk.*` namespaces. All callers in the repo updated accordingly.
- `ProviderManager.LoadProviderAssemblyAsync` now consults `ProviderClassifier` and applies policy decisions based on classification + the new trust settings before falling through to the existing hash/trust flow.
- Duplicate provider ids: subsequent registrations of an id already held by an `Official` provider are rejected. Other duplicates replace the previously registered instance.
- Provider integrity warnings (mismatched, unavailable, unknown, community, unsigned) are now surfaced via the existing `AIRuntimeMessage` pipeline on every AI component that selects the affected provider.

### Removed

- `InternalsVisibleTo` entries for `SmartHopper.Providers.*` in `SmartHopper.Infrastructure.csproj`. Built-in providers now compile only against the public SDK surface.

### Security

- Provider classification is now purely cryptographic; spoofed names cannot reach `Official` without matching strong-name, Authenticode, or hash manifest signals.
- `BlockNonOfficialProviders=true` is a hard gate that blocks any non-Official provider regardless of per-provider trust or `AllowCommunityProviders`.
- Community/unsigned providers are blocked at load time unless `AllowCommunityProviders=true` AND a per-provider trust prompt has been accepted; trust is invalidated automatically if a community provider's SHA-256 changes.
- Trusting a community provider grants full process privileges and is documented as a deliberate user decision.

### ⚠️ BREAKING CHANGES

- **Renamed AI Tools** (old → new):
Expand Down
17 changes: 17 additions & 0 deletions SmartHopper.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core.Grasshoppe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core.Tests", "src\SmartHopper.Core.Tests\SmartHopper.Core.Tests.csproj", "{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.ProviderSdk", "src\SmartHopper.ProviderSdk\SmartHopper.ProviderSdk.csproj", "{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -224,11 +226,26 @@ Global
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x64.Build.0 = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x86.ActiveCfg = Release|Any CPU
{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x86.Build.0 = Release|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x64.ActiveCfg = Debug|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x64.Build.0 = Debug|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x86.ActiveCfg = Debug|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x86.Build.0 = Debug|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|Any CPU.Build.0 = Release|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x64.ActiveCfg = Release|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x64.Build.0 = Release|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x86.ActiveCfg = Release|Any CPU
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D5011CB-B808-41E4-A6FC-C01F3190649C}
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
24 changes: 17 additions & 7 deletions docs/Providers/ProviderManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,29 @@ Provide a central service to load external providers (`SmartHopper.Providers.*.d
## Key features

- Discovery
- Scans application directory for `SmartHopper.Providers.*.dll`.
- Instantiates `IAIProviderFactory` implementations to create providers and settings.
- Supply-chain security
- Authenticode certificate thumbprint must match host.
- Strong-name public key token must match host.
- First discovery prompts user to trust; decision persisted.
- Scans the app-local directory and `%AppData%/SmartHopper/Providers` for `SmartHopper.Providers.*.dll`.
- Each candidate is loaded into a per-provider `AssemblyLoadContext` (see `ProviderAssemblyLoader`) so private dependencies stay isolated.
- SDK type identity (`IAIProviderFactory`) is validated before activation. Mismatch → `Invalid`.
- SemVer compatibility is checked via `BuiltAgainstSdk`/`MinHostSdk` assembly attributes.
- Cryptographic classification (`ProviderClassifier`)
- `Official` — strong-name token matches host AND/OR Authenticode matches host AND/OR SHA-256 is in the published manifest, with no contradicting signal.
- `OfficialTampered` — one signal says official but another contradicts. Always blocked.
- `Community` — valid managed assembly not tied to SmartHopper. Subject to `AllowCommunityProviders` and `BlockNonOfficialProviders`.
- `Invalid` — load failure, missing factory, SDK type-identity mismatch, version incompatibility.
- Trust settings (`SmartHopperSettings`)
- `AllowCommunityProviders` (default `false`): community providers are blocked unless this is on.
- `BlockNonOfficialProviders` (default `false`): hard override that allows only `Official` providers.
- `ProviderIntegrityCheckMode` continues to govern hash-mismatch behavior for Official providers.
- `TrustedProviderRecords` — structured per-provider trust records (legacy `TrustedProviders` boolean map is migrated automatically).
- Registration & initialization
- Registers provider + settings; runs `InitializeProviderAsync()` in background.
- Duplicate provider ids: Official > Community > everything else. Tampered/Invalid never win.
- `InitializeProviderAsync()` is wrapped in a 30-second per-provider timeout so a hanging provider can't block discovery.
- Accessors
- `GetProviders(includeUntrusted=false)`
- `GetProvider(name)` (handles "Default" indirection)
- `GetProviderSettings(name)`, `GetProviderAssembly(name)`, `GetProviderIcon(name)`
- `GetDefaultAIProvider()`
- `GetProviderClassification(name)`, `IsProviderCommunity(name)`, `IsProviderUnsigned(name)`, `IsProviderMismatched(name)`, `IsProviderUnknown(name)`, `IsProviderUnavailable(name)`, `GetProviderTrustRecord(name)`
- Settings management
- `UpdateProviderSettings(name, Dictionary<string, object>)`
- Validates via `IAIProviderSettings.ValidateSettings`, persists via `SmartHopperSettings`, refreshes provider cache.
Expand Down
85 changes: 85 additions & 0 deletions docs/Providers/ProviderSdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# SmartHopper Provider SDK

The Provider SDK (`SmartHopper.ProviderSdk`) is a standalone, MIT-licensed assembly that exposes the contracts, base classes, and DTOs a community AI provider needs to integrate with SmartHopper. Provider authors can build against this SDK on a clean machine without cloning or building the rest of the SmartHopper repo.

## What the SDK contains

The SDK is **API-connection only**:

- **Contracts** — `IAIProvider`, `IAIProviderFactory`, `IAIProviderSettings`, `IAIProviderModels`.
- **Base classes** — `AIProvider`, `AIProvider<T>`, `AIProviderSettings`, `AIProviderModels`, `AIProviderStreamingAdapter`.
- **Request / response DTOs** — `AICall.Core.{Base, Interactions, Requests, Returns}`, `AICall.Metrics`, minimal `AICall.JsonSchemas` types referenced by request bodies.
- **Model capabilities** — `AIModels.*`, `AIExtraDescriptor`, `AIModelCapabilityRegistry` singleton.
- **Settings descriptors** — `SettingDescriptor`, secret flags, validation result types.
- **Streaming** — `IStreamingAdapter` and provider-facing delta/result types.
- **Tool DTOs** — structures required to encode/decode tool calls and tool results inside a provider response (no `ToolManager`, no tool registration).
- **Compatibility metadata** — `SmartHopperProviderSdkVersionAttribute`, `BuiltAgainstSdkAttribute`, `MinHostSdkAttribute`, `SmartHopperProviderIdAttribute`.

## What is NOT in the SDK

These types stay host-side in `SmartHopper.Infrastructure` and the rest of the application:

- `ProviderManager`, `ProviderHashVerifier`, `ProviderClassifier`, trust dialogs, signature/hash policy, `SmartHopperSettings` persistence, secret storage.
- `AICall.{Policies, Sessions, Validation, Execution, Batch}`.
- `AITools.*` (`ToolManager`, `IAIToolProvider`).
- Rhino/Eto UI, WebChat, badges.

## Target frameworks

- `net7.0` (cross-platform; macOS-friendly)
- `net7.0-windows` (Windows-specific surface)

## License

The SDK is licensed under **MIT**. SmartHopper itself remains LGPLv3. The permissive license on the SDK lets closed-source community providers link against it without taking on LGPL obligations on their own assemblies.

## Naming convention

Provider assemblies discovered at runtime must match `SmartHopper.Providers.*.dll`. SmartHopper scans:

1. The app-local directory (next to `SmartHopper.Infrastructure.dll`).
2. `%AppData%/SmartHopper/Providers` (Windows) or the platform equivalent under the user's application data folder.

App-local providers win on duplicate-id conflicts (see plan §2.2).

## Minimal provider

```csharp
using SmartHopper.ProviderSdk.AIProviders;
using SmartHopper.ProviderSdk.Settings;

[assembly: SmartHopper.ProviderSdk.Metadata.BuiltAgainstSdk("1.0.0")]
[assembly: SmartHopper.ProviderSdk.Metadata.MinHostSdk("1.0.0")]
[assembly: SmartHopper.ProviderSdk.Metadata.SmartHopperProviderId("my-provider")]

public sealed class MyProviderFactory : IAIProviderFactory
{
public IAIProvider CreateProvider() => new MyProvider();
public IAIProviderSettings CreateProviderSettings() => new MyProviderSettings();
}
```

The host instantiates the factory through an isolated `AssemblyLoadContext` so private dependencies of the provider don't leak into the rest of the SmartHopper process.

## SemVer & compatibility

- The SDK uses Semantic Versioning. `MAJOR` is reserved for breaking provider contract changes.
- Providers declare both `BuiltAgainstSdk` and `MinHostSdk` via assembly attributes.
- At load time the host enforces `BuiltAgainstSdk.MAJOR == HostSdk.MAJOR` and `HostSdk >= provider.MinHostSdk`. Mismatches classify the provider as `Invalid` and block it with a clear diagnostic.
- Two host majors cannot coexist in one Rhino process and this is documented as unsupported.

## Trust model

Community providers (those not cryptographically attributable to SmartHopper) are only loaded when:

1. `AllowCommunityProviders = true` in `SmartHopperSettings.json`, and
2. `BlockNonOfficialProviders = false`, and
3. The user accepts the per-provider trust prompt the first time a given community DLL is discovered.

The classification is purely cryptographic — strong-name token + Authenticode (Windows) + the SmartHopper hash manifest. File names and provider ids cannot reach `Official` without one of those signals.

Community/unsigned providers surface warning runtime messages on every AI component that uses them.

## Threat notes

Trusting a community provider grants it full SmartHopper process privileges. The SDK does not sandbox provider code. Community providers can read other providers' settings only through the `IProviderSettingsStore` abstraction, which scopes access by provider id.
8 changes: 6 additions & 2 deletions docs/Providers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ Providers implement API-specific logic while conforming to a common contract so

## Security

- Signature verification before load, trusted providers tracked in settings.
- Secrets stored using OS secure mechanisms; no hardcoded keys.
- **Cryptographic classification** — providers are classified as `Official`, `OfficialTampered`, `Community`, or `Invalid` based purely on strong-name token, Authenticode signature (Windows), and SHA-256 manifest. Names and provider ids never affect classification.
- **Trust gates** — community providers are blocked unless `AllowCommunityProviders=true`. The global `BlockNonOfficialProviders=true` switch overrides everything to allow Official providers only.
- **Per-provider trust** — first-time discovery of an allowed community provider triggers a trust prompt. Trust is invalidated automatically if the file's SHA-256 changes.
- **Visible warnings** — every AI component using a community/unsigned/unverified provider receives a runtime warning message in Grasshopper.
- **Secrets** — stored using OS secure mechanisms (DPAPI on Windows, Keychain on macOS); no hardcoded keys. Provider code is scoped to its own keys via `IProviderSettingsStore`.

## Extensibility

Expand All @@ -35,6 +38,7 @@ Providers implement API-specific logic while conforming to a common contract so

## Detailed docs

- [Provider SDK (community-facing)](./ProviderSdk.md)
- [ProviderManager](./ProviderManager.md)
- [IAIProvider](./IAIProvider.md)
- [AIProvider](./AIProvider.md)
Expand Down
34 changes: 34 additions & 0 deletions samples/SmartHopper.Providers.Sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SmartHopper Provider SDK — Sample scaffolding

This directory contains a starter csproj that builds a SmartHopper community provider against the `SmartHopper.ProviderSdk` package only — no SmartHopper host references required.

## Layout

```
samples/
└── SmartHopper.Providers.Sample/
├── SmartHopper.Providers.Sample.csproj # references SmartHopper.ProviderSdk
└── README.md # this file
```

To turn this into a working provider:

1. Add a class deriving from `SmartHopper.ProviderSdk.AIProviders.AIProvider` (or `AIProvider<T>` for typed responses).
2. Add an `IAIProviderFactory` implementation that returns instances of your provider and its settings.
3. Add an `IAIProviderSettings` subclass with descriptors for API key, model id, etc.
4. Decorate the assembly with:
```csharp
[assembly: SmartHopper.ProviderSdk.Metadata.BuiltAgainstSdk("1.0.0")]
[assembly: SmartHopper.ProviderSdk.Metadata.MinHostSdk("1.0.0")]
[assembly: SmartHopper.ProviderSdk.Metadata.SmartHopperProviderId("my-provider")]
```
5. Build, copy the resulting `SmartHopper.Providers.MyProvider.dll` into either:
- the SmartHopper app-local directory (next to `SmartHopper.Infrastructure.dll`), or
- `%AppData%/SmartHopper/Providers` on Windows (platform equivalent elsewhere).
6. In SmartHopper settings, enable `AllowCommunityProviders` and accept the per-provider trust prompt.

For full reference docs, see [docs/Providers/ProviderSdk.md](../../docs/Providers/ProviderSdk.md) and the in-tree providers under `src/SmartHopper.Providers.*` for working examples.

## License

Sample code is licensed under MIT, matching the SDK itself. SmartHopper host code remains under LGPLv3.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!--
SmartHopper Provider SDK Sample
Demonstrates the minimum surface needed to ship a community AI provider
for SmartHopper. This project intentionally references the SmartHopper.ProviderSdk
NuGet package only — it does not depend on SmartHopper.Infrastructure or any
SmartHopper host assembly.

In CI, this project is rebuilt from the produced .nupkg to catch packaging
issues that wouldn't show up with a project reference.

Licensed under MIT, same as the SDK.
-->
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net7.0;net7.0-windows</TargetFrameworks>
<RootNamespace>SmartHopper.Providers.Sample</RootNamespace>
<AssemblyName>SmartHopper.Providers.Sample</AssemblyName>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnableDynamicLoading>true</EnableDynamicLoading>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591;NU1701</NoWarn>
</PropertyGroup>

<ItemGroup>
<!-- In CI: switch this to a PackageReference pointing at the produced .nupkg. -->
<ProjectReference Include="..\..\src\SmartHopper.ProviderSdk\SmartHopper.ProviderSdk.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;
using SmartHopper.Core.ComponentBase;
using SmartHopper.Infrastructure.AICall.Core;
using SmartHopper.Infrastructure.AICall.Core.Base;
using SmartHopper.Infrastructure.AICall.Core.Interactions;
using SmartHopper.Infrastructure.AICall.Core.Requests;
using SmartHopper.Infrastructure.AICall.Core.Returns;
using SmartHopper.ProviderSdk.AICall.Core;
using SmartHopper.ProviderSdk.AICall.Core.Base;
using SmartHopper.ProviderSdk.AICall.Core.Interactions;
using SmartHopper.ProviderSdk.AICall.Core.Requests;
using SmartHopper.ProviderSdk.AICall.Core.Returns;

namespace SmartHopper.Components.Test.Providers
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
using Grasshopper.Kernel.Types;
using Newtonsoft.Json.Linq;
using SmartHopper.Core.ComponentBase;
using SmartHopper.Infrastructure.AICall.Core.Interactions;
using SmartHopper.Providers.Anthropic;
using SmartHopper.ProviderSdk.AICall.Core.Interactions;

namespace SmartHopper.Components.Test.Providers
{
Expand Down
Loading