Skip to content

Commit 5756322

Browse files
committed
Control-plane parity plus enhancements
1 parent 3544969 commit 5756322

79 files changed

Lines changed: 4547 additions & 136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
5252
SystemPrompt: request.Instructions,
5353
OutputFormat: request.Context.OutputFormat,
5454
Temperature: 0.1m,
55-
Metadata: baseMetadata));
55+
Metadata: baseMetadata,
56+
ContainsImageInput: request.Context.UserContent?.Any(static block => block.Kind == ContentBlockKind.Image) == true));
5657

5758
var resolvedProviderName = resolvedRequest.ProviderName;
5859

@@ -102,6 +103,16 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
102103
throw CreateMissingProviderException(resolvedProviderName, requestedModel, "provider resolution");
103104
}
104105

106+
if (request.Context.UserContent?.Any(static block => block.Kind == ContentBlockKind.Image) == true
107+
&& !provider.SupportsImageInput)
108+
{
109+
throw new ProviderExecutionException(
110+
resolvedProviderName,
111+
requestedModel,
112+
ProviderFailureKind.StreamFailed,
113+
$"Provider '{resolvedProviderName}' does not support structured image input.");
114+
}
115+
105116
// --- Build initial conversation messages ---
106117
// Do not add request.Instructions as a shared "system" chat message here.
107118
// Provider adapters apply system instructions via ProviderRequest.SystemPrompt
@@ -115,7 +126,11 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
115126
messages.AddRange(history);
116127
}
117128

118-
messages.Add(new ChatMessage("user", [new ContentBlock(ContentBlockKind.Text, request.Context.Prompt, null, null, null, null)]));
129+
messages.Add(new ChatMessage(
130+
"user",
131+
request.Context.UserContent?.Count > 0
132+
? request.Context.UserContent
133+
: [new ContentBlock(ContentBlockKind.Text, request.Context.Prompt, null, null, null, null)]));
119134

120135
// --- Tool-calling loop ---
121136
var allProviderEvents = new List<ProviderEvent>();
@@ -143,7 +158,8 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
143158
Metadata: baseMetadata,
144159
Messages: messages,
145160
Tools: availableTools,
146-
MaxTokens: options.MaxTokensPerRequest));
161+
MaxTokens: options.MaxTokensPerRequest,
162+
ContainsImageInput: messages.Any(static message => message.Content.Any(static block => block.Kind == ContentBlockKind.Image))));
147163

148164
lastProviderRequest = providerRequest;
149165

src/SharpClaw.Code.Agents/Models/AgentRunContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace SharpClaw.Code.Agents.Models;
2626
/// </param>
2727
/// <param name="IsInteractive">Whether tool approvals can interact with the caller.</param>
2828
/// <param name="ApprovalSettings">Optional bounded auto-approval settings forwarded to tool execution.</param>
29+
/// <param name="UserContent">Optional structured user content blocks for the current turn.</param>
2930
public sealed record AgentRunContext(
3031
string SessionId,
3132
string TurnId,
@@ -42,4 +43,5 @@ public sealed record AgentRunContext(
4243
IToolMutationRecorder? ToolMutationRecorder = null,
4344
IReadOnlyList<ChatMessage>? ConversationHistory = null,
4445
bool IsInteractive = true,
45-
ApprovalSettings? ApprovalSettings = null);
46+
ApprovalSettings? ApprovalSettings = null,
47+
IReadOnlyList<ContentBlock>? UserContent = null);

src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,17 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
3232
{
3333
ArgumentNullException.ThrowIfNull(request);
3434
var allowedTools = ResolveAllowedTools(request.Context.Metadata);
35+
var trustedPluginNames = ResolveTrustedNames(request.Context.Metadata, SharpClawWorkflowMetadataKeys.TrustedPluginNamesJson);
36+
var trustedMcpServerNames = ResolveTrustedNames(request.Context.Metadata, SharpClawWorkflowMetadataKeys.TrustedMcpServerNamesJson);
37+
var effectivePermissionMode = ResolvePermissionMode(request.Context.Metadata, request.Context.PermissionMode);
3538

3639
// Build tool execution context from agent run context
3740
var toolExecutionContext = new ToolExecutionContext(
3841
SessionId: request.Context.SessionId,
3942
TurnId: request.Context.TurnId,
4043
WorkspaceRoot: request.Context.WorkingDirectory,
4144
WorkingDirectory: request.Context.WorkingDirectory,
42-
PermissionMode: request.Context.PermissionMode,
45+
PermissionMode: effectivePermissionMode,
4346
OutputFormat: request.Context.OutputFormat,
4447
EnvironmentVariables: null,
4548
Model: request.Context.Model,
@@ -54,8 +57,8 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
5457
&& string.Equals(acp, "true", StringComparison.OrdinalIgnoreCase)
5558
? "acp"
5659
: null,
57-
TrustedPluginNames: null,
58-
TrustedMcpServerNames: null,
60+
TrustedPluginNames: trustedPluginNames,
61+
TrustedMcpServerNames: trustedMcpServerNames,
5962
PrimaryMode: request.Context.PrimaryMode,
6063
MutationRecorder: request.Context.ToolMutationRecorder,
6164
ApprovalSettings: request.Context.ApprovalSettings);
@@ -168,6 +171,41 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
168171
}
169172
}
170173

174+
private static IReadOnlyCollection<string>? ResolveTrustedNames(
175+
IReadOnlyDictionary<string, string>? metadata,
176+
string metadataKey)
177+
{
178+
if (metadata is null
179+
|| !metadata.TryGetValue(metadataKey, out var payload)
180+
|| string.IsNullOrWhiteSpace(payload))
181+
{
182+
return null;
183+
}
184+
185+
try
186+
{
187+
return JsonSerializer.Deserialize(payload, ProtocolJsonContext.Default.StringArray);
188+
}
189+
catch (JsonException)
190+
{
191+
return null;
192+
}
193+
}
194+
195+
private static PermissionMode ResolvePermissionMode(
196+
IReadOnlyDictionary<string, string>? metadata,
197+
PermissionMode fallback)
198+
{
199+
if (metadata is not null
200+
&& metadata.TryGetValue(SharpClawWorkflowMetadataKeys.PreferredPermissionMode, out var payload)
201+
&& Enum.TryParse<PermissionMode>(payload, ignoreCase: true, out var parsed))
202+
{
203+
return parsed;
204+
}
205+
206+
return fallback;
207+
}
208+
171209
private static IEnumerable<ToolDefinition> FilterAdvertisedTools(
172210
IReadOnlyList<ToolDefinition> registryTools,
173211
IReadOnlyCollection<string>? allowedTools)

src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,23 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
3030
services.AddSingleton<IReplTerminal, Terminal.SpectreReplTerminal>();
3131
services.AddSingleton<ReplCommandHandler>();
3232
services.AddSingleton<SessionCommandHandler>();
33+
services.AddSingleton<PermissionsCommandHandler>();
34+
services.AddSingleton<AuthCommandHandler>();
35+
services.AddSingleton<InitCommandHandler>();
36+
services.AddSingleton<ResearchCommandHandler>();
37+
services.AddSingleton<ScheduleCommandHandler>();
38+
services.AddSingleton<EvolutionCommandHandler>();
3339
services.AddSingleton<ICommandHandler, PromptCommandHandler>();
3440
services.AddSingleton<ICommandHandler, StatusCommandHandler>();
3541
services.AddSingleton<ICommandHandler, DoctorCommandHandler>();
3642
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<SessionCommandHandler>());
43+
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<PermissionsCommandHandler>());
3744
services.AddSingleton<ICommandHandler, ModelsCommandHandler>();
45+
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<AuthCommandHandler>());
46+
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<InitCommandHandler>());
47+
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ResearchCommandHandler>());
48+
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ScheduleCommandHandler>());
49+
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<EvolutionCommandHandler>());
3850
services.AddSingleton<ICommandHandler, UsageCommandHandler>();
3951
services.AddSingleton<ICommandHandler, CostCommandHandler>();
4052
services.AddSingleton<ICommandHandler, StatsCommandHandler>();
@@ -62,7 +74,14 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
6274
services.AddSingleton<ISlashCommandHandler, DoctorCommandHandler>();
6375
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<SessionCommandHandler>());
6476
services.AddSingleton<ISlashCommandHandler, SessionsSlashCommandHandler>();
77+
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<PermissionsCommandHandler>());
6578
services.AddSingleton<ISlashCommandHandler, ModelsCommandHandler>();
79+
services.AddSingleton<ISlashCommandHandler, ModelSlashCommandHandler>();
80+
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<AuthCommandHandler>());
81+
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<InitCommandHandler>());
82+
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ResearchCommandHandler>());
83+
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ScheduleCommandHandler>());
84+
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<EvolutionCommandHandler>());
6685
services.AddSingleton<ISlashCommandHandler, UsageCommandHandler>();
6786
services.AddSingleton<ISlashCommandHandler, CostCommandHandler>();
6887
services.AddSingleton<ISlashCommandHandler, StatsCommandHandler>();
@@ -86,6 +105,9 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
86105
services.AddSingleton<ISlashCommandHandler, VersionCommandHandler>();
87106
services.AddSingleton<ISlashCommandHandler, UndoCommandHandler>();
88107
services.AddSingleton<ISlashCommandHandler, RedoCommandHandler>();
108+
services.AddSingleton<ISlashCommandHandler, NewSessionSlashCommandHandler>();
109+
services.AddSingleton<ISlashCommandHandler, ResumeSlashCommandHandler>();
110+
services.AddSingleton<ISlashCommandHandler, ClearSlashCommandHandler>();
89111

90112
services.AddSingleton<IOutputRenderer, Rendering.TextOutputRenderer>();
91113
services.AddSingleton<IOutputRenderer, Rendering.JsonOutputRenderer>();

src/SharpClaw.Code.Cli/Terminal/SpectreReplTerminal.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,8 @@ public void WriteInfo(string message)
3333
/// <inheritdoc />
3434
public void WriteError(string message)
3535
=> AnsiConsole.MarkupLine($"[red]{Markup.Escape(message)}[/]");
36+
37+
/// <inheritdoc />
38+
public void ClearScreen()
39+
=> AnsiConsole.Clear();
3640
}

src/SharpClaw.Code.Commands/Abstractions/IReplTerminal.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public interface IReplTerminal
2929
/// </summary>
3030
/// <param name="message">The message to write.</param>
3131
void WriteError(string message);
32+
33+
/// <summary>
34+
/// Clears the visible terminal screen.
35+
/// </summary>
36+
void ClearScreen();
3237
}
Lines changed: 10 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,27 @@
11
using SharpClaw.Code.Commands.Models;
2-
using SharpClaw.Code.Commands.Options;
3-
using SharpClaw.Code.Protocol.Commands;
4-
using SharpClaw.Code.Protocol.Models;
52

63
namespace SharpClaw.Code.Commands;
74

85
/// <summary>
9-
/// Shows or adjusts bounded auto-approval settings for REPL-driven prompts.
6+
/// Provides a durable alias over the permissions auto-approval subcommands.
107
/// </summary>
11-
public sealed class ApprovalsSlashCommandHandler(
12-
ReplInteractionState replState,
13-
OutputRendererDispatcher outputRendererDispatcher) : ISlashCommandHandler
8+
public sealed class ApprovalsSlashCommandHandler(PermissionsCommandHandler permissionsCommandHandler) : ISlashCommandHandler
149
{
1510
/// <inheritdoc />
1611
public string CommandName => "approvals";
1712

1813
/// <inheritdoc />
19-
public string Description => "Shows or sets auto-approval scopes and budget for REPL prompts.";
14+
public string Description => "Alias for /permissions approvals show|set|clear.";
2015

2116
/// <inheritdoc />
2217
public Task<int> ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
23-
{
24-
if (command.Arguments.Length == 0)
25-
{
26-
var effective = replState.ApprovalSettingsOverride ?? context.ApprovalSettings;
27-
return RenderAsync(
28-
$"Auto-approvals: {ApprovalSettingsText.RenderSummary(effective)} (override: {(replState.ApprovalSettingsOverride is null ? "none" : ApprovalSettingsText.RenderSummary(replState.ApprovalSettingsOverride))}).",
29-
context,
30-
cancellationToken);
31-
}
32-
33-
if (string.Equals(command.Arguments[0], "reset", StringComparison.OrdinalIgnoreCase)
34-
|| string.Equals(command.Arguments[0], "clear", StringComparison.OrdinalIgnoreCase))
35-
{
36-
replState.ApprovalSettingsOverride = null;
37-
return RenderAsync("Auto-approval reset for the next prompt.", context, cancellationToken);
38-
}
39-
40-
if (!string.Equals(command.Arguments[0], "set", StringComparison.OrdinalIgnoreCase) || command.Arguments.Length < 2)
41-
{
42-
return RenderAsync("Usage: /approvals [set <scopes> [budget]|reset]", context, cancellationToken, success: false);
43-
}
44-
45-
var budget = command.Arguments.Length >= 3
46-
? ParseBudget(command.Arguments[2])
47-
: null;
48-
var settings = ApprovalSettingsText.Parse(command.Arguments[1], budget) ?? ApprovalSettings.Empty;
49-
replState.ApprovalSettingsOverride = settings;
50-
return RenderAsync(
51-
$"Auto-approval override set to {ApprovalSettingsText.RenderSummary(settings)}.",
18+
=> permissionsCommandHandler.ExecuteAsync(
19+
new SlashCommandParseResult(
20+
true,
21+
"permissions",
22+
command.Arguments.Length == 0
23+
? ["approvals", "show"]
24+
: ["approvals", .. command.Arguments]),
5225
context,
5326
cancellationToken);
54-
}
55-
56-
private async Task<int> RenderAsync(
57-
string message,
58-
CommandExecutionContext context,
59-
CancellationToken cancellationToken,
60-
bool success = true)
61-
{
62-
await outputRendererDispatcher.RenderCommandResultAsync(
63-
new CommandResult(success, success ? 0 : 1, context.OutputFormat, message, null),
64-
context.OutputFormat,
65-
cancellationToken).ConfigureAwait(false);
66-
return success ? 0 : 1;
67-
}
68-
69-
private static int? ParseBudget(string value)
70-
=> int.TryParse(value, out var parsed) && parsed > 0
71-
? parsed
72-
: throw new InvalidOperationException($"Invalid auto-approve budget '{value}'.");
7327
}

0 commit comments

Comments
 (0)