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
1 change: 1 addition & 0 deletions docs/WINDOWS_NODE_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Local MCP clients also see MCP-only `app.*` commands such as `app.navigate`, `ap
- **URL Validation**: Canvas blocks `file://`, `javascript:`, localhost, private IPs, IPv6 localhost
- **Screen Capture Notification**: User is notified when screen snapshots are captured
- **Screen Recording Allowlist**: `screen.record` must be explicitly allowed by the gateway and does not leave a hidden local MP4 copy on Windows
- **Session Attribution**: Only the optional top-level `sessionKey` stamped by the Gateway on `node.invoke.request` is trusted. Older Gateways omit it, so those invokes remain unattributed; a caller-supplied nested `args.sessionKey` is never used as a fallback.
- **Command Center Redaction**: recent node invoke activity records command name, status, duration, node id, and privacy class only; it does not store base64 payloads, screenshots, recordings, tokens, or command arguments
- **Node Mode Toggle**: Must be explicitly enabled by user
- **Command Validation**: Only alphanumeric commands with dots/hyphens allowed
Expand Down
3 changes: 1 addition & 2 deletions src/OpenClaw.Shared/Capabilities/SystemCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ private NodeInvokeResponse HandleRunPrepare(NodeInvokeRequest request)
var validated = validation.Request!;
var argv = validated.Argv;
var rawCommand = GetStringArg(request.Args, "rawCommand");
var sessionKey = request.SessionKey ?? validated.SessionKey;

Logger.Info(
$"system.run.prepare: {rawCommand ?? FormatExecCommand(argv)} (cwd={validated.Cwd ?? "default"})");
Expand All @@ -220,7 +219,7 @@ private NodeInvokeResponse HandleRunPrepare(NodeInvokeRequest request)
cwd = validated.Cwd,
rawCommand,
agentId = validated.AgentId,
sessionKey
sessionKey = validated.SessionKey
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static ExecApprovalV2ValidationOutcome Validate(NodeInvokeRequest request
timeoutMs,
env,
TryGetString(request.Args, "agentId"),
TryGetString(request.Args, "sessionKey")));
request.SessionKey));
}

private static ExecApprovalV2ValidationOutcome Deny(string reason)
Expand Down
5 changes: 4 additions & 1 deletion src/OpenClaw.Shared/NodeCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public class NodeInvokeRequest
public string Id { get; set; } = "";
public string Command { get; set; } = "";
public JsonElement Args { get; set; }
public string? SessionKey { get; set; }

/// <summary>Gateway-stamped run correlation; capability callers may read but not assign it.</summary>
[JsonIgnore]
public string? SessionKey { get; internal set; }

[JsonIgnore]
public NodeToolInvocation? Telemetry { get; set; }
Expand Down
34 changes: 13 additions & 21 deletions src/OpenClaw.Shared/WindowsNodeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ private async Task HandleEventAsync(JsonElement root)
await HandlePairingResolvedEventAsync(root, eventType);
break;
case "node.invoke.request":
await HandleNodeInvokeEventAsync(root);
await StartNodeInvokeEventAsync(root.Clone());
break;
case "node.invoke.cancel":
await HandleNodeInvokeCancelAsync(root, "payload", responseId: null);
Expand Down Expand Up @@ -422,7 +422,7 @@ private async Task HandlePairingResolvedEventAsync(JsonElement root, string? eve
}
}

private async Task HandleNodeInvokeEventAsync(JsonElement root)
private async Task StartNodeInvokeEventAsync(JsonElement root)
{
var telemetry = new NodeToolInvocation(NodeToolTransport.Gateway);
_logger.Info("[NODE] Received node.invoke.request event");
Expand Down Expand Up @@ -508,7 +508,7 @@ await SendGatewayResultAndCompleteTelemetryAsync(
}
}

var sessionKey = ExtractNodeInvokeSessionKey(payload, args);
var sessionKey = ExtractGatewayNodeInvokeSessionKey(payload);

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

Expand Down Expand Up @@ -1183,16 +1183,14 @@ await SendGatewayResultAndCompleteTelemetryAsync(
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,
SessionKey = sessionKey,
SessionKey = ExtractGatewayNodeInvokeSessionKey(paramsEl),
Telemetry = telemetry
};

Expand Down Expand Up @@ -1569,25 +1567,19 @@ private static bool TryGetCancellationTargetId(JsonElement container, out string
return false;
}

private static string? ExtractNodeInvokeSessionKey(JsonElement envelope, JsonElement args)
private static string? ExtractGatewayNodeInvokeSessionKey(JsonElement envelope)
{
if (envelope.TryGetProperty("sessionKey", out var envelopeSessionKey) &&
envelopeSessionKey.ValueKind == JsonValueKind.String)
if (envelope.TryGetProperty("sessionKey", out var envelopeSessionKey))
{
var sessionKey = envelopeSessionKey.GetString();
if (!string.IsNullOrWhiteSpace(sessionKey))
return sessionKey;
}
if (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;
}

return null;
}

Expand Down
4 changes: 3 additions & 1 deletion tests/OpenClaw.Shared.Tests/CapabilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ public async Task RunPrepare_ReturnsPlan_WithArgvAndCwd()
{
Id = "p3",
Command = "system.run.prepare",
Args = Parse("""{"command":["ls","-la"],"cwd":"/tmp","agentId":"agent1","sessionKey":"sk1"}""")
Args = Parse("""{"command":["ls","-la"],"cwd":"/tmp","agentId":"agent1","sessionKey":"spoofed"}"""),
SessionKey = "trusted-session"
};

var res = await cap.ExecuteAsync(req);
Expand All @@ -254,6 +255,7 @@ public async Task RunPrepare_ReturnsPlan_WithArgvAndCwd()
Assert.Equal("/tmp", cwd.GetString());
Assert.True(plan.TryGetProperty("agentId", out var agentId));
Assert.Equal("agent1", agentId.GetString());
Assert.Equal("trusted-session", plan.GetProperty("sessionKey").GetString());
}

[Fact]
Expand Down
18 changes: 12 additions & 6 deletions tests/OpenClaw.Shared.Tests/ExecApprovalV2InputValidationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ private static JsonElement Parse(string json)
return doc.RootElement.Clone();
}

private static NodeInvokeRequest Req(string argsJson)
=> new() { Id = "r1", Command = "system.run", Args = Parse(argsJson) };
private static NodeInvokeRequest Req(string argsJson, string? sessionKey = null)
=> new()
{
Id = "r1",
Command = "system.run",
Args = Parse(argsJson),
SessionKey = sessionKey
};

// -------------------------------------------------------------------------
// Allow paths
Expand Down Expand Up @@ -62,9 +68,9 @@ public void Valid_AllOptionalFields_Parsed()
"cwd": "C:\\repo",
"timeoutMs": 5000,
"agentId": "agent-1",
"sessionKey": "sess-abc"
"sessionKey": "forged-session"
}
"""));
""", sessionKey: "sess-abc"));

Assert.True(outcome.IsValid);
var r = outcome.Request!;
Expand Down Expand Up @@ -446,10 +452,10 @@ public void AgentIdWrongType_TreatedAsAbsent()
}

[Fact]
public void SessionKeyWrongType_TreatedAsAbsent()
public void ArgsSessionKey_IsIgnored()
{
var outcome = ExecApprovalV2InputValidator.Validate(
Req("""{"command":["echo"],"sessionKey":[]}"""));
Req("""{"command":["echo"],"sessionKey":"forged-session"}"""));

Assert.True(outcome.IsValid);
Assert.Null(outcome.Request!.SessionKey);
Expand Down
Loading