uno-cli 1.0.1__tar.gz → 1.0.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.
- {uno_cli-1.0.1 → uno_cli-1.0.2}/PKG-INFO +1 -1
- {uno_cli-1.0.1 → uno_cli-1.0.2}/pyproject.toml +1 -1
- uno_cli-1.0.2/tests/test_dry_run.py +138 -0
- uno_cli-1.0.2/uno_cli/_version.py +1 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/uno_cli/cli.py +31 -2
- uno_cli-1.0.1/uno_cli/_version.py +0 -1
- {uno_cli-1.0.1 → uno_cli-1.0.2}/.gitignore +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/LICENSE +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/README.md +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/tests/__init__.py +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/tests/test_auth.py +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/tests/test_cli.py +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/tests/test_scope.py +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/tests/test_token_masking.py +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/uno_cli/__init__.py +0 -0
- {uno_cli-1.0.1 → uno_cli-1.0.2}/uno_cli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uno-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Uno CLI — Agent tool gateway by ClawdChat. Access 2000+ real-world tools from the command line.
|
|
5
5
|
Project-URL: Homepage, https://clawdtools.uno
|
|
6
6
|
Project-URL: Documentation, https://clawdtools.uno/docs
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""--dry-run preview tests.
|
|
2
|
+
|
|
3
|
+
``uno call --dry-run <tool>`` is the primary user-confirmation surface: it
|
|
4
|
+
formats the planned invocation (tool + arguments) as JSON without reading any
|
|
5
|
+
credential, opening any socket, or spending any credit. These tests are the
|
|
6
|
+
safety net that guarantees the dry-run remains strictly local.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from uno_cli import cli
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Shared fake API that explodes if ever called ───────────────────
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _PanicAPI:
|
|
23
|
+
"""Any method on this class raises — a dry-run must never hit it."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *a, **kw):
|
|
26
|
+
raise AssertionError(
|
|
27
|
+
"dry-run called the network client; it must not touch the network"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Parser surface ─────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestDryRunFlag:
|
|
35
|
+
def test_dry_run_flag_defaults_to_false(self):
|
|
36
|
+
parser = cli.build_parser()
|
|
37
|
+
args = parser.parse_args(["call", "weather.get", "--args", "{}"])
|
|
38
|
+
assert args.dry_run is False
|
|
39
|
+
|
|
40
|
+
def test_dry_run_flag_parsed_when_present(self):
|
|
41
|
+
parser = cli.build_parser()
|
|
42
|
+
args = parser.parse_args(["call", "--dry-run", "weather.get", "--args", "{}"])
|
|
43
|
+
assert args.dry_run is True
|
|
44
|
+
|
|
45
|
+
def test_dry_run_flag_trailing_position_also_works(self):
|
|
46
|
+
parser = cli.build_parser()
|
|
47
|
+
args = parser.parse_args(["call", "weather.get", "--args", "{}", "--dry-run"])
|
|
48
|
+
assert args.dry_run is True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── cmd_call dry-run path ──────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestDryRunCall:
|
|
55
|
+
def _run_dry_run(self, monkeypatch, capsys, *, tool, args_json):
|
|
56
|
+
"""Invoke cmd_call in dry-run mode; any network attempt explodes."""
|
|
57
|
+
# If _get_api / UnoAPI ever gets called, the test fails loudly.
|
|
58
|
+
monkeypatch.setattr(cli, "_get_api", lambda a: _PanicAPI())
|
|
59
|
+
monkeypatch.setattr(cli, "UnoAPI", _PanicAPI)
|
|
60
|
+
|
|
61
|
+
ns = argparse.Namespace(
|
|
62
|
+
tool=tool,
|
|
63
|
+
args=args_json,
|
|
64
|
+
timeout=60,
|
|
65
|
+
dry_run=True,
|
|
66
|
+
base_url="https://clawdtools.uno",
|
|
67
|
+
)
|
|
68
|
+
cli.cmd_call(ns)
|
|
69
|
+
return json.loads(capsys.readouterr().out)
|
|
70
|
+
|
|
71
|
+
def test_dry_run_returns_preview_without_network(self, monkeypatch, capsys):
|
|
72
|
+
out = self._run_dry_run(
|
|
73
|
+
monkeypatch,
|
|
74
|
+
capsys,
|
|
75
|
+
tool="weather-free.weather_now",
|
|
76
|
+
args_json='{"city": "Beijing"}',
|
|
77
|
+
)
|
|
78
|
+
assert out["success"] is True
|
|
79
|
+
assert out["dry_run"] is True
|
|
80
|
+
assert out["preview"]["tool"] == "weather-free.weather_now"
|
|
81
|
+
assert out["preview"]["arguments"] == {"city": "Beijing"}
|
|
82
|
+
assert out["preview"]["timeout_seconds"] == 60
|
|
83
|
+
assert "No network request" in out["note"]
|
|
84
|
+
|
|
85
|
+
def test_dry_run_with_empty_args_returns_empty_dict(self, monkeypatch, capsys):
|
|
86
|
+
out = self._run_dry_run(
|
|
87
|
+
monkeypatch, capsys, tool="time.get_current_time", args_json=None
|
|
88
|
+
)
|
|
89
|
+
assert out["preview"]["arguments"] == {}
|
|
90
|
+
|
|
91
|
+
def test_dry_run_without_credentials_still_works(self, monkeypatch, capsys):
|
|
92
|
+
"""Dry-run must not call _get_api, so even unauthenticated users can
|
|
93
|
+
preview an invocation."""
|
|
94
|
+
|
|
95
|
+
def _panic(*a, **kw):
|
|
96
|
+
raise AssertionError("dry-run read credentials; it must not.")
|
|
97
|
+
|
|
98
|
+
monkeypatch.setattr(cli.AuthManager, "load_key", _panic)
|
|
99
|
+
self._run_dry_run(
|
|
100
|
+
monkeypatch,
|
|
101
|
+
capsys,
|
|
102
|
+
tool="weather-free.weather_now",
|
|
103
|
+
args_json='{"city": "Tokyo"}',
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def test_dry_run_respects_scope_guard(self, monkeypatch, capsys):
|
|
107
|
+
"""Dry-run should still be blocked by UNO_ALLOWED_TOOLS — a skill
|
|
108
|
+
scoped to gpt-image-2 should not be able to preview a call to github
|
|
109
|
+
even in dry-run mode."""
|
|
110
|
+
monkeypatch.setenv("UNO_ALLOWED_TOOLS", "gpt-image-2.*")
|
|
111
|
+
monkeypatch.setattr(cli, "_get_api", lambda a: _PanicAPI())
|
|
112
|
+
|
|
113
|
+
ns = argparse.Namespace(
|
|
114
|
+
tool="github.create_issue",
|
|
115
|
+
args='{"title": "x"}',
|
|
116
|
+
timeout=60,
|
|
117
|
+
dry_run=True,
|
|
118
|
+
base_url="https://clawdtools.uno",
|
|
119
|
+
)
|
|
120
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
121
|
+
cli.cmd_call(ns)
|
|
122
|
+
assert excinfo.value.code == 1
|
|
123
|
+
out = json.loads(capsys.readouterr().out)
|
|
124
|
+
assert out["error"] == "tool_not_allowed"
|
|
125
|
+
|
|
126
|
+
def test_dry_run_preview_does_not_leak_secrets(self, monkeypatch, capsys):
|
|
127
|
+
"""If the user passes an api_key-looking arg, the normal output
|
|
128
|
+
stripper should still redact it on the way out."""
|
|
129
|
+
out = self._run_dry_run(
|
|
130
|
+
monkeypatch,
|
|
131
|
+
capsys,
|
|
132
|
+
tool="some-tool.run",
|
|
133
|
+
args_json='{"api_key": "uno_should_be_masked_longlonglong"}',
|
|
134
|
+
)
|
|
135
|
+
# _out passes everything through _strip_secrets; the api_key field
|
|
136
|
+
# inside preview.arguments is a legitimate tool arg, but the masker
|
|
137
|
+
# errs on the side of caution by key-name.
|
|
138
|
+
assert "uno_should_be_masked_longlonglong" not in json.dumps(out)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.2"
|
|
@@ -655,11 +655,10 @@ def cmd_call(args):
|
|
|
655
655
|
allowed=allowed_patterns,
|
|
656
656
|
hint=(
|
|
657
657
|
"This skill is scoped to a subset of Uno tools. "
|
|
658
|
-
"Install uno-cli separately to access the full
|
|
658
|
+
"Install uno-cli separately to access the full tool catalog."
|
|
659
659
|
),
|
|
660
660
|
)
|
|
661
661
|
|
|
662
|
-
api = _get_api(args)
|
|
663
662
|
arguments = {}
|
|
664
663
|
raw_args = getattr(args, "args", None)
|
|
665
664
|
if raw_args:
|
|
@@ -674,6 +673,30 @@ def cmd_call(args):
|
|
|
674
673
|
_fail(f"Invalid JSON in --args: {e}")
|
|
675
674
|
|
|
676
675
|
timeout = getattr(args, "timeout", 60) or 60
|
|
676
|
+
|
|
677
|
+
# Dry-run preview: format the planned invocation without authenticating,
|
|
678
|
+
# issuing any network request, or spending credits. This is the primary
|
|
679
|
+
# "user confirmation" surface — an agent is expected to dry-run a call,
|
|
680
|
+
# show the JSON to the human, and only re-run without --dry-run after
|
|
681
|
+
# explicit approval. Read-only and credentials-free by construction.
|
|
682
|
+
if getattr(args, "dry_run", False):
|
|
683
|
+
_out({
|
|
684
|
+
"success": True,
|
|
685
|
+
"dry_run": True,
|
|
686
|
+
"preview": {
|
|
687
|
+
"tool": tool_slug,
|
|
688
|
+
"arguments": arguments,
|
|
689
|
+
"timeout_seconds": timeout,
|
|
690
|
+
},
|
|
691
|
+
"note": (
|
|
692
|
+
"No network request was made, no credentials were read, and "
|
|
693
|
+
"no credits were spent. Re-run without --dry-run to actually "
|
|
694
|
+
"invoke the tool."
|
|
695
|
+
),
|
|
696
|
+
})
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
api = _get_api(args)
|
|
677
700
|
try:
|
|
678
701
|
result = api.post("/v1/call", {"tool": tool_slug, "arguments": arguments}, timeout=timeout)
|
|
679
702
|
if result.get("error"):
|
|
@@ -839,6 +862,7 @@ def build_parser():
|
|
|
839
862
|
"Examples:\n"
|
|
840
863
|
" uno call time.get_current_time --args '{\"timezone\":\"Asia/Shanghai\"}'\n"
|
|
841
864
|
" uno call weather-cn.weatherArea --args '{\"area\":\"Beijing\"}'\n"
|
|
865
|
+
" uno call --dry-run weather-cn.weatherArea --args '{\"area\":\"Beijing\"}'\n"
|
|
842
866
|
" uno call twelve-data.quote --args '{\"symbol\":\"AAPL\"}' --timeout 30\n\n"
|
|
843
867
|
"Scope enforcement (for single-purpose skills):\n"
|
|
844
868
|
" UNO_ALLOWED_TOOLS='gpt-image-2.*,time.*' # comma-separated glob whitelist"
|
|
@@ -847,6 +871,11 @@ def build_parser():
|
|
|
847
871
|
)
|
|
848
872
|
call_p.add_argument("tool", help="Tool slug (e.g. weather-cn.weatherArea)")
|
|
849
873
|
call_p.add_argument("--args", help='JSON arguments (e.g. \'{"city":"Beijing"}\')')
|
|
874
|
+
call_p.add_argument(
|
|
875
|
+
"--dry-run",
|
|
876
|
+
action="store_true",
|
|
877
|
+
help="Preview the planned invocation without issuing any network request or spending credits.",
|
|
878
|
+
)
|
|
850
879
|
call_p.add_argument("--timeout", type=int, default=60, help="Request timeout in seconds (default: 60)")
|
|
851
880
|
|
|
852
881
|
rate_p = sub.add_parser("rate", help="Rate a tool (0-5)")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|