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
46 changes: 38 additions & 8 deletions src/OpenClaw.Chat/ChatModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,33 @@ public enum ChatTimelineItemKind
/// </remarks>
public enum ChatPermissionDecision
{
Pending,
Allowed,
Denied,
Expired
Pending = 0,
Allowed = 1,
Denied = 2,
Expired = 3,
AllowedAlways = 4
}

public static class ChatPermissionActionKeys
{
public const string AllowOnce = "allow-once";
public const string AllowAlways = "allow-always";
public const string Deny = "deny";

public static readonly string[] ExecApprovalDefaults = [AllowOnce, AllowAlways, Deny];

public static string[] NormalizeActions(IReadOnlyList<string>? actions)
{
if (actions is not { Count: > 0 })
return ExecApprovalDefaults;

var normalized = actions
.Where(action => !string.IsNullOrWhiteSpace(action))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();

return normalized.Length > 0 ? normalized : ExecApprovalDefaults;
}
}

public enum ChatToolCallStatus
Expand Down Expand Up @@ -103,9 +126,10 @@ public record ChatTimelineItem(
ChatTone? Tone = null,
string? ToolCallId = null,
string? PermissionRequestId = null,
ChatPermissionDecision PermissionDecision = ChatPermissionDecision.Pending);
ChatPermissionDecision PermissionDecision = ChatPermissionDecision.Pending,
IReadOnlyList<string>? PermissionActions = null);

public record ChatPermissionRequest(string RequestId, string PermissionKind, string ToolName, string Detail);
public record ChatPermissionRequest(string RequestId, string PermissionKind, string ToolName, string Detail, IReadOnlyList<string>? Actions = null);

public record ChatTimelineState(
System.Collections.Immutable.ImmutableList<ChatTimelineItem> Entries,
Expand Down Expand Up @@ -152,7 +176,7 @@ public record ChatContextChangedEvent(string? Cwd, string? GitBranch) : ChatEven
public record ChatStatusEvent(string Text, ChatTone Tone) : ChatEvent;
public record ChatErrorEvent(string Text) : ChatEvent;
public record ChatRestoredEvent(string Text) : ChatEvent;
public record ChatPermissionRequestEvent(string RequestId, string PermissionKind, string ToolName, string Detail) : ChatEvent;
public record ChatPermissionRequestEvent(string RequestId, string PermissionKind, string ToolName, string Detail, IReadOnlyList<string>? Actions = null) : ChatEvent;
public record ChatModelChangedEvent(string Model) : ChatEvent;
public record ChatRawEvent(string EventType, string? Text = null) : ChatEvent;

Expand Down Expand Up @@ -231,5 +255,11 @@ Task SendMessageAsync(string threadId, string message, CancellationToken cancell
Task SetModelAsync(string threadId, string model, CancellationToken cancellationToken = default);
Task SetThinkingLevelAsync(string threadId, string thinkingLevel, CancellationToken cancellationToken = default);
Task SetPermissionModeAsync(string threadId, bool allowAll, CancellationToken cancellationToken = default);
Task RespondToPermissionAsync(string threadId, string requestId, bool allow, CancellationToken cancellationToken = default);
Task RespondToPermissionAsync(string threadId, string requestId, string action, CancellationToken cancellationToken = default);
Task RespondToPermissionAsync(string threadId, string requestId, bool allow, CancellationToken cancellationToken = default) =>
RespondToPermissionAsync(
threadId,
requestId,
allow ? ChatPermissionActionKeys.AllowOnce : ChatPermissionActionKeys.Deny,
cancellationToken);
}
5 changes: 3 additions & 2 deletions src/OpenClaw.Chat/ChatTimelineReducer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,14 @@ static ChatTimelineState ApplyPermissionRequest(ChatTimelineState state, ChatPer
ToolName: e.ToolName,
IntentSummary: e.PermissionKind,
PermissionRequestId: e.RequestId,
PermissionDecision: ChatPermissionDecision.Pending);
PermissionDecision: ChatPermissionDecision.Pending,
PermissionActions: e.Actions);

return state with
{
Entries = entries.Add(entry),
NextId = state.NextId + 1,
PendingPermission = new ChatPermissionRequest(e.RequestId, e.PermissionKind, e.ToolName, detail)
PendingPermission = new ChatPermissionRequest(e.RequestId, e.PermissionKind, e.ToolName, detail, e.Actions)
};
}

Expand Down
13 changes: 9 additions & 4 deletions src/OpenClaw.Shared/Capabilities/SystemCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ private NodeInvokeResponse HandleRunPrepare(NodeInvokeRequest request)
var rawCommand = GetStringArg(request.Args, "rawCommand");
var cwd = GetStringArg(request.Args, "cwd");
var agentId = GetStringArg(request.Args, "agentId");
var sessionKey = GetStringArg(request.Args, "sessionKey");
var sessionKey = request.SessionKey ?? GetStringArg(request.Args, "sessionKey");

Logger.Info($"system.run.prepare: {rawCommand} (cwd={cwd ?? "default"})");

Expand Down Expand Up @@ -357,6 +357,7 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)

var shell = GetStringArg(request.Args, "shell");
var cwd = GetStringArg(request.Args, "cwd");
var sessionKey = request.SessionKey ?? GetStringArg(request.Args, "sessionKey");
var timeoutMs = GetIntArg(request.Args, "timeoutMs",
GetIntArg(request.Args, "timeout", DefaultRunTimeoutMs));
// Clamp caller-supplied timeouts. timeoutMs <= 0 historically meant
Expand Down Expand Up @@ -406,7 +407,7 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
if (_approvalPolicy != null)
{
var approval = _approvalPolicy.Evaluate(fullCommand, shell);
var approvalCheck = await EnsureApprovedAsync(fullCommand, shell, approval);
var approvalCheck = await EnsureApprovedAsync(fullCommand, shell, approval, sessionKey, correlationId);
if (!approvalCheck.Allowed)
{
Logger.Warn($"system.run DENIED: {fullCommand} ({approval.Reason})");
Expand Down Expand Up @@ -437,7 +438,7 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
continue;
}

var innerApprovalCheck = await EnsureApprovedAsync(target.Command, target.Shell, innerApproval);
var innerApprovalCheck = await EnsureApprovedAsync(target.Command, target.Shell, innerApproval, sessionKey, correlationId);
if (!innerApprovalCheck.Allowed)
{
Logger.Warn($"system.run DENIED: {target.Command} ({innerApproval.Reason})");
Expand Down Expand Up @@ -478,6 +479,8 @@ private async Task<ExecApprovalCheckResult> EnsureApprovedAsync(
string command,
string? shell,
ExecApprovalResult approval,
string? sessionKey,
string correlationId,
CancellationToken cancellationToken = default)
{
if (approval.Allowed)
Expand All @@ -494,7 +497,9 @@ private async Task<ExecApprovalCheckResult> EnsureApprovedAsync(
Command = command,
Shell = shell,
MatchedPattern = approval.MatchedPattern,
Reason = approval.Reason ?? "Command requires approval"
Reason = approval.Reason ?? "Command requires approval",
SessionKey = sessionKey,
CorrelationId = correlationId
}, cancellationToken);

if (decision.Kind == ExecApprovalPromptDecisionKind.Deny)
Expand Down
21 changes: 16 additions & 5 deletions src/OpenClaw.Shared/ExecApprovalPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,28 @@ public enum ExecApprovalAction

/// <summary>
/// JsonConverter for <see cref="ExecApprovalAction"/> that emits/accepts the canonical
/// camelCase values ("allow", "deny", "prompt") but also accepts the legacy "ask" alias.
/// Older builds of the Permissions UI wrote "ask" for the Prompt action; without this
/// converter, deserialization would throw and the entire policy file (including any
/// user-authored rules) would be silently replaced with the default policy on load.
/// camelCase values ("allow", "deny", "prompt") but also accepts legacy values written
/// by older builds: the "ask" alias and numeric enum values.
/// </summary>
internal sealed class ExecApprovalActionConverter : JsonConverter<ExecApprovalAction>
{
public override ExecApprovalAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
if (!reader.TryGetInt32(out var numericValue))
throw new JsonException("Expected integer value for ExecApprovalAction");

return numericValue switch
{
(int)ExecApprovalAction.Allow => ExecApprovalAction.Allow,
(int)ExecApprovalAction.Deny => ExecApprovalAction.Deny,
(int)ExecApprovalAction.Prompt => ExecApprovalAction.Prompt,
_ => throw new JsonException($"Unknown ExecApprovalAction numeric value '{numericValue}'")
};
}
if (reader.TokenType != JsonTokenType.String)
throw new JsonException($"Expected string for ExecApprovalAction, got {reader.TokenType}");
throw new JsonException($"Expected string or number for ExecApprovalAction, got {reader.TokenType}");

var value = reader.GetString();
return value?.ToLowerInvariant() switch
Expand Down
2 changes: 2 additions & 0 deletions src/OpenClaw.Shared/ExecApprovalPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public sealed class ExecApprovalPromptRequest
public string? Shell { get; init; }
public string? MatchedPattern { get; init; }
public string Reason { get; init; } = "";
public string? SessionKey { get; init; }
public string? CorrelationId { get; init; }
}

public sealed class ExecApprovalPromptDecision
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Shared/NodeCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class NodeInvokeRequest
public string Id { get; set; } = "";
public string Command { get; set; } = "";
public JsonElement Args { get; set; }
public string? SessionKey { get; set; }
}

public class NodeInvokeCompletedEventArgs : EventArgs
Expand Down
31 changes: 29 additions & 2 deletions src/OpenClaw.Shared/WindowsNodeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ private async Task HandleNodeInvokeEventAsync(JsonElement root)
}
}
}

var sessionKey = ExtractNodeInvokeSessionKey(payload, args);

_logger.Info($"[NODE] Invoking command: {command}");

Expand All @@ -489,7 +491,8 @@ private async Task HandleNodeInvokeEventAsync(JsonElement root)
{
Id = requestId,
Command = command,
Args = args
Args = args,
SessionKey = sessionKey
};

// Find capability that can handle this command
Expand Down Expand Up @@ -1041,14 +1044,16 @@ private async Task HandleNodeInvokeAsync(JsonElement root, string? requestId)
var args = paramsEl.TryGetProperty("args", out var argsEl)
? argsEl.Clone()
: default;
var sessionKey = ExtractNodeInvokeSessionKey(paramsEl, args);

_logger.Info($"Received node.invoke: {command}");

var request = new NodeInvokeRequest
{
Id = requestId,
Command = command,
Args = args
Args = args,
SessionKey = sessionKey
};

// Find capability that can handle this command
Expand Down Expand Up @@ -1109,6 +1114,28 @@ private async Task HandleNodeInvokeAsync(JsonElement root, string? requestId)
}, CancellationToken.None);
}

private static string? ExtractNodeInvokeSessionKey(JsonElement envelope, JsonElement args)
{
if (envelope.TryGetProperty("sessionKey", out var envelopeSessionKey) &&
envelopeSessionKey.ValueKind == JsonValueKind.String)
{
var sessionKey = envelopeSessionKey.GetString();
if (!string.IsNullOrWhiteSpace(sessionKey))
return sessionKey;
}

if (args.ValueKind == JsonValueKind.Object &&
args.TryGetProperty("sessionKey", out var argsSessionKey) &&
argsSessionKey.ValueKind == JsonValueKind.String)
{
var sessionKey = argsSessionKey.GetString();
if (!string.IsNullOrWhiteSpace(sessionKey))
return sessionKey;
}

return null;
}

private void RaiseInvokeCompleted(string requestId, string command, bool ok, string? error, TimeSpan duration)
{
InvokeCompleted?.Invoke(this, new NodeInvokeCompletedEventArgs
Expand Down
5 changes: 3 additions & 2 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1756,8 +1756,9 @@ private void OnManagerStateChanged(object? sender, GatewayConnectionSnapshot sna
new AppLogger(),
_dispatcherQueue,
DataPath,
() => _keepAliveWindow?.Content as FrameworkElement,
settings,
rootProvider: () => _keepAliveWindow?.Content as FrameworkElement,
chatProviderProvider: () => _chatCoordinator?.Provider,
settings: settings,
enableMcpServer: settings.EnableMcpServer,
identityDataPath: IdentityDataPath,
sharedGatewayTokenResolver: () => _gatewayRegistry?.GetActive()?.SharedGatewayToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public Task SendMessageAsync(string threadId, string message, CancellationToken
public Task SetThinkingLevelAsync(string threadId, string thinkingLevel, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task SetPermissionModeAsync(string threadId, bool allowAll, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task RespondToPermissionAsync(string threadId, string requestId, bool allow, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task RespondToPermissionAsync(string threadId, string requestId, string action, CancellationToken cancellationToken = default) => Task.CompletedTask;

public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Loading
Loading