[codex] Add governed external CLI connectors#102
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a governed “External CLI” connector framework to OpenClaw.NET, enabling named allowlisted command execution via official platform CLIs with preview/fingerprints, redaction, bounded execution, and audit/runtime event emission (disabled by default).
Changes:
- Introduces
ExternalCliOptions/connector/command models plus a registry+runner for safe template expansion, validation, redaction, fingerprinting, and structured output parsing. - Wires the feature into the agent tool surface (
external_cli), gateway admin APIs, audit/runtime event sinks, and client/CLI commands. - Adds documentation and targeted tests for policy enforcement, parsing, truncation, and approval gating.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/OpenClaw.Tests/ExternalCliTests.cs | Adds tests covering preview policy checks, template expansion, JSON parsing, truncation, and approval gating. |
| src/OpenClaw.Gateway/ExternalCliStores.cs | Adds gateway-backed audit (jsonl) and runtime event sinks for external CLI activity. |
| src/OpenClaw.Gateway/Endpoints/EndpointHelpers.cs | Extends role mapping to recognize admin.external-cli.execute scope. |
| src/OpenClaw.Gateway/Endpoints/AdminEndpoints.Support.cs | Extends admin endpoint service container to include external CLI registry/runner/audit/events. |
| src/OpenClaw.Gateway/Endpoints/AdminEndpoints.ExternalCli.cs | Adds admin endpoints for listing connectors/commands and for preview/execute flows with audit/events. |
| src/OpenClaw.Gateway/Endpoints/AdminEndpoints.cs | Wires external CLI services into admin endpoint initialization and maps the new endpoints. |
| src/OpenClaw.Gateway/Composition/SecurityServicesExtensions.cs | Registers IExternalCliAuditSink and IExternalCliEventSink implementations. |
| src/OpenClaw.Gateway/Composition/RuntimeInitializationExtensions.RuntimeFactories.cs | Registers the ExternalCliTool when ExternalCli.Enabled is true. |
| src/OpenClaw.Gateway/Composition/RuntimeInitializationExtensions.CompositionStages.cs | Adds external CLI services to resolved runtime services. |
| src/OpenClaw.Gateway/Composition/CoreServicesExtensions.cs | Registers IExternalCliConnectorRegistry and IExternalCliRunner in DI. |
| src/OpenClaw.Gateway/appsettings.json | Adds an OpenClaw:ExternalCli section with conservative, disabled-by-default defaults. |
| src/OpenClaw.Core/Validation/ConfigValidator.cs | Adds config validation for key external CLI option constraints. |
| src/OpenClaw.Core/Pipeline/ToolActionPolicyResolver.cs | Adds external_cli action-aware resolution and flags it as action-aware approval-capable. |
| src/OpenClaw.Core/Models/ToolingPolicyModels.cs | Extends ToolActionDescriptor to include approval/fingerprint/risk/readonly metadata. |
| src/OpenClaw.Core/Models/Session.cs | Adds JSON source-gen registrations for external CLI request/response/audit/event models. |
| src/OpenClaw.Core/Models/GatewayConfig.cs | Adds ExternalCli to the gateway config model. |
| src/OpenClaw.Core/Models/ExternalCliModels.cs | Introduces external CLI config + API request/response + audit/event models. |
| src/OpenClaw.Core/ExternalCli/ExternalCliServices.cs | Implements registry/runner, template expansion, parameter validation, redaction, fingerprinting, and output parsing. |
| src/OpenClaw.Core/Abstractions/IToolActionDescriptorProvider.cs | Introduces a tool interface for action-aware descriptors used by the executor. |
| src/OpenClaw.Client/OpenClawHttpClient.cs | Adds client methods/URIs for external CLI admin endpoints. |
| src/OpenClaw.Cli/Program.cs | Adds openclaw external ... command routing. |
| src/OpenClaw.Cli/OpenClawHttpClient.cs | Exposes external CLI client calls through the CLI wrapper client. |
| src/OpenClaw.Cli/ExternalCliCommands.cs | Implements openclaw external list/status/commands/preview/execute commands. |
| src/OpenClaw.Cli/CliArgs.cs | Adds --dry-run to recognized CLI flags. |
| src/OpenClaw.Agent/Tools/ExternalCliTool.cs | Adds external_cli tool implementation with preview/execute and audit/events. |
| src/OpenClaw.Agent/OpenClawToolExecutor.cs | Uses tool-provided action descriptors and adds post-approval fingerprint stability checks. |
| docs/SITE_MAP.md | Adds External CLI Connectors guide entry. |
| docs/README.md | Links the new External CLI Connectors documentation. |
| docs/EXTERNAL_CLI_CONNECTORS.md | Adds documentation for security model, config, approvals, audit/events, parsing, admin API, and CLI. |
| scope.StartsWith("admin.channels.auth.mutate", StringComparison.Ordinal) || | ||
| scope.StartsWith("admin.channels.auth.restart", StringComparison.Ordinal) || | ||
| scope.StartsWith("admin.models.evaluate", StringComparison.Ordinal) || | ||
| scope.StartsWith("admin.external-cli.execute", StringComparison.Ordinal) || |
| catch (InvalidOperationException ex) | ||
| { | ||
| events.Record(new ExternalCliRuntimeEvent | ||
| { | ||
| Action = "command_blocked_by_policy", | ||
| Severity = "warning", | ||
| Summary = ex.Message | ||
| }); | ||
| return Results.BadRequest(new OperationStatusResponse { Success = false, Error = ex.Message }); | ||
| } |
| catch (InvalidOperationException ex) | ||
| { | ||
| events.Record(new ExternalCliRuntimeEvent | ||
| { | ||
| Action = "command_blocked_by_policy", | ||
| Severity = "warning", | ||
| Summary = ex.Message | ||
| }); | ||
| return Results.BadRequest(new OperationStatusResponse { Success = false, Error = ex.Message }); | ||
| } |
| var fingerprint = ComputeFingerprint( | ||
| connectorName, | ||
| commandName, | ||
| connector.Executable, | ||
| args, | ||
| workingDirectory, | ||
| timeout, | ||
| outputFormat, | ||
| dryRun, | ||
| command); | ||
| var parametersHash = ComputeParametersHash(request.Parameters); |
| Preview returns: | ||
|
|
||
| - resolved executable and argument list | ||
| - redacted command-line preview | ||
| - risk level | ||
| - read-only or mutating classification | ||
| - whether approval is required | ||
| - output format | ||
| - required identity or scopes, when configured |
| foreach (var (commandName, command) in connector.Commands) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(commandName)) | ||
| errors.Add($"ExternalCli.Connectors.{connectorName}.Commands contains an empty command name."); | ||
| if (command.ArgsTemplate.Length == 0) | ||
| errors.Add($"ExternalCli.Connectors.{connectorName}.Commands.{commandName}.ArgsTemplate must contain at least one argument."); | ||
| if (command.SupportsDryRun && command.DryRunArgsTemplate.Length == 0) | ||
| errors.Add($"ExternalCli.Connectors.{connectorName}.Commands.{commandName}.DryRunArgsTemplate must be set when SupportsDryRun=true."); | ||
| if (command.TimeoutSeconds is <= 0) | ||
| errors.Add($"ExternalCli.Connectors.{connectorName}.Commands.{commandName}.TimeoutSeconds must be >= 1 when set."); | ||
|
|
||
| var risk = ExternalCliRiskLevel.Normalize(command.RiskLevel); | ||
| if (!string.Equals(risk, command.RiskLevel, StringComparison.OrdinalIgnoreCase)) | ||
| errors.Add($"ExternalCli.Connectors.{connectorName}.Commands.{commandName}.RiskLevel must be low, medium, or high."); | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(command.StructuredOutput)) | ||
| { | ||
| var format = ExternalCliOutputFormat.Normalize(command.StructuredOutput); | ||
| if (!string.Equals(format, command.StructuredOutput, StringComparison.OrdinalIgnoreCase)) | ||
| errors.Add($"ExternalCli.Connectors.{connectorName}.Commands.{commandName}.StructuredOutput must be one of: json, ndjson, csv, text, table."); | ||
| } |
| openclaw external list [--json] | ||
| openclaw external status <connector> [--json] | ||
| openclaw external commands <connector> [--json] | ||
| openclaw external preview <connector> <command> [--param key=value]... [--dry-run] [--json] | ||
| openclaw external execute <connector> <command> [--param key=value]... [--yes] [--json] | ||
|
|
||
| Notes: | ||
| - Commands talk to the gateway admin API. | ||
| - Mutating or high-risk commands require --yes after preview review. | ||
| - External CLI connectors are disabled unless OpenClaw:ExternalCli:Enabled=true. | ||
| """); |
| string? version = null; | ||
| if (resolvedExecutable is not null && connector.VersionCommand is { Args.Length: > 0 } versionCommand) | ||
| version = await RunStatusCommandAsync(resolvedExecutable, versionCommand, connector, ct); | ||
|
|
||
| string authStatus = "unknown"; | ||
| bool? authenticated = null; | ||
| if (resolvedExecutable is not null && connector.StatusCommand is { Args.Length: > 0 } statusCommand) |
| var request = JsonSerializer.Deserialize( | ||
| string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson, | ||
| CoreJsonContext.Default.ExternalCliToolRequest) ?? new ExternalCliToolRequest(); | ||
| if (!string.Equals(request.Action, "execute", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return new ToolActionDescriptor | ||
| { | ||
| Action = request.Action, | ||
| IsMutation = false, | ||
| RequiresApproval = false, | ||
| Summary = BuildSummary(request) | ||
| }; | ||
| } |
🤖 Augment PR SummarySummary: Adds a disabled-by-default, governed “external_cli” tool for invoking allowlisted platform CLIs via named connectors/commands. Changes:
Technical Notes: Uses 🤖 Was this summary useful? React with 👍 or 👎 |
| "connector":{"type":"string","description":"Configured connector name, such as gh, az, kubectl, stripe, or lark."}, | ||
| "command":{"type":"string","description":"Allowlisted command name inside the connector."}, | ||
| "parameters":{"type":"object","additionalProperties":true,"description":"Named command parameters. Unknown parameters are rejected unless the command explicitly allows them."}, | ||
| "execute_dry_run":{"type":"boolean","default":false,"description":"For preview only: execute the explicit dry-run template when the command supports it."}, |
There was a problem hiding this comment.
ExternalCliTool.ParameterSchema documents snake_case fields like execute_dry_run/approved_fingerprint/approval_reason, but ExternalCliToolRequest is deserialized with the core JSON context’s camelCase policy, so these values likely won’t bind (e.g., request.ExecuteDryRun will stay false even if the caller sends execute_dry_run).
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| ArgsHash = ExternalCliConnectorRegistry.ComputeArgsHash(result.Preview.Arguments.ToArray()), | ||
| RedactedArgsPreview = result.Preview.RedactedCommandLine, | ||
| ParametersHash = result.Preview.ParametersHash, | ||
| ApprovalFingerprint = approvalFingerprint, |
There was a problem hiding this comment.
ExternalCliAuditEntry.ApprovalFingerprint is recorded from the request-provided approval fingerprint, so executions that don’t populate it (e.g., dry-run previews, or agent tool calls that rely on the tool-approval flow) won’t store the actual result.Preview.Fingerprint even though it’s available for auditing.
Severity: medium
Other Locations
src/OpenClaw.Agent/Tools/ExternalCliTool.cs:178
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
|
|
||
| foreach (var required in placeholderNames) | ||
| { | ||
| if (!parameters.ContainsKey(required)) |
There was a problem hiding this comment.
Parameter placeholder/allowed-name sets are computed case-insensitively, but lookups use parameters.ContainsKey/TryGetValue which will be case-sensitive for typical request dictionaries; this can lead to confusing failures where a key like Repo passes the allowlist check but is then treated as “missing” for {{repo}}.
Severity: medium
Other Locations
src/OpenClaw.Core/ExternalCli/ExternalCliServices.cs:367
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| new OperationStatusResponse | ||
| { | ||
| Success = false, | ||
| Error = "External CLI command requires approval. Preview the command and resubmit with the matching approved_fingerprint." |
There was a problem hiding this comment.
This error message tells clients to resubmit approved_fingerprint, but the JSON request model/serialization here is camelCase (e.g., approvedFingerprint); this mismatch may cause unnecessary client confusion when handling 409 responses.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| foreach (var required in placeholderNames) | ||
| { | ||
| if (!parameters.ContainsKey(required)) | ||
| throw new InvalidOperationException($"External CLI parameter '{required}' is required."); | ||
| } |
| foreach (var key in parameters.Keys) | ||
| { | ||
| if (!allowedNames.Contains(key)) | ||
| throw new InvalidOperationException($"External CLI parameter '{key}' is not allowed for this command."); | ||
| } |
| foreach (var candidate in candidates) | ||
| { | ||
| var fullPath = Path.Combine(dir, candidate); | ||
| if (File.Exists(fullPath)) | ||
| return fullPath; | ||
| } |
| foreach (var pattern in redactionRules) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(pattern)) | ||
| continue; | ||
|
|
||
| try | ||
| { | ||
| current = Regex.Replace( | ||
| current, | ||
| pattern, | ||
| "[REDACTED:external-cli]", | ||
| RegexOptions.CultureInvariant, | ||
| TimeSpan.FromMilliseconds(250)); | ||
| } | ||
| catch (ArgumentException) | ||
| { | ||
| // Invalid config is reported by startup validation; keep redaction best-effort. | ||
| continue; | ||
| } | ||
| } |
| foreach (var pattern in redactionRules) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(pattern)) | ||
| continue; | ||
| try | ||
| { | ||
| current = Regex.Replace( | ||
| current, | ||
| pattern, | ||
| "[REDACTED:external-cli]", | ||
| RegexOptions.CultureInvariant, | ||
| TimeSpan.FromMilliseconds(250)); | ||
| } | ||
| catch (ArgumentException) | ||
| { | ||
| // Invalid config is reported by startup validation; keep redaction best-effort. | ||
| continue; | ||
| } | ||
| } |
Summary
Closes #100.
Validation