Skip to content

Commit bde72ea

Browse files
authored
Add governed external CLI connectors
* Add governed external CLI connectors * Address external CLI review feedback * Address additional external CLI review feedback * Address external CLI path validation comments
1 parent 8a5f010 commit bde72ea

29 files changed

Lines changed: 2966 additions & 7 deletions

docs/EXTERNAL_CLI_CONNECTORS.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# External CLI Connectors
2+
3+
External CLI connectors let OpenClaw.NET wrap official platform CLIs through a governed native tool named `external_cli`. The feature is disabled by default and is intentionally not a general-purpose shell.
4+
5+
Official CLIs provide platform depth. OpenClaw.NET provides the runtime controls: named command allowlists, risk scoring, previews, approvals, redaction, timeouts, audit records, and runtime events.
6+
7+
## Security Model
8+
9+
External CLIs can operate under powerful user, bot, cloud, cluster, or payment identities. Treat mutating commands as high-risk. Use least privilege, dry-run previews, approvals, and audit logs.
10+
11+
The connector does not accept arbitrary command strings. Agents call a configured connector and command name with named parameters:
12+
13+
```json
14+
{
15+
"action": "execute",
16+
"connector": "gh",
17+
"command": "issue_list",
18+
"parameters": {
19+
"repo": "clawdotnet/openclaw.net"
20+
}
21+
}
22+
```
23+
24+
The runtime expands configured argument templates directly into `ProcessStartInfo.ArgumentList`. It does not pass through a shell and it rejects missing or unknown parameters unless the specific command allows them.
25+
26+
## Configuration
27+
28+
`OpenClaw:ExternalCli:Enabled` must be true before the `external_cli` tool is registered. Each connector and each command must also be explicitly configured.
29+
30+
```json
31+
{
32+
"OpenClaw": {
33+
"ExternalCli": {
34+
"Enabled": true,
35+
"DefaultTimeoutSeconds": 60,
36+
"MaxStdoutBytes": 262144,
37+
"MaxStderrBytes": 65536,
38+
"RedactSecrets": true,
39+
"AllowFreeformCommands": false,
40+
"RequireApprovalForMutatingCommands": true,
41+
"Connectors": {
42+
"gh": {
43+
"Enabled": true,
44+
"DisplayName": "GitHub CLI",
45+
"Executable": "gh",
46+
"DefaultOutputFormat": "json",
47+
"StatusCommand": {
48+
"Args": [ "auth", "status" ],
49+
"TimeoutSeconds": 20
50+
},
51+
"VersionCommand": {
52+
"Args": [ "--version" ],
53+
"TimeoutSeconds": 10
54+
},
55+
"Commands": {
56+
"repo_view": {
57+
"Description": "View repository metadata",
58+
"ArgsTemplate": [ "repo", "view", "{{repo}}", "--json", "name,owner,description,url,isPrivate" ],
59+
"RiskLevel": "low",
60+
"ReadOnly": true,
61+
"StructuredOutput": "json",
62+
"Parameters": {
63+
"repo": {
64+
"Required": true,
65+
"Pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"
66+
}
67+
}
68+
},
69+
"issue_create": {
70+
"Description": "Create a GitHub issue",
71+
"ArgsTemplate": [ "issue", "create", "--repo", "{{repo}}", "--title", "{{title}}", "--body", "{{body}}" ],
72+
"RiskLevel": "medium",
73+
"ReadOnly": false,
74+
"RequiresApproval": true,
75+
"StructuredOutput": "text",
76+
"Parameters": {
77+
"repo": { "Required": true },
78+
"title": { "Required": true, "MaxLength": 200 },
79+
"body": { "Required": true, "MaxLength": 16000 }
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}
88+
```
89+
90+
Important defaults:
91+
92+
- `Enabled=false`: no tool registration.
93+
- `AllowFreeformCommands=false`: raw commands are rejected.
94+
- `RequireApprovalForMutatingCommands=true`: non-read-only commands require approval unless policy is changed.
95+
- `RiskLevel=high`: always approval-required.
96+
- `ReadOnly=false`: treated as mutating.
97+
98+
## Preview And Approval
99+
100+
Use preview before execution:
101+
102+
```bash
103+
openclaw external preview gh repo_view --param repo=clawdotnet/openclaw.net
104+
```
105+
106+
Preview returns:
107+
108+
- resolved executable and argument list
109+
- redacted command-line preview
110+
- risk level
111+
- read-only or mutating classification
112+
- whether approval is required
113+
- output format
114+
- required identity or scopes, when configured
115+
- a stable approval fingerprint
116+
117+
Preview does not execute the command unless `--dry-run` is supplied and the command has an explicit `DryRunArgsTemplate`. The runtime never guesses dry-run flags.
118+
119+
Approval-required admin execution must include the matching preview fingerprint. The CLI does this when `--yes` is supplied after preview review:
120+
121+
```bash
122+
openclaw external execute gh issue_create \
123+
--param repo=clawdotnet/openclaw.net \
124+
--param title="Example" \
125+
--param body="Example body" \
126+
--yes
127+
```
128+
129+
Agent tool calls use the existing OpenClaw tool approval path. If the command template, resolved args, or policy changes between approval and execution, execution is blocked by fingerprint mismatch.
130+
131+
## Audit, Events, And Redaction
132+
133+
External CLI execution writes an append-only audit record with:
134+
135+
- connector and command
136+
- executable
137+
- redacted argument preview
138+
- argument and parameter hashes
139+
- actor, session, channel, and sender where available
140+
- approval fingerprint where available
141+
- exit code, duration, timeout flag
142+
- stdout and stderr truncation flags
143+
- risk level and working directory
144+
145+
Runtime events are emitted for status checks, previews, dry-runs, execution, failures, timeouts, truncation, redaction, and policy blocks. Raw secrets are not stored in audit records.
146+
147+
Redaction applies to argument previews, stdout, stderr, audit records, runtime events, and error messages. Add connector or command `RedactionRules` for platform-specific token formats.
148+
149+
## Output Parsing
150+
151+
Set `StructuredOutput` per command:
152+
153+
- `json`: parse stdout as JSON and return parsed JSON alongside redacted stdout.
154+
- `ndjson`: parse newline-delimited JSON into a JSON array.
155+
- `csv`, `table`, `text`: returned as redacted text in the initial implementation.
156+
157+
The connector does not inject global `--json` flags. Put output flags in each command template.
158+
159+
## Conservative Presets
160+
161+
Keep presets disabled by default and enable only the commands needed for the operator surface.
162+
163+
### GitHub CLI
164+
165+
Read-only examples:
166+
167+
- `auth status`
168+
- `repo view`
169+
- `issue list`
170+
- `pr list`
171+
- `pr view`
172+
- `release list`
173+
174+
Approval-required examples:
175+
176+
- `issue create`
177+
- `issue comment`
178+
- `pr review`
179+
- `pr merge`
180+
- `release create`
181+
182+
### Azure CLI
183+
184+
Read-only examples:
185+
186+
- `account show`
187+
- `group list`
188+
- `resource list`
189+
- `webapp list`
190+
191+
Approval-required examples:
192+
193+
- resource create, update, or delete
194+
- deployment create
195+
- role assignment changes
196+
197+
### kubectl
198+
199+
Read-only examples:
200+
201+
- `config current-context`
202+
- `get pods -o json`
203+
- `get services -o json`
204+
- `get deployments -o json`
205+
- `describe` as text when approved by policy
206+
207+
High-risk approval-required examples:
208+
209+
- `apply`
210+
- `delete`
211+
- `scale`
212+
- `rollout restart`
213+
- `exec`
214+
- `port-forward`
215+
216+
Logs can expose secrets and should be at least medium risk.
217+
218+
### Stripe CLI
219+
220+
Read-only examples:
221+
222+
- listen status, if safe for your environment
223+
- fixtures/list, if applicable
224+
- customers/list, only with least-privilege credentials
225+
226+
High-risk approval-required examples:
227+
228+
- payment mutations
229+
- refunds
230+
- customer and subscription mutations
231+
- webhook trigger
232+
- event replay
233+
234+
### Lark / Feishu CLI
235+
236+
Read-only examples:
237+
238+
- `auth status`
239+
- schema inspection
240+
- calendar agenda
241+
- docs search/read
242+
- sheets read
243+
- mail search/read
244+
- meeting minutes query
245+
246+
Approval-required examples:
247+
248+
- send messages
249+
- write docs
250+
- write sheets
251+
- send email
252+
- approve or reject workflows
253+
- create or update OKRs
254+
- raw API calls
255+
256+
## Admin API
257+
258+
The gateway exposes:
259+
260+
- `GET /admin/external-cli/connectors`
261+
- `GET /admin/external-cli/connectors/{connector}`
262+
- `GET /admin/external-cli/connectors/{connector}/commands`
263+
- `POST /admin/external-cli/preview`
264+
- `POST /admin/external-cli/execute`
265+
266+
POST endpoints require CSRF for browser-session auth. Execute requires operator role and matching approval metadata for approval-required commands.
267+
268+
## CLI
269+
270+
```bash
271+
openclaw external list
272+
openclaw external status gh
273+
openclaw external commands gh
274+
openclaw external preview gh repo_view --param repo=clawdotnet/openclaw.net
275+
openclaw external execute gh repo_view --param repo=clawdotnet/openclaw.net
276+
```
277+
278+
Use `--json` for machine-readable responses.

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Use this page as the map. If you are unsure where to go next, the groups below a
2020
| [USER_GUIDE.md](USER_GUIDE.md) | Providers, tools, skills, channels, and the day-to-day operator surface. |
2121
| [RELEASES.md](RELEASES.md) | Desktop download bundles, release assets, and signing/notarization status. |
2222
| [TOOLS_GUIDE.md](TOOLS_GUIDE.md) | Native tool catalog, behavior, and configuration. |
23+
| [EXTERNAL_CLI_CONNECTORS.md](EXTERNAL_CLI_CONNECTORS.md) | Governed external CLI connectors, named command allowlists, approvals, redaction, and audit behavior. |
2324
| [plugins/payment.md](plugins/payment.md) | Native payment tool, virtual cards, machine payments, providers, and safe agent-facing actions. |
2425
| [cli/payment.md](cli/payment.md) | `openclaw payment ...` gateway-backed CLI commands and safe output contract. |
2526
| [mempalace-memory.md](mempalace-memory.md) | Optional ElBruno.MempalaceNet memory provider and temporal KG tool. |

docs/SITE_MAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Use this map when turning the Markdown docs into a documentation website. It kee
1212
| Overview | Getting Started | [GETTING_STARTED.md](GETTING_STARTED.md) |
1313
| Guides | User Guide | [USER_GUIDE.md](USER_GUIDE.md) |
1414
| Guides | Tools Guide | [TOOLS_GUIDE.md](TOOLS_GUIDE.md) |
15+
| Guides | External CLI Connectors | [EXTERNAL_CLI_CONNECTORS.md](EXTERNAL_CLI_CONNECTORS.md) |
1516
| Guides | Model Profiles | [MODEL_PROFILES.md](MODEL_PROFILES.md) |
1617
| Guides | Prompt Caching | [PROMPT_CACHING.md](PROMPT_CACHING.md) |
1718
| Reference | Compatibility | [COMPATIBILITY.md](COMPATIBILITY.md) |
@@ -52,6 +53,7 @@ Overview
5253
Guides
5354
User Guide
5455
Tools Guide
56+
External CLI Connectors
5557
Model Profiles
5658
Prompt Caching
5759

src/OpenClaw.Agent/OpenClawToolExecutor.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,16 +204,18 @@ public async Task<ToolExecutionResult> ExecuteAsync(
204204
}
205205
}
206206

207-
var approvalDescriptor = ToolActionPolicyResolver.Resolve(tool.Name, persistedArgsJson);
207+
var approvalDescriptor = ResolveToolActionDescriptor(tool, persistedArgsJson);
208208
var normalizedToolName = NormalizeApprovalToolName(tool.Name);
209209
var explicitlyConfiguredApproval = _config.Tooling.ApprovalRequiredTools
210210
.Any(item => string.Equals(NormalizeApprovalToolName(item), normalizedToolName, StringComparison.Ordinal));
211211
var presetRequiresApproval = preset?.ApprovalRequiredTools.Contains(tool.Name) == true;
212-
var defaultActionAwareApproval = ToolActionPolicyResolver.SupportsActionAwareApproval(tool.Name) && approvalDescriptor.IsMutation;
212+
var defaultActionAwareApproval = ToolActionPolicyResolver.SupportsActionAwareApproval(tool.Name)
213+
&& (approvalDescriptor.IsMutation || approvalDescriptor.RequiresApproval);
213214
var listedApproval = _requireToolApproval && (_approvalRequiredTools.Contains(normalizedToolName) || presetRequiresApproval);
214-
var requiresApproval = ToolActionPolicyResolver.SupportsActionAwareApproval(tool.Name) && !explicitlyConfiguredApproval && !presetRequiresApproval
215+
var requiresApproval = approvalDescriptor.RequiresApproval ||
216+
(ToolActionPolicyResolver.SupportsActionAwareApproval(tool.Name) && !explicitlyConfiguredApproval && !presetRequiresApproval
215217
? defaultActionAwareApproval
216-
: listedApproval || defaultActionAwareApproval;
218+
: listedApproval || defaultActionAwareApproval);
217219

218220
if (requiresApproval)
219221
{
@@ -256,6 +258,24 @@ public async Task<ToolExecutionResult> ExecuteAsync(
256258
}
257259
}
258260

261+
if (requiresApproval && !string.IsNullOrWhiteSpace(approvalDescriptor.ApprovalFingerprint))
262+
{
263+
var currentDescriptor = ResolveToolActionDescriptor(tool, persistedArgsJson);
264+
if (!string.Equals(currentDescriptor.ApprovalFingerprint, approvalDescriptor.ApprovalFingerprint, StringComparison.Ordinal))
265+
{
266+
var message = $"Tool '{tool.Name}' changed after approval was requested; execution blocked.";
267+
return CreateImmediateResult(
268+
toolName,
269+
persistedArgsJson,
270+
message,
271+
callId: callId,
272+
resultStatus: ToolResultStatuses.Blocked,
273+
failureCode: ToolFailureCodes.ApprovalRequired,
274+
failureMessage: message,
275+
nextStep: "Preview the command again and request approval for the updated fingerprint.");
276+
}
277+
}
278+
259279
var sw = Stopwatch.StartNew();
260280
string result;
261281
string resultStatus = ToolResultStatuses.Completed;
@@ -449,6 +469,11 @@ private static bool IsToolAllowedForSession(Session session, string toolName, Re
449469
return true;
450470
}
451471

472+
private static ToolActionDescriptor ResolveToolActionDescriptor(ITool tool, string argsJson)
473+
=> tool is IToolActionDescriptorProvider descriptorProvider
474+
? descriptorProvider.ResolveActionDescriptor(argsJson)
475+
: ToolActionPolicyResolver.Resolve(tool.Name, argsJson);
476+
452477
private async Task<string> ExecuteStreamingToolCollectAsync(
453478
IStreamingTool tool,
454479
string argsJson,

0 commit comments

Comments
 (0)