1claw-crewai-tools 0.1.0__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.
- 1claw_crewai_tools-0.1.0/LICENSE +21 -0
- 1claw_crewai_tools-0.1.0/PKG-INFO +171 -0
- 1claw_crewai_tools-0.1.0/README.md +137 -0
- 1claw_crewai_tools-0.1.0/pyproject.toml +74 -0
- 1claw_crewai_tools-0.1.0/setup.cfg +4 -0
- 1claw_crewai_tools-0.1.0/src/1claw_crewai_tools.egg-info/PKG-INFO +171 -0
- 1claw_crewai_tools-0.1.0/src/1claw_crewai_tools.egg-info/SOURCES.txt +14 -0
- 1claw_crewai_tools-0.1.0/src/1claw_crewai_tools.egg-info/dependency_links.txt +1 -0
- 1claw_crewai_tools-0.1.0/src/1claw_crewai_tools.egg-info/requires.txt +12 -0
- 1claw_crewai_tools-0.1.0/src/1claw_crewai_tools.egg-info/top_level.txt +1 -0
- 1claw_crewai_tools-0.1.0/src/oneclaw_crewai/__init__.py +6 -0
- 1claw_crewai_tools-0.1.0/src/oneclaw_crewai/_client.py +134 -0
- 1claw_crewai_tools-0.1.0/src/oneclaw_crewai/_tool.py +83 -0
- 1claw_crewai_tools-0.1.0/src/oneclaw_crewai/py.typed +0 -0
- 1claw_crewai_tools-0.1.0/tests/test_client.py +187 -0
- 1claw_crewai_tools-0.1.0/tests/test_tool.py +79 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 1Claw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 1claw-crewai-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Secure secret fetching for CrewAI agents via 1claw HSM-backed vaults
|
|
5
|
+
Author: 1Claw
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://docs.1claw.xyz
|
|
8
|
+
Project-URL: Homepage, https://github.com/1ClawAI/1claw-crewai-tools
|
|
9
|
+
Project-URL: Repository, https://github.com/1ClawAI/1claw-crewai-tools
|
|
10
|
+
Keywords: 1claw,crewai,secrets,vault,agents
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: <3.14,>=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: crewai>=0.100.0
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
30
|
+
Requires-Dist: freezegun>=1.5; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# 1claw-crewai-tools
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/1claw-crewai-tools/)
|
|
38
|
+
[](https://pypi.org/project/1claw-crewai-tools/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://github.com/1ClawAI/1claw-crewai-tools/actions/workflows/ci.yml)
|
|
41
|
+
|
|
42
|
+
**Secure secret fetching for CrewAI agents via [1Claw](https://1claw.xyz) HSM-backed vaults.**
|
|
43
|
+
|
|
44
|
+
Credentials are retrieved at **runtime** from your vault using a **scoped agent identity** — not copied into prompts, repos, or long-lived agent memory. Do not log tool return values.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
PyPI distribution name: **`1claw-crewai-tools`**. Python import package: **`oneclaw_crewai`**.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install 1claw-crewai-tools
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import os
|
|
58
|
+
from crewai import Agent, Crew, Process, Task
|
|
59
|
+
from oneclaw_crewai import OneclawVaultTool
|
|
60
|
+
|
|
61
|
+
vault_tool = OneclawVaultTool(
|
|
62
|
+
agent_id=os.environ["ONECLAW_AGENT_ID"],
|
|
63
|
+
api_key=os.environ["ONECLAW_AGENT_API_KEY"],
|
|
64
|
+
vault_id=os.environ["ONECLAW_VAULT_ID"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
agent = Agent(
|
|
68
|
+
role="Engineer",
|
|
69
|
+
goal="Build features using vault-stored API keys",
|
|
70
|
+
backstory="You use tools instead of pasted secrets.",
|
|
71
|
+
tools=[vault_tool],
|
|
72
|
+
verbose=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
task = Task(
|
|
76
|
+
description="Read path api-keys/example using the vault tool; do not echo raw values in logs.",
|
|
77
|
+
expected_output="Confirmation that the path was read.",
|
|
78
|
+
agent=agent,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
crew = Crew(agents=[agent], tasks=[task], process=Process.sequential)
|
|
82
|
+
crew.kickoff()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Testing
|
|
86
|
+
|
|
87
|
+
### Unit tests (offline, no credentials needed)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# From the package root:
|
|
91
|
+
python -m venv .venv && source .venv/bin/activate
|
|
92
|
+
pip install -e ".[dev]"
|
|
93
|
+
pytest -v # 10 tests — client + tool
|
|
94
|
+
ruff check src tests examples
|
|
95
|
+
mypy
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
All network calls are mocked with `respx`; no 1Claw account is required.
|
|
99
|
+
|
|
100
|
+
### Live integration test (requires 1Claw + LLM key)
|
|
101
|
+
|
|
102
|
+
`examples/test_live.py` runs three progressive phases against the real API:
|
|
103
|
+
|
|
104
|
+
| Phase | What it validates |
|
|
105
|
+
|-------|-------------------|
|
|
106
|
+
| 1 | Raw `OneclawClient` — agent token exchange + `get_secret` (no CrewAI) |
|
|
107
|
+
| 2 | Single-agent crew — CrewAI invokes `oneclaw_vault` tool, LLM reports result |
|
|
108
|
+
| 3 | Two-agent crew — fetcher reads secret, writer produces unrelated output |
|
|
109
|
+
|
|
110
|
+
**Prerequisites:**
|
|
111
|
+
|
|
112
|
+
1. A [1Claw](https://1claw.xyz) account with a vault containing at least one secret.
|
|
113
|
+
2. An agent registered in that org, bound to the vault (`vault_ids`), with a read policy on the secret path (e.g. `demo/**` or `**`).
|
|
114
|
+
3. An LLM API key — **OpenAI** (`OPENAI_API_KEY`) or **Google Gemini** (`GOOGLE_API_KEY`).
|
|
115
|
+
|
|
116
|
+
For Gemini, install the provider extra: `pip install "crewai[google-genai]"`.
|
|
117
|
+
|
|
118
|
+
**Run:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export ONECLAW_AGENT_ID="<agent-uuid>"
|
|
122
|
+
export ONECLAW_AGENT_API_KEY="ocv_..."
|
|
123
|
+
export ONECLAW_VAULT_ID="<vault-uuid>"
|
|
124
|
+
export GOOGLE_API_KEY="..." # or OPENAI_API_KEY
|
|
125
|
+
|
|
126
|
+
python examples/test_live.py demo/api-key
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The script accepts an optional secret path argument (defaults to `test/crewai-live`).
|
|
130
|
+
|
|
131
|
+
**Example output (Phase 2):**
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
╭─── 🔧 Tool Execution Started (#1) ───╮
|
|
135
|
+
│ Tool: oneclaw_vault │
|
|
136
|
+
│ Args: {'path': 'demo/api-key'} │
|
|
137
|
+
╰───────────────────────────────────────╯
|
|
138
|
+
|
|
139
|
+
╭─── ✅ Agent Final Answer ────╮
|
|
140
|
+
│ Fetch succeeded, │
|
|
141
|
+
│ character length: 32. │
|
|
142
|
+
╰──────────────────────────────╯
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Known issues and gotchas
|
|
146
|
+
|
|
147
|
+
| Issue | Details |
|
|
148
|
+
|-------|---------|
|
|
149
|
+
| **Tool name must be a valid identifier** | OpenAI (and other providers) require function names to start with a letter or underscore. The tool is named `oneclaw_vault` — not `1claw Vault` — for this reason. If you subclass and change `name`, keep it alphanumeric + underscores, starting with a letter. |
|
|
150
|
+
| **CrewAI verbose logging prints secret values** | When `verbose=True`, CrewAI's internal executor logs the raw return value of every tool call. This is a CrewAI framework behaviour, not this package. In production, set `verbose=False` or redirect stdout. |
|
|
151
|
+
| **Google Gemini requires an extra** | `pip install "crewai[google-genai]"` — without it, `LLM(model="gemini/...")` raises `ImportError`. |
|
|
152
|
+
| **`cache_function` must be a callable** | CrewAI's `BaseTool.cache_function` field is typed as `Callable[..., bool]`, not `bool`. This package sets it to a function that always returns `False`. If you override it, pass a callable, not a bare `False`. |
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- **[Quickstart](docs/quickstart.md)** — prerequisites, env vars, paths, security, troubleshooting
|
|
157
|
+
- **[1Claw docs](https://docs.1claw.xyz)** — vaults, agents, policies
|
|
158
|
+
|
|
159
|
+
## Security
|
|
160
|
+
|
|
161
|
+
- Tool output can contain **plaintext credentials**. Never `print()` or log the return value of `_run` / tool execution.
|
|
162
|
+
- `OneclawVaultTool` sets CrewAI's **`cache_function`** to a callable that **always returns `False`** so credential reads are **not** cached by the framework.
|
|
163
|
+
- CrewAI's `verbose=True` prints tool output including secrets to stdout — use `verbose=False` in production.
|
|
164
|
+
|
|
165
|
+
## Repository
|
|
166
|
+
|
|
167
|
+
Source: [github.com/1ClawAI/1claw-crewai-tools](https://github.com/1ClawAI/1claw-crewai-tools)
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# 1claw-crewai-tools
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/1claw-crewai-tools/)
|
|
4
|
+
[](https://pypi.org/project/1claw-crewai-tools/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/1ClawAI/1claw-crewai-tools/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
**Secure secret fetching for CrewAI agents via [1Claw](https://1claw.xyz) HSM-backed vaults.**
|
|
9
|
+
|
|
10
|
+
Credentials are retrieved at **runtime** from your vault using a **scoped agent identity** — not copied into prompts, repos, or long-lived agent memory. Do not log tool return values.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
PyPI distribution name: **`1claw-crewai-tools`**. Python import package: **`oneclaw_crewai`**.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install 1claw-crewai-tools
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import os
|
|
24
|
+
from crewai import Agent, Crew, Process, Task
|
|
25
|
+
from oneclaw_crewai import OneclawVaultTool
|
|
26
|
+
|
|
27
|
+
vault_tool = OneclawVaultTool(
|
|
28
|
+
agent_id=os.environ["ONECLAW_AGENT_ID"],
|
|
29
|
+
api_key=os.environ["ONECLAW_AGENT_API_KEY"],
|
|
30
|
+
vault_id=os.environ["ONECLAW_VAULT_ID"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
agent = Agent(
|
|
34
|
+
role="Engineer",
|
|
35
|
+
goal="Build features using vault-stored API keys",
|
|
36
|
+
backstory="You use tools instead of pasted secrets.",
|
|
37
|
+
tools=[vault_tool],
|
|
38
|
+
verbose=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
task = Task(
|
|
42
|
+
description="Read path api-keys/example using the vault tool; do not echo raw values in logs.",
|
|
43
|
+
expected_output="Confirmation that the path was read.",
|
|
44
|
+
agent=agent,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
crew = Crew(agents=[agent], tasks=[task], process=Process.sequential)
|
|
48
|
+
crew.kickoff()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Testing
|
|
52
|
+
|
|
53
|
+
### Unit tests (offline, no credentials needed)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# From the package root:
|
|
57
|
+
python -m venv .venv && source .venv/bin/activate
|
|
58
|
+
pip install -e ".[dev]"
|
|
59
|
+
pytest -v # 10 tests — client + tool
|
|
60
|
+
ruff check src tests examples
|
|
61
|
+
mypy
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
All network calls are mocked with `respx`; no 1Claw account is required.
|
|
65
|
+
|
|
66
|
+
### Live integration test (requires 1Claw + LLM key)
|
|
67
|
+
|
|
68
|
+
`examples/test_live.py` runs three progressive phases against the real API:
|
|
69
|
+
|
|
70
|
+
| Phase | What it validates |
|
|
71
|
+
|-------|-------------------|
|
|
72
|
+
| 1 | Raw `OneclawClient` — agent token exchange + `get_secret` (no CrewAI) |
|
|
73
|
+
| 2 | Single-agent crew — CrewAI invokes `oneclaw_vault` tool, LLM reports result |
|
|
74
|
+
| 3 | Two-agent crew — fetcher reads secret, writer produces unrelated output |
|
|
75
|
+
|
|
76
|
+
**Prerequisites:**
|
|
77
|
+
|
|
78
|
+
1. A [1Claw](https://1claw.xyz) account with a vault containing at least one secret.
|
|
79
|
+
2. An agent registered in that org, bound to the vault (`vault_ids`), with a read policy on the secret path (e.g. `demo/**` or `**`).
|
|
80
|
+
3. An LLM API key — **OpenAI** (`OPENAI_API_KEY`) or **Google Gemini** (`GOOGLE_API_KEY`).
|
|
81
|
+
|
|
82
|
+
For Gemini, install the provider extra: `pip install "crewai[google-genai]"`.
|
|
83
|
+
|
|
84
|
+
**Run:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export ONECLAW_AGENT_ID="<agent-uuid>"
|
|
88
|
+
export ONECLAW_AGENT_API_KEY="ocv_..."
|
|
89
|
+
export ONECLAW_VAULT_ID="<vault-uuid>"
|
|
90
|
+
export GOOGLE_API_KEY="..." # or OPENAI_API_KEY
|
|
91
|
+
|
|
92
|
+
python examples/test_live.py demo/api-key
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The script accepts an optional secret path argument (defaults to `test/crewai-live`).
|
|
96
|
+
|
|
97
|
+
**Example output (Phase 2):**
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
╭─── 🔧 Tool Execution Started (#1) ───╮
|
|
101
|
+
│ Tool: oneclaw_vault │
|
|
102
|
+
│ Args: {'path': 'demo/api-key'} │
|
|
103
|
+
╰───────────────────────────────────────╯
|
|
104
|
+
|
|
105
|
+
╭─── ✅ Agent Final Answer ────╮
|
|
106
|
+
│ Fetch succeeded, │
|
|
107
|
+
│ character length: 32. │
|
|
108
|
+
╰──────────────────────────────╯
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Known issues and gotchas
|
|
112
|
+
|
|
113
|
+
| Issue | Details |
|
|
114
|
+
|-------|---------|
|
|
115
|
+
| **Tool name must be a valid identifier** | OpenAI (and other providers) require function names to start with a letter or underscore. The tool is named `oneclaw_vault` — not `1claw Vault` — for this reason. If you subclass and change `name`, keep it alphanumeric + underscores, starting with a letter. |
|
|
116
|
+
| **CrewAI verbose logging prints secret values** | When `verbose=True`, CrewAI's internal executor logs the raw return value of every tool call. This is a CrewAI framework behaviour, not this package. In production, set `verbose=False` or redirect stdout. |
|
|
117
|
+
| **Google Gemini requires an extra** | `pip install "crewai[google-genai]"` — without it, `LLM(model="gemini/...")` raises `ImportError`. |
|
|
118
|
+
| **`cache_function` must be a callable** | CrewAI's `BaseTool.cache_function` field is typed as `Callable[..., bool]`, not `bool`. This package sets it to a function that always returns `False`. If you override it, pass a callable, not a bare `False`. |
|
|
119
|
+
|
|
120
|
+
## Documentation
|
|
121
|
+
|
|
122
|
+
- **[Quickstart](docs/quickstart.md)** — prerequisites, env vars, paths, security, troubleshooting
|
|
123
|
+
- **[1Claw docs](https://docs.1claw.xyz)** — vaults, agents, policies
|
|
124
|
+
|
|
125
|
+
## Security
|
|
126
|
+
|
|
127
|
+
- Tool output can contain **plaintext credentials**. Never `print()` or log the return value of `_run` / tool execution.
|
|
128
|
+
- `OneclawVaultTool` sets CrewAI's **`cache_function`** to a callable that **always returns `False`** so credential reads are **not** cached by the framework.
|
|
129
|
+
- CrewAI's `verbose=True` prints tool output including secrets to stdout — use `verbose=False` in production.
|
|
130
|
+
|
|
131
|
+
## Repository
|
|
132
|
+
|
|
133
|
+
Source: [github.com/1ClawAI/1claw-crewai-tools](https://github.com/1ClawAI/1claw-crewai-tools)
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "1claw-crewai-tools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Secure secret fetching for CrewAI agents via 1claw HSM-backed vaults"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10,<3.14"
|
|
13
|
+
authors = [{ name = "1Claw" }]
|
|
14
|
+
keywords = ["1claw", "crewai", "secrets", "vault", "agents"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"crewai>=0.100.0",
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"pydantic>=2.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"respx>=0.21",
|
|
35
|
+
"ruff>=0.4",
|
|
36
|
+
"mypy>=1.8",
|
|
37
|
+
"freezegun>=1.5",
|
|
38
|
+
"build>=1.0",
|
|
39
|
+
"twine>=5.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Documentation = "https://docs.1claw.xyz"
|
|
44
|
+
Homepage = "https://github.com/1ClawAI/1claw-crewai-tools"
|
|
45
|
+
Repository = "https://github.com/1ClawAI/1claw-crewai-tools"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
oneclaw_crewai = ["py.typed"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
src = ["src", "tests", "examples"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "W"]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.10"
|
|
63
|
+
strict = true
|
|
64
|
+
warn_return_any = true
|
|
65
|
+
warn_unused_configs = true
|
|
66
|
+
mypy_path = "src"
|
|
67
|
+
packages = ["oneclaw_crewai"]
|
|
68
|
+
|
|
69
|
+
[[tool.mypy.overrides]]
|
|
70
|
+
module = ["crewai.*", "crewai_tools.*"]
|
|
71
|
+
ignore_missing_imports = true
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 1claw-crewai-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Secure secret fetching for CrewAI agents via 1claw HSM-backed vaults
|
|
5
|
+
Author: 1Claw
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://docs.1claw.xyz
|
|
8
|
+
Project-URL: Homepage, https://github.com/1ClawAI/1claw-crewai-tools
|
|
9
|
+
Project-URL: Repository, https://github.com/1ClawAI/1claw-crewai-tools
|
|
10
|
+
Keywords: 1claw,crewai,secrets,vault,agents
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: <3.14,>=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: crewai>=0.100.0
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
30
|
+
Requires-Dist: freezegun>=1.5; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# 1claw-crewai-tools
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/1claw-crewai-tools/)
|
|
38
|
+
[](https://pypi.org/project/1claw-crewai-tools/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://github.com/1ClawAI/1claw-crewai-tools/actions/workflows/ci.yml)
|
|
41
|
+
|
|
42
|
+
**Secure secret fetching for CrewAI agents via [1Claw](https://1claw.xyz) HSM-backed vaults.**
|
|
43
|
+
|
|
44
|
+
Credentials are retrieved at **runtime** from your vault using a **scoped agent identity** — not copied into prompts, repos, or long-lived agent memory. Do not log tool return values.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
PyPI distribution name: **`1claw-crewai-tools`**. Python import package: **`oneclaw_crewai`**.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install 1claw-crewai-tools
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import os
|
|
58
|
+
from crewai import Agent, Crew, Process, Task
|
|
59
|
+
from oneclaw_crewai import OneclawVaultTool
|
|
60
|
+
|
|
61
|
+
vault_tool = OneclawVaultTool(
|
|
62
|
+
agent_id=os.environ["ONECLAW_AGENT_ID"],
|
|
63
|
+
api_key=os.environ["ONECLAW_AGENT_API_KEY"],
|
|
64
|
+
vault_id=os.environ["ONECLAW_VAULT_ID"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
agent = Agent(
|
|
68
|
+
role="Engineer",
|
|
69
|
+
goal="Build features using vault-stored API keys",
|
|
70
|
+
backstory="You use tools instead of pasted secrets.",
|
|
71
|
+
tools=[vault_tool],
|
|
72
|
+
verbose=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
task = Task(
|
|
76
|
+
description="Read path api-keys/example using the vault tool; do not echo raw values in logs.",
|
|
77
|
+
expected_output="Confirmation that the path was read.",
|
|
78
|
+
agent=agent,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
crew = Crew(agents=[agent], tasks=[task], process=Process.sequential)
|
|
82
|
+
crew.kickoff()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Testing
|
|
86
|
+
|
|
87
|
+
### Unit tests (offline, no credentials needed)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# From the package root:
|
|
91
|
+
python -m venv .venv && source .venv/bin/activate
|
|
92
|
+
pip install -e ".[dev]"
|
|
93
|
+
pytest -v # 10 tests — client + tool
|
|
94
|
+
ruff check src tests examples
|
|
95
|
+
mypy
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
All network calls are mocked with `respx`; no 1Claw account is required.
|
|
99
|
+
|
|
100
|
+
### Live integration test (requires 1Claw + LLM key)
|
|
101
|
+
|
|
102
|
+
`examples/test_live.py` runs three progressive phases against the real API:
|
|
103
|
+
|
|
104
|
+
| Phase | What it validates |
|
|
105
|
+
|-------|-------------------|
|
|
106
|
+
| 1 | Raw `OneclawClient` — agent token exchange + `get_secret` (no CrewAI) |
|
|
107
|
+
| 2 | Single-agent crew — CrewAI invokes `oneclaw_vault` tool, LLM reports result |
|
|
108
|
+
| 3 | Two-agent crew — fetcher reads secret, writer produces unrelated output |
|
|
109
|
+
|
|
110
|
+
**Prerequisites:**
|
|
111
|
+
|
|
112
|
+
1. A [1Claw](https://1claw.xyz) account with a vault containing at least one secret.
|
|
113
|
+
2. An agent registered in that org, bound to the vault (`vault_ids`), with a read policy on the secret path (e.g. `demo/**` or `**`).
|
|
114
|
+
3. An LLM API key — **OpenAI** (`OPENAI_API_KEY`) or **Google Gemini** (`GOOGLE_API_KEY`).
|
|
115
|
+
|
|
116
|
+
For Gemini, install the provider extra: `pip install "crewai[google-genai]"`.
|
|
117
|
+
|
|
118
|
+
**Run:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export ONECLAW_AGENT_ID="<agent-uuid>"
|
|
122
|
+
export ONECLAW_AGENT_API_KEY="ocv_..."
|
|
123
|
+
export ONECLAW_VAULT_ID="<vault-uuid>"
|
|
124
|
+
export GOOGLE_API_KEY="..." # or OPENAI_API_KEY
|
|
125
|
+
|
|
126
|
+
python examples/test_live.py demo/api-key
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The script accepts an optional secret path argument (defaults to `test/crewai-live`).
|
|
130
|
+
|
|
131
|
+
**Example output (Phase 2):**
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
╭─── 🔧 Tool Execution Started (#1) ───╮
|
|
135
|
+
│ Tool: oneclaw_vault │
|
|
136
|
+
│ Args: {'path': 'demo/api-key'} │
|
|
137
|
+
╰───────────────────────────────────────╯
|
|
138
|
+
|
|
139
|
+
╭─── ✅ Agent Final Answer ────╮
|
|
140
|
+
│ Fetch succeeded, │
|
|
141
|
+
│ character length: 32. │
|
|
142
|
+
╰──────────────────────────────╯
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Known issues and gotchas
|
|
146
|
+
|
|
147
|
+
| Issue | Details |
|
|
148
|
+
|-------|---------|
|
|
149
|
+
| **Tool name must be a valid identifier** | OpenAI (and other providers) require function names to start with a letter or underscore. The tool is named `oneclaw_vault` — not `1claw Vault` — for this reason. If you subclass and change `name`, keep it alphanumeric + underscores, starting with a letter. |
|
|
150
|
+
| **CrewAI verbose logging prints secret values** | When `verbose=True`, CrewAI's internal executor logs the raw return value of every tool call. This is a CrewAI framework behaviour, not this package. In production, set `verbose=False` or redirect stdout. |
|
|
151
|
+
| **Google Gemini requires an extra** | `pip install "crewai[google-genai]"` — without it, `LLM(model="gemini/...")` raises `ImportError`. |
|
|
152
|
+
| **`cache_function` must be a callable** | CrewAI's `BaseTool.cache_function` field is typed as `Callable[..., bool]`, not `bool`. This package sets it to a function that always returns `False`. If you override it, pass a callable, not a bare `False`. |
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- **[Quickstart](docs/quickstart.md)** — prerequisites, env vars, paths, security, troubleshooting
|
|
157
|
+
- **[1Claw docs](https://docs.1claw.xyz)** — vaults, agents, policies
|
|
158
|
+
|
|
159
|
+
## Security
|
|
160
|
+
|
|
161
|
+
- Tool output can contain **plaintext credentials**. Never `print()` or log the return value of `_run` / tool execution.
|
|
162
|
+
- `OneclawVaultTool` sets CrewAI's **`cache_function`** to a callable that **always returns `False`** so credential reads are **not** cached by the framework.
|
|
163
|
+
- CrewAI's `verbose=True` prints tool output including secrets to stdout — use `verbose=False` in production.
|
|
164
|
+
|
|
165
|
+
## Repository
|
|
166
|
+
|
|
167
|
+
Source: [github.com/1ClawAI/1claw-crewai-tools](https://github.com/1ClawAI/1claw-crewai-tools)
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/1claw_crewai_tools.egg-info/PKG-INFO
|
|
5
|
+
src/1claw_crewai_tools.egg-info/SOURCES.txt
|
|
6
|
+
src/1claw_crewai_tools.egg-info/dependency_links.txt
|
|
7
|
+
src/1claw_crewai_tools.egg-info/requires.txt
|
|
8
|
+
src/1claw_crewai_tools.egg-info/top_level.txt
|
|
9
|
+
src/oneclaw_crewai/__init__.py
|
|
10
|
+
src/oneclaw_crewai/_client.py
|
|
11
|
+
src/oneclaw_crewai/_tool.py
|
|
12
|
+
src/oneclaw_crewai/py.typed
|
|
13
|
+
tests/test_client.py
|
|
14
|
+
tests/test_tool.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oneclaw_crewai
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""HTTP client for the 1Claw Vault REST API (agent authentication)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OneclawError(Exception):
|
|
13
|
+
"""Base error for 1Claw API failures."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OneclawAuthError(OneclawError):
|
|
19
|
+
"""Raised when authentication or authorization fails (401/403)."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OneclawSecretNotFoundError(OneclawError):
|
|
25
|
+
"""Raised when the requested secret path does not exist (404)."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OneclawClient:
|
|
31
|
+
"""Synchronous httpx client with JWT caching for agent API access."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
agent_id: str,
|
|
36
|
+
api_key: str,
|
|
37
|
+
vault_id: str,
|
|
38
|
+
base_url: str = "https://api.1claw.xyz",
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Create a client for the given agent credentials and vault.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
agent_id: 1Claw agent UUID string.
|
|
44
|
+
api_key: Agent API key (``ocv_`` prefix).
|
|
45
|
+
vault_id: Target vault UUID string.
|
|
46
|
+
base_url: Vault API base URL (no trailing slash).
|
|
47
|
+
"""
|
|
48
|
+
self._agent_id = agent_id
|
|
49
|
+
self._api_key = api_key
|
|
50
|
+
self._vault_id = vault_id
|
|
51
|
+
self._base_url = base_url.rstrip("/")
|
|
52
|
+
self._http = httpx.Client(timeout=30.0)
|
|
53
|
+
self._access_token: str | None = None
|
|
54
|
+
self._token_expires_at: float | None = None
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
"""Close the underlying HTTP client."""
|
|
58
|
+
self._http.close()
|
|
59
|
+
|
|
60
|
+
def __enter__(self) -> OneclawClient:
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, *args: object) -> None:
|
|
64
|
+
self.close()
|
|
65
|
+
|
|
66
|
+
def _ensure_token(self) -> None:
|
|
67
|
+
"""Obtain or refresh the bearer token if missing or near expiry."""
|
|
68
|
+
now = time.time()
|
|
69
|
+
if (
|
|
70
|
+
self._access_token is not None
|
|
71
|
+
and self._token_expires_at is not None
|
|
72
|
+
and now < self._token_expires_at - 30
|
|
73
|
+
):
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
url = f"{self._base_url}/v1/auth/agent-token"
|
|
77
|
+
resp = self._http.post(
|
|
78
|
+
url,
|
|
79
|
+
json={"agent_id": self._agent_id, "api_key": self._api_key},
|
|
80
|
+
headers={"Content-Type": "application/json"},
|
|
81
|
+
)
|
|
82
|
+
if resp.status_code in (401, 403):
|
|
83
|
+
raise OneclawAuthError(f"Authentication failed: HTTP {resp.status_code}")
|
|
84
|
+
if not resp.is_success:
|
|
85
|
+
raise OneclawError(f"Token request failed: HTTP {resp.status_code}")
|
|
86
|
+
|
|
87
|
+
data: dict[str, Any] = resp.json()
|
|
88
|
+
token = data.get("access_token")
|
|
89
|
+
if not isinstance(token, str) or not token:
|
|
90
|
+
raise OneclawError("Token response missing access_token")
|
|
91
|
+
expires_in = data.get("expires_in", 300)
|
|
92
|
+
if not isinstance(expires_in, (int, float)):
|
|
93
|
+
expires_in = 300
|
|
94
|
+
|
|
95
|
+
self._access_token = token
|
|
96
|
+
self._token_expires_at = now + float(expires_in)
|
|
97
|
+
|
|
98
|
+
def get_secret(self, path: str) -> str:
|
|
99
|
+
"""GET a decrypted secret value by vault path.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
path: Secret path (e.g. ``api-keys/stripe``).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The decrypted secret value string.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
OneclawAuthError: On 401 or 403.
|
|
109
|
+
OneclawSecretNotFoundError: On 404.
|
|
110
|
+
OneclawError: On other non-success responses or malformed JSON.
|
|
111
|
+
"""
|
|
112
|
+
self._ensure_token()
|
|
113
|
+
assert self._access_token is not None
|
|
114
|
+
|
|
115
|
+
encoded = quote(path.lstrip("/"), safe="")
|
|
116
|
+
url = f"{self._base_url}/v1/vaults/{self._vault_id}/secrets/{encoded}"
|
|
117
|
+
resp = self._http.get(
|
|
118
|
+
url,
|
|
119
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if resp.status_code in (401, 403):
|
|
123
|
+
raise OneclawAuthError(f"Secret request unauthorized: HTTP {resp.status_code}")
|
|
124
|
+
if resp.status_code == 404:
|
|
125
|
+
raise OneclawSecretNotFoundError(f"Secret not found at path: {path}")
|
|
126
|
+
if not resp.is_success:
|
|
127
|
+
raise OneclawError(f"Secret request failed: HTTP {resp.status_code}")
|
|
128
|
+
|
|
129
|
+
body: dict[str, Any] = resp.json()
|
|
130
|
+
value = body.get("value")
|
|
131
|
+
if not isinstance(value, str):
|
|
132
|
+
raise OneclawError("Secret response missing string value")
|
|
133
|
+
# SECRET: do not log — value is sensitive; never print or log this string.
|
|
134
|
+
return value
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""CrewAI BaseTool that reads secrets from a 1Claw vault."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from crewai.tools import BaseTool
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from ._client import OneclawClient, OneclawError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _never_cache_vault_tool(_args: Any = None, _result: Any = None) -> bool:
|
|
14
|
+
"""Return False so CrewAI does not cache tool output (credentials may rotate)."""
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OneclawVaultInput(BaseModel):
|
|
19
|
+
"""Input schema for :class:`OneclawVaultTool`."""
|
|
20
|
+
|
|
21
|
+
path: str = Field(
|
|
22
|
+
...,
|
|
23
|
+
description=(
|
|
24
|
+
"The secret path in the 1claw vault, e.g. 'api-keys/stripe' or "
|
|
25
|
+
"'db/postgres-url'. Use the exact path the vault owner configured."
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OneclawVaultTool(BaseTool):
|
|
31
|
+
"""Tool that fetches a secret value from a 1Claw vault by path."""
|
|
32
|
+
|
|
33
|
+
name: str = "oneclaw_vault"
|
|
34
|
+
description: str = (
|
|
35
|
+
"Fetch a secret from the 1Claw HSM-backed vault by its path. "
|
|
36
|
+
"Use this tool whenever you need an API key, token, connection string, "
|
|
37
|
+
"or other credential. Never ask the user to paste credentials — "
|
|
38
|
+
"fetch them from the vault instead. "
|
|
39
|
+
"Input: the secret path (e.g. 'api-keys/openai')."
|
|
40
|
+
)
|
|
41
|
+
args_schema: type[BaseModel] = OneclawVaultInput
|
|
42
|
+
# CrewAI uses a callable here (not a bool); must always return False.
|
|
43
|
+
cache_function: Any = _never_cache_vault_tool
|
|
44
|
+
|
|
45
|
+
_client: OneclawClient | None = None
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
agent_id: str,
|
|
51
|
+
api_key: str,
|
|
52
|
+
vault_id: str,
|
|
53
|
+
base_url: str = "https://api.1claw.xyz",
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Build a vault tool for the given agent and vault.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
agent_id: 1Claw agent id (UUID).
|
|
60
|
+
api_key: Agent API key (``ocv_`` prefix).
|
|
61
|
+
vault_id: Vault id (UUID) the agent may access per policy.
|
|
62
|
+
base_url: Vault API base URL.
|
|
63
|
+
**kwargs: Forwarded to :class:`crewai.tools.BaseTool`.
|
|
64
|
+
"""
|
|
65
|
+
super().__init__(**kwargs)
|
|
66
|
+
object.__setattr__(
|
|
67
|
+
self,
|
|
68
|
+
"_client",
|
|
69
|
+
OneclawClient(
|
|
70
|
+
agent_id=agent_id,
|
|
71
|
+
api_key=api_key,
|
|
72
|
+
vault_id=vault_id,
|
|
73
|
+
base_url=base_url,
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _run(self, path: str) -> str:
|
|
78
|
+
"""Fetch the secret at ``path``; returns error string on failure (does not raise)."""
|
|
79
|
+
assert self._client is not None
|
|
80
|
+
try:
|
|
81
|
+
return self._client.get_secret(path)
|
|
82
|
+
except OneclawError as exc:
|
|
83
|
+
return f"[1claw error] {exc}"
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Tests for :mod:`oneclaw_crewai._client`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
import respx
|
|
11
|
+
from freezegun import freeze_time
|
|
12
|
+
|
|
13
|
+
from oneclaw_crewai._client import (
|
|
14
|
+
OneclawAuthError,
|
|
15
|
+
OneclawClient,
|
|
16
|
+
OneclawError,
|
|
17
|
+
OneclawSecretNotFoundError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _vault_id() -> str:
|
|
22
|
+
return str(uuid.uuid4())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _agent_id() -> str:
|
|
26
|
+
return str(uuid.uuid4())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@respx.mock
|
|
30
|
+
def test_successful_auth_and_secret_fetch(api_base: str) -> None:
|
|
31
|
+
vid = _vault_id()
|
|
32
|
+
aid = _agent_id()
|
|
33
|
+
respx.post(f"{api_base}/v1/auth/agent-token").mock(
|
|
34
|
+
return_value=httpx.Response(
|
|
35
|
+
200,
|
|
36
|
+
json={"access_token": "jwt-one", "expires_in": 3600},
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
respx.get(f"{api_base}/v1/vaults/{vid}/secrets/api-keys%2Fstripe").mock(
|
|
40
|
+
return_value=httpx.Response(
|
|
41
|
+
200,
|
|
42
|
+
json={"value": "sk_test_abc", "path": "api-keys/stripe", "version": 1},
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
client = OneclawClient(
|
|
47
|
+
agent_id=aid,
|
|
48
|
+
api_key="ocv_testkey",
|
|
49
|
+
vault_id=vid,
|
|
50
|
+
base_url=api_base,
|
|
51
|
+
)
|
|
52
|
+
try:
|
|
53
|
+
assert client.get_secret("api-keys/stripe") == "sk_test_abc"
|
|
54
|
+
finally:
|
|
55
|
+
client.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@respx.mock
|
|
59
|
+
def test_jwt_caching_single_auth_for_two_fetches(api_base: str) -> None:
|
|
60
|
+
vid = _vault_id()
|
|
61
|
+
aid = _agent_id()
|
|
62
|
+
auth_route = respx.post(f"{api_base}/v1/auth/agent-token").mock(
|
|
63
|
+
return_value=httpx.Response(
|
|
64
|
+
200,
|
|
65
|
+
json={"access_token": "jwt-one", "expires_in": 3600},
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
respx.get(f"{api_base}/v1/vaults/{vid}/secrets/a").mock(
|
|
69
|
+
return_value=httpx.Response(200, json={"value": "v1"}),
|
|
70
|
+
)
|
|
71
|
+
respx.get(f"{api_base}/v1/vaults/{vid}/secrets/b").mock(
|
|
72
|
+
return_value=httpx.Response(200, json={"value": "v2"}),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
client = OneclawClient(
|
|
76
|
+
agent_id=aid,
|
|
77
|
+
api_key="ocv_testkey",
|
|
78
|
+
vault_id=vid,
|
|
79
|
+
base_url=api_base,
|
|
80
|
+
)
|
|
81
|
+
try:
|
|
82
|
+
assert client.get_secret("a") == "v1"
|
|
83
|
+
assert client.get_secret("b") == "v2"
|
|
84
|
+
assert auth_route.call_count == 1
|
|
85
|
+
finally:
|
|
86
|
+
client.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@respx.mock
|
|
90
|
+
@pytest.mark.parametrize("status", [401, 403])
|
|
91
|
+
def test_auth_error_on_token_failure(api_base: str, status: int) -> None:
|
|
92
|
+
vid = _vault_id()
|
|
93
|
+
aid = _agent_id()
|
|
94
|
+
respx.post(f"{api_base}/v1/auth/agent-token").mock(
|
|
95
|
+
return_value=httpx.Response(status, json={"detail": "nope"}),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
client = OneclawClient(
|
|
99
|
+
agent_id=aid,
|
|
100
|
+
api_key="ocv_testkey",
|
|
101
|
+
vault_id=vid,
|
|
102
|
+
base_url=api_base,
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
with pytest.raises(OneclawAuthError):
|
|
106
|
+
client.get_secret("x")
|
|
107
|
+
finally:
|
|
108
|
+
client.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@respx.mock
|
|
112
|
+
def test_secret_not_found(api_base: str) -> None:
|
|
113
|
+
vid = _vault_id()
|
|
114
|
+
aid = _agent_id()
|
|
115
|
+
respx.post(f"{api_base}/v1/auth/agent-token").mock(
|
|
116
|
+
return_value=httpx.Response(200, json={"access_token": "jwt", "expires_in": 3600}),
|
|
117
|
+
)
|
|
118
|
+
respx.get(f"{api_base}/v1/vaults/{vid}/secrets/missing").mock(
|
|
119
|
+
return_value=httpx.Response(404, json={"detail": "not found"}),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
client = OneclawClient(
|
|
123
|
+
agent_id=aid,
|
|
124
|
+
api_key="ocv_testkey",
|
|
125
|
+
vault_id=vid,
|
|
126
|
+
base_url=api_base,
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
with pytest.raises(OneclawSecretNotFoundError):
|
|
130
|
+
client.get_secret("missing")
|
|
131
|
+
finally:
|
|
132
|
+
client.close()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@respx.mock
|
|
136
|
+
def test_other_http_error(api_base: str) -> None:
|
|
137
|
+
vid = _vault_id()
|
|
138
|
+
aid = _agent_id()
|
|
139
|
+
respx.post(f"{api_base}/v1/auth/agent-token").mock(
|
|
140
|
+
return_value=httpx.Response(200, json={"access_token": "jwt", "expires_in": 3600}),
|
|
141
|
+
)
|
|
142
|
+
respx.get(f"{api_base}/v1/vaults/{vid}/secrets/boom").mock(
|
|
143
|
+
return_value=httpx.Response(500, json={"detail": "server"}),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
client = OneclawClient(
|
|
147
|
+
agent_id=aid,
|
|
148
|
+
api_key="ocv_testkey",
|
|
149
|
+
vault_id=vid,
|
|
150
|
+
base_url=api_base,
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
with pytest.raises(OneclawError):
|
|
154
|
+
client.get_secret("boom")
|
|
155
|
+
finally:
|
|
156
|
+
client.close()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@respx.mock
|
|
160
|
+
def test_token_refresh_when_near_expiry(api_base: str) -> None:
|
|
161
|
+
vid = _vault_id()
|
|
162
|
+
aid = _agent_id()
|
|
163
|
+
auth = respx.post(f"{api_base}/v1/auth/agent-token").mock(
|
|
164
|
+
side_effect=[
|
|
165
|
+
httpx.Response(200, json={"access_token": "jwt-first", "expires_in": 100}),
|
|
166
|
+
httpx.Response(200, json={"access_token": "jwt-second", "expires_in": 3600}),
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
get_route = respx.get(f"{api_base}/v1/vaults/{vid}/secrets/x").mock(
|
|
170
|
+
return_value=httpx.Response(200, json={"value": "ok"}),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
client = OneclawClient(
|
|
174
|
+
agent_id=aid,
|
|
175
|
+
api_key="ocv_testkey",
|
|
176
|
+
vault_id=vid,
|
|
177
|
+
base_url=api_base,
|
|
178
|
+
)
|
|
179
|
+
try:
|
|
180
|
+
with freeze_time("2024-06-01T12:00:00Z") as frozen:
|
|
181
|
+
assert client.get_secret("x") == "ok"
|
|
182
|
+
frozen.tick(delta=timedelta(seconds=71))
|
|
183
|
+
assert client.get_secret("x") == "ok"
|
|
184
|
+
assert auth.call_count == 2
|
|
185
|
+
assert get_route.call_count == 2
|
|
186
|
+
finally:
|
|
187
|
+
client.close()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for :mod:`oneclaw_crewai._tool`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import respx
|
|
10
|
+
|
|
11
|
+
from oneclaw_crewai._client import OneclawAuthError
|
|
12
|
+
from oneclaw_crewai._tool import OneclawVaultTool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _ids() -> tuple[str, str, str]:
|
|
16
|
+
return str(uuid.uuid4()), str(uuid.uuid4()), "ocv_testkey1234567890"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@respx.mock
|
|
20
|
+
def test_run_returns_secret_on_success() -> None:
|
|
21
|
+
agent_id, vault_id, api_key = _ids()
|
|
22
|
+
base = "https://api.1claw.example"
|
|
23
|
+
respx.post(f"{base}/v1/auth/agent-token").mock(
|
|
24
|
+
return_value=httpx.Response(200, json={"access_token": "jwt", "expires_in": 3600}),
|
|
25
|
+
)
|
|
26
|
+
respx.get(f"{base}/v1/vaults/{vault_id}/secrets/api-keys%2Fopenai").mock(
|
|
27
|
+
return_value=httpx.Response(200, json={"value": "the-key-material"}),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
tool = OneclawVaultTool(
|
|
31
|
+
agent_id=agent_id,
|
|
32
|
+
api_key=api_key,
|
|
33
|
+
vault_id=vault_id,
|
|
34
|
+
base_url=base,
|
|
35
|
+
)
|
|
36
|
+
try:
|
|
37
|
+
assert tool._run("api-keys/openai") == "the-key-material"
|
|
38
|
+
finally:
|
|
39
|
+
if tool._client is not None:
|
|
40
|
+
tool._client.close()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_run_returns_error_string_on_oneclaw_error() -> None:
|
|
44
|
+
agent_id, vault_id, api_key = _ids()
|
|
45
|
+
tool = OneclawVaultTool(
|
|
46
|
+
agent_id=agent_id,
|
|
47
|
+
api_key=api_key,
|
|
48
|
+
vault_id=vault_id,
|
|
49
|
+
base_url="https://api.1claw.example",
|
|
50
|
+
)
|
|
51
|
+
try:
|
|
52
|
+
assert tool._client is not None
|
|
53
|
+
with patch.object(
|
|
54
|
+
tool._client,
|
|
55
|
+
"get_secret",
|
|
56
|
+
side_effect=OneclawAuthError("denied"),
|
|
57
|
+
):
|
|
58
|
+
out = tool._run("any/path")
|
|
59
|
+
assert out.startswith("[1claw error]")
|
|
60
|
+
assert "denied" in out
|
|
61
|
+
finally:
|
|
62
|
+
if tool._client is not None:
|
|
63
|
+
tool._client.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_cache_function_disabled() -> None:
|
|
67
|
+
agent_id, vault_id, api_key = _ids()
|
|
68
|
+
tool = OneclawVaultTool(
|
|
69
|
+
agent_id=agent_id,
|
|
70
|
+
api_key=api_key,
|
|
71
|
+
vault_id=vault_id,
|
|
72
|
+
base_url="https://api.1claw.example",
|
|
73
|
+
)
|
|
74
|
+
try:
|
|
75
|
+
assert callable(tool.cache_function)
|
|
76
|
+
assert tool.cache_function({"path": "x"}, "out") is False
|
|
77
|
+
finally:
|
|
78
|
+
if tool._client is not None:
|
|
79
|
+
tool._client.close()
|