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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/1claw-crewai-tools.svg)](https://pypi.org/project/1claw-crewai-tools/)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/1claw-crewai-tools.svg)](https://pypi.org/project/1claw-crewai-tools/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+ [![CI](https://github.com/1ClawAI/1claw-crewai-tools/actions/workflows/ci.yml/badge.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/1claw-crewai-tools.svg)](https://pypi.org/project/1claw-crewai-tools/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/1claw-crewai-tools.svg)](https://pypi.org/project/1claw-crewai-tools/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![CI](https://github.com/1ClawAI/1claw-crewai-tools/actions/workflows/ci.yml/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/1claw-crewai-tools.svg)](https://pypi.org/project/1claw-crewai-tools/)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/1claw-crewai-tools.svg)](https://pypi.org/project/1claw-crewai-tools/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+ [![CI](https://github.com/1ClawAI/1claw-crewai-tools/actions/workflows/ci.yml/badge.svg)](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,12 @@
1
+ crewai>=0.100.0
2
+ httpx>=0.27
3
+ pydantic>=2.0
4
+
5
+ [dev]
6
+ pytest>=8.0
7
+ respx>=0.21
8
+ ruff>=0.4
9
+ mypy>=1.8
10
+ freezegun>=1.5
11
+ build>=1.0
12
+ twine>=5.0
@@ -0,0 +1,6 @@
1
+ """1Claw CrewAI tools — secure vault access for agents."""
2
+
3
+ from ._tool import OneclawVaultTool
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["OneclawVaultTool"]
@@ -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()