keyward 0.0.1__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.
- keyward-0.0.1/.github/workflows/ci.yml +33 -0
- keyward-0.0.1/.github/workflows/release.yml +23 -0
- keyward-0.0.1/.gitignore +29 -0
- keyward-0.0.1/.python-version +1 -0
- keyward-0.0.1/CHANGELOG.md +37 -0
- keyward-0.0.1/LICENSE +21 -0
- keyward-0.0.1/PKG-INFO +222 -0
- keyward-0.0.1/README.md +174 -0
- keyward-0.0.1/SECURITY.md +30 -0
- keyward-0.0.1/docs/ARCHITECTURE.md +281 -0
- keyward-0.0.1/pyproject.toml +64 -0
- keyward-0.0.1/scripts/verify_swap.py +117 -0
- keyward-0.0.1/src/keyward/__init__.py +7 -0
- keyward-0.0.1/src/keyward/__main__.py +4 -0
- keyward-0.0.1/src/keyward/agent.py +93 -0
- keyward-0.0.1/src/keyward/cli.py +269 -0
- keyward-0.0.1/src/keyward/config.py +27 -0
- keyward-0.0.1/src/keyward/daemon.py +190 -0
- keyward-0.0.1/src/keyward/discovery.py +40 -0
- keyward-0.0.1/src/keyward/inject.py +71 -0
- keyward-0.0.1/src/keyward/py.typed +0 -0
- keyward-0.0.1/src/keyward/store.py +157 -0
- keyward-0.0.1/tests/conftest.py +37 -0
- keyward-0.0.1/tests/test_agent.py +74 -0
- keyward-0.0.1/tests/test_cli.py +120 -0
- keyward-0.0.1/tests/test_daemon.py +265 -0
- keyward-0.0.1/tests/test_inject.py +246 -0
- keyward-0.0.1/uv.lock +1043 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: ${{ matrix.os }} / py${{ matrix.python-version }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest]
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6.0.2
|
|
19
|
+
|
|
20
|
+
- name: Install uv and Python
|
|
21
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: uv sync --dev
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: uv run ruff check .
|
|
28
|
+
|
|
29
|
+
- name: Format check
|
|
30
|
+
run: uv run ruff format --check .
|
|
31
|
+
|
|
32
|
+
- name: Tests
|
|
33
|
+
run: uv run pytest -v
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6.0.2
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
18
|
+
|
|
19
|
+
- name: Build
|
|
20
|
+
run: uv build
|
|
21
|
+
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
keyward-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
|
|
10
|
+
# Tooling
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.tox/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Editors
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
*.swp
|
|
22
|
+
.DS_Store
|
|
23
|
+
|
|
24
|
+
# Local state (never commit)
|
|
25
|
+
*.log
|
|
26
|
+
audit.log
|
|
27
|
+
keyward.sock
|
|
28
|
+
config.toml
|
|
29
|
+
.remember
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to keyward are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `keyward.activate()` — runtime API for in-process token activation, so apps
|
|
11
|
+
can call into the daemon without going through `keyward run`.
|
|
12
|
+
- `keyward.DaemonNotRunning` exception, raised by `activate(strict=True)`.
|
|
13
|
+
- `scripts/verify_swap.py <name>` — standalone Python script for verifying that
|
|
14
|
+
the daemon swaps a token for the real secret.
|
|
15
|
+
- `SECURITY.md`, `CHANGELOG.md`, `py.typed` marker, GitHub Actions CI matrix
|
|
16
|
+
(Linux + macOS, Python 3.11 / 3.12 / 3.13), ruff lint+format config.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `CacheEntry` is now a `NamedTuple` with named fields rather than a positional
|
|
20
|
+
tuple alias.
|
|
21
|
+
- Daemon discovery (`live_daemon_info`, `live_daemon_url`) extracted to
|
|
22
|
+
`keyward.discovery` and reused by both the CLI and `activate()`.
|
|
23
|
+
- Endpoint scheme is validated at `keyward add` time and again in the daemon
|
|
24
|
+
request handler. Only `http://` and `https://` are allowed; bare hostnames
|
|
25
|
+
default to `https://`.
|
|
26
|
+
|
|
27
|
+
## [0.0.1] - 2026-04-21
|
|
28
|
+
|
|
29
|
+
Initial scaffold and v0.2 functionality.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- CLI: `init`, `add`, `list`, `rm`, `rotate`, `restart`, `run`.
|
|
33
|
+
- OS-keychain storage via `keyring`.
|
|
34
|
+
- Token format: `kw_` + 16 hex chars.
|
|
35
|
+
- Local aiohttp proxy with Bearer and `x-api-key` support, SSE streaming.
|
|
36
|
+
- macOS LaunchAgent install/uninstall via `keyward init`.
|
|
37
|
+
- Threat model and architecture documentation.
|
keyward-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sumedh
|
|
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.
|
keyward-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keyward
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Local secret broker that keeps API keys out of files AI agents can read.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sumedhrasal/keyward
|
|
6
|
+
Project-URL: Repository, https://github.com/sumedhrasal/keyward
|
|
7
|
+
Author: sumedh
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 sumedh
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: ai-agents,api-keys,proxy,secrets,security
|
|
31
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
32
|
+
Classifier: Environment :: Console
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: MacOS
|
|
36
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Security
|
|
42
|
+
Requires-Python: >=3.11
|
|
43
|
+
Requires-Dist: aiohttp>=3.9
|
|
44
|
+
Requires-Dist: keyring>=24.0
|
|
45
|
+
Requires-Dist: tomli-w>=1.0
|
|
46
|
+
Requires-Dist: typer>=0.12
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# keyward
|
|
50
|
+
|
|
51
|
+
A local secret broker for developers who run AI coding agents on their own machines.
|
|
52
|
+
|
|
53
|
+
## The goal
|
|
54
|
+
|
|
55
|
+
Keep API keys out of any file an AI agent, co-pilot, or third-party tool can read,
|
|
56
|
+
without adding friction to normal development.
|
|
57
|
+
|
|
58
|
+
Your code never contains real keys. It contains opaque tokens like `kw_ab12cd34`.
|
|
59
|
+
A local daemon swaps the token for the real key only when the outbound request
|
|
60
|
+
goes to an allowlisted endpoint, and records every use.
|
|
61
|
+
|
|
62
|
+
If an agent reads your code, config, or environment, it sees tokens. Tokens are
|
|
63
|
+
useless off-host: they only resolve inside the daemon, which will not forward
|
|
64
|
+
them to destinations you have not explicitly approved.
|
|
65
|
+
|
|
66
|
+
## Why this is not just encryption
|
|
67
|
+
|
|
68
|
+
"One-way encryption you can decrypt" does not exist. What this package actually
|
|
69
|
+
provides is **tokenization plus a scoped, audited forward proxy**. The security
|
|
70
|
+
properties that matter are:
|
|
71
|
+
|
|
72
|
+
- real secrets live in the OS keychain, never on disk in plaintext
|
|
73
|
+
- code and config contain only tokens
|
|
74
|
+
- the daemon forwards to an allowlist, so a leaked token cannot exfiltrate data to a new host
|
|
75
|
+
- every resolution is logged
|
|
76
|
+
- new destinations require explicit user approval
|
|
77
|
+
|
|
78
|
+
## Intended user experience
|
|
79
|
+
|
|
80
|
+
Onboarding is the product. If any step feels heavier than `export KEY=...`,
|
|
81
|
+
it has failed its design goal.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
# one-time setup
|
|
85
|
+
pip install keyward
|
|
86
|
+
keyward init
|
|
87
|
+
|
|
88
|
+
# add a key (prompts for the secret; never passed on the command line)
|
|
89
|
+
keyward add openai --endpoint api.openai.com
|
|
90
|
+
|
|
91
|
+
# run any program with tokens injected as env vars
|
|
92
|
+
keyward run -- python app.py
|
|
93
|
+
keyward run -- pytest
|
|
94
|
+
keyward run -- npm start
|
|
95
|
+
|
|
96
|
+
# rotate a key in place; tokens stay the same so no code changes
|
|
97
|
+
keyward rotate openai
|
|
98
|
+
|
|
99
|
+
# list, remove, inspect
|
|
100
|
+
keyward list
|
|
101
|
+
keyward rm openai
|
|
102
|
+
keyward log --since 1h
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Your code stays boring:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import os, openai
|
|
109
|
+
client = openai.OpenAI() # reads OPENAI_API_KEY and OPENAI_BASE_URL from env
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Under `keyward run`, those variables point at the local daemon with a token.
|
|
113
|
+
Outside `keyward run`, they are not set at all.
|
|
114
|
+
|
|
115
|
+
## Activating from inside your app
|
|
116
|
+
|
|
117
|
+
If you don't want to wrap every command with `keyward run`, call
|
|
118
|
+
`keyward.activate()` once near the top of your app. With the daemon installed
|
|
119
|
+
as a login agent (`keyward init`), this is all you need:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# .env (or your normal env-loading mechanism)
|
|
123
|
+
# OPENAI_API_KEY=kw_ab12cd34
|
|
124
|
+
import os
|
|
125
|
+
from dotenv import load_dotenv
|
|
126
|
+
load_dotenv()
|
|
127
|
+
|
|
128
|
+
import keyward
|
|
129
|
+
keyward.activate() # rewrites OPENAI_BASE_URL to point at the daemon
|
|
130
|
+
|
|
131
|
+
from openai import OpenAI
|
|
132
|
+
client = OpenAI() # transparently goes through keyward
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`activate()` looks at every registered key, and for each one whose `env_vars`
|
|
136
|
+
already hold its token in `os.environ`, sets the matching `base_url_env` to the
|
|
137
|
+
daemon URL. Real keys are left alone. It also exports `KEYWARD_DAEMON` as a
|
|
138
|
+
stable signal you can check from your code (`if "KEYWARD_DAEMON" in os.environ:
|
|
139
|
+
...`) to confirm activation.
|
|
140
|
+
|
|
141
|
+
It returns a `keyward.ActivateResult` with three lists so you can see exactly
|
|
142
|
+
what happened:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
result = keyward.activate(strict=False)
|
|
146
|
+
if result.skipped_no_env:
|
|
147
|
+
print(f"token not found in env for: {result.skipped_no_env}")
|
|
148
|
+
print("Did you load your .env file before calling activate()?")
|
|
149
|
+
# result.activated — keys that are now routing through the daemon
|
|
150
|
+
# result.skipped_no_env — keys whose token was not found in any env var
|
|
151
|
+
# result.skipped_no_base_url — keys with no base_url_env configured
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
If no daemon is running, `activate()` raises `keyward.DaemonNotRunning`. Pass
|
|
155
|
+
`strict=False` to return an empty result instead — useful for code that should
|
|
156
|
+
work both with and without keyward installed.
|
|
157
|
+
|
|
158
|
+
## What works today (v0.2)
|
|
159
|
+
|
|
160
|
+
| Area | Status |
|
|
161
|
+
|-----------------------|------------------------------------------------------------------------|
|
|
162
|
+
| CLI commands | `init`, `add`, `list`, `rm`, `rotate`, `restart`, `run` all functional |
|
|
163
|
+
| Keychain storage | macOS Keychain, Windows Credential Manager, Linux libsecret via `keyring` |
|
|
164
|
+
| Proxy forwarding | Authorization: Bearer and x-api-key, on both ingress and egress |
|
|
165
|
+
| Streaming | Server-Sent Events forwarded without buffering |
|
|
166
|
+
| Login agent | macOS LaunchAgent install/uninstall/kickstart via `keyward init` |
|
|
167
|
+
| Daemon reuse | `keyward run` reuses a live daemon; else spawns ephemeral |
|
|
168
|
+
| Audit log | Stub only (prints TODO; no log is written yet) |
|
|
169
|
+
| Endpoint enforcement | Each token is bound to one host at `keyward add` time; the daemon ignores the request host and always forwards to the stored endpoint — so a token cannot be used against a different host |
|
|
170
|
+
| Multi-endpoint allowlist + approval flow | Not yet — v0.3 scope; see ARCHITECTURE.md |
|
|
171
|
+
| Linux systemd / Windows scheduled task | Not wired up yet |
|
|
172
|
+
| Websocket proxying | Returns 501; HTTP only for now |
|
|
173
|
+
| Request body streaming| Buffered; fine for LLM chat, not for large uploads |
|
|
174
|
+
| Caller attestation | Trust-anything on localhost; see ARCHITECTURE.md |
|
|
175
|
+
|
|
176
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design, threat
|
|
177
|
+
model, and the list of deferred items.
|
|
178
|
+
|
|
179
|
+
## Verifying the key swap
|
|
180
|
+
|
|
181
|
+
The sharpest test is to point keyward at a request-echoing endpoint and look
|
|
182
|
+
for your raw secret (and the absence of the token) in the response.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# pick a distinctive fake secret so you can spot it in the echo
|
|
186
|
+
keyward add echotest --endpoint httpbin.org
|
|
187
|
+
# at the prompt, enter: sk-fake-secret-12345
|
|
188
|
+
|
|
189
|
+
keyward restart # only needed if a LaunchAgent daemon is already running
|
|
190
|
+
|
|
191
|
+
keyward run -- curl -s "$ECHOTEST_BASE_URL/anything" \
|
|
192
|
+
-H "Authorization: Bearer $ECHOTEST_API_KEY"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
In the JSON response, under `headers.Authorization`:
|
|
196
|
+
- `Bearer sk-fake-secret-12345` means the swap worked.
|
|
197
|
+
- Anything starting with `Bearer kw_` means the swap did not happen (bug).
|
|
198
|
+
|
|
199
|
+
For the Anthropic-style (x-api-key):
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
keyward add echotestx --endpoint httpbin.org --auth-style x-api-key
|
|
203
|
+
keyward restart
|
|
204
|
+
keyward run -- curl -s "$ECHOTESTX_BASE_URL/anything" \
|
|
205
|
+
-H "x-api-key: $ECHOTESTX_API_KEY"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Check `headers.X-Api-Key` in the response.
|
|
209
|
+
|
|
210
|
+
Clean up with `keyward rm echotest -y && keyward rm echotestx -y`.
|
|
211
|
+
|
|
212
|
+
There is also a Python equivalent that uses `keyward.activate()`:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
keyward add echotest --endpoint httpbin.org
|
|
216
|
+
keyward restart
|
|
217
|
+
uv run python scripts/verify_swap.py echotest
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT. See [LICENSE](LICENSE).
|
keyward-0.0.1/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# keyward
|
|
2
|
+
|
|
3
|
+
A local secret broker for developers who run AI coding agents on their own machines.
|
|
4
|
+
|
|
5
|
+
## The goal
|
|
6
|
+
|
|
7
|
+
Keep API keys out of any file an AI agent, co-pilot, or third-party tool can read,
|
|
8
|
+
without adding friction to normal development.
|
|
9
|
+
|
|
10
|
+
Your code never contains real keys. It contains opaque tokens like `kw_ab12cd34`.
|
|
11
|
+
A local daemon swaps the token for the real key only when the outbound request
|
|
12
|
+
goes to an allowlisted endpoint, and records every use.
|
|
13
|
+
|
|
14
|
+
If an agent reads your code, config, or environment, it sees tokens. Tokens are
|
|
15
|
+
useless off-host: they only resolve inside the daemon, which will not forward
|
|
16
|
+
them to destinations you have not explicitly approved.
|
|
17
|
+
|
|
18
|
+
## Why this is not just encryption
|
|
19
|
+
|
|
20
|
+
"One-way encryption you can decrypt" does not exist. What this package actually
|
|
21
|
+
provides is **tokenization plus a scoped, audited forward proxy**. The security
|
|
22
|
+
properties that matter are:
|
|
23
|
+
|
|
24
|
+
- real secrets live in the OS keychain, never on disk in plaintext
|
|
25
|
+
- code and config contain only tokens
|
|
26
|
+
- the daemon forwards to an allowlist, so a leaked token cannot exfiltrate data to a new host
|
|
27
|
+
- every resolution is logged
|
|
28
|
+
- new destinations require explicit user approval
|
|
29
|
+
|
|
30
|
+
## Intended user experience
|
|
31
|
+
|
|
32
|
+
Onboarding is the product. If any step feels heavier than `export KEY=...`,
|
|
33
|
+
it has failed its design goal.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
# one-time setup
|
|
37
|
+
pip install keyward
|
|
38
|
+
keyward init
|
|
39
|
+
|
|
40
|
+
# add a key (prompts for the secret; never passed on the command line)
|
|
41
|
+
keyward add openai --endpoint api.openai.com
|
|
42
|
+
|
|
43
|
+
# run any program with tokens injected as env vars
|
|
44
|
+
keyward run -- python app.py
|
|
45
|
+
keyward run -- pytest
|
|
46
|
+
keyward run -- npm start
|
|
47
|
+
|
|
48
|
+
# rotate a key in place; tokens stay the same so no code changes
|
|
49
|
+
keyward rotate openai
|
|
50
|
+
|
|
51
|
+
# list, remove, inspect
|
|
52
|
+
keyward list
|
|
53
|
+
keyward rm openai
|
|
54
|
+
keyward log --since 1h
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Your code stays boring:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import os, openai
|
|
61
|
+
client = openai.OpenAI() # reads OPENAI_API_KEY and OPENAI_BASE_URL from env
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Under `keyward run`, those variables point at the local daemon with a token.
|
|
65
|
+
Outside `keyward run`, they are not set at all.
|
|
66
|
+
|
|
67
|
+
## Activating from inside your app
|
|
68
|
+
|
|
69
|
+
If you don't want to wrap every command with `keyward run`, call
|
|
70
|
+
`keyward.activate()` once near the top of your app. With the daemon installed
|
|
71
|
+
as a login agent (`keyward init`), this is all you need:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# .env (or your normal env-loading mechanism)
|
|
75
|
+
# OPENAI_API_KEY=kw_ab12cd34
|
|
76
|
+
import os
|
|
77
|
+
from dotenv import load_dotenv
|
|
78
|
+
load_dotenv()
|
|
79
|
+
|
|
80
|
+
import keyward
|
|
81
|
+
keyward.activate() # rewrites OPENAI_BASE_URL to point at the daemon
|
|
82
|
+
|
|
83
|
+
from openai import OpenAI
|
|
84
|
+
client = OpenAI() # transparently goes through keyward
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`activate()` looks at every registered key, and for each one whose `env_vars`
|
|
88
|
+
already hold its token in `os.environ`, sets the matching `base_url_env` to the
|
|
89
|
+
daemon URL. Real keys are left alone. It also exports `KEYWARD_DAEMON` as a
|
|
90
|
+
stable signal you can check from your code (`if "KEYWARD_DAEMON" in os.environ:
|
|
91
|
+
...`) to confirm activation.
|
|
92
|
+
|
|
93
|
+
It returns a `keyward.ActivateResult` with three lists so you can see exactly
|
|
94
|
+
what happened:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = keyward.activate(strict=False)
|
|
98
|
+
if result.skipped_no_env:
|
|
99
|
+
print(f"token not found in env for: {result.skipped_no_env}")
|
|
100
|
+
print("Did you load your .env file before calling activate()?")
|
|
101
|
+
# result.activated — keys that are now routing through the daemon
|
|
102
|
+
# result.skipped_no_env — keys whose token was not found in any env var
|
|
103
|
+
# result.skipped_no_base_url — keys with no base_url_env configured
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If no daemon is running, `activate()` raises `keyward.DaemonNotRunning`. Pass
|
|
107
|
+
`strict=False` to return an empty result instead — useful for code that should
|
|
108
|
+
work both with and without keyward installed.
|
|
109
|
+
|
|
110
|
+
## What works today (v0.2)
|
|
111
|
+
|
|
112
|
+
| Area | Status |
|
|
113
|
+
|-----------------------|------------------------------------------------------------------------|
|
|
114
|
+
| CLI commands | `init`, `add`, `list`, `rm`, `rotate`, `restart`, `run` all functional |
|
|
115
|
+
| Keychain storage | macOS Keychain, Windows Credential Manager, Linux libsecret via `keyring` |
|
|
116
|
+
| Proxy forwarding | Authorization: Bearer and x-api-key, on both ingress and egress |
|
|
117
|
+
| Streaming | Server-Sent Events forwarded without buffering |
|
|
118
|
+
| Login agent | macOS LaunchAgent install/uninstall/kickstart via `keyward init` |
|
|
119
|
+
| Daemon reuse | `keyward run` reuses a live daemon; else spawns ephemeral |
|
|
120
|
+
| Audit log | Stub only (prints TODO; no log is written yet) |
|
|
121
|
+
| Endpoint enforcement | Each token is bound to one host at `keyward add` time; the daemon ignores the request host and always forwards to the stored endpoint — so a token cannot be used against a different host |
|
|
122
|
+
| Multi-endpoint allowlist + approval flow | Not yet — v0.3 scope; see ARCHITECTURE.md |
|
|
123
|
+
| Linux systemd / Windows scheduled task | Not wired up yet |
|
|
124
|
+
| Websocket proxying | Returns 501; HTTP only for now |
|
|
125
|
+
| Request body streaming| Buffered; fine for LLM chat, not for large uploads |
|
|
126
|
+
| Caller attestation | Trust-anything on localhost; see ARCHITECTURE.md |
|
|
127
|
+
|
|
128
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design, threat
|
|
129
|
+
model, and the list of deferred items.
|
|
130
|
+
|
|
131
|
+
## Verifying the key swap
|
|
132
|
+
|
|
133
|
+
The sharpest test is to point keyward at a request-echoing endpoint and look
|
|
134
|
+
for your raw secret (and the absence of the token) in the response.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# pick a distinctive fake secret so you can spot it in the echo
|
|
138
|
+
keyward add echotest --endpoint httpbin.org
|
|
139
|
+
# at the prompt, enter: sk-fake-secret-12345
|
|
140
|
+
|
|
141
|
+
keyward restart # only needed if a LaunchAgent daemon is already running
|
|
142
|
+
|
|
143
|
+
keyward run -- curl -s "$ECHOTEST_BASE_URL/anything" \
|
|
144
|
+
-H "Authorization: Bearer $ECHOTEST_API_KEY"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
In the JSON response, under `headers.Authorization`:
|
|
148
|
+
- `Bearer sk-fake-secret-12345` means the swap worked.
|
|
149
|
+
- Anything starting with `Bearer kw_` means the swap did not happen (bug).
|
|
150
|
+
|
|
151
|
+
For the Anthropic-style (x-api-key):
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
keyward add echotestx --endpoint httpbin.org --auth-style x-api-key
|
|
155
|
+
keyward restart
|
|
156
|
+
keyward run -- curl -s "$ECHOTESTX_BASE_URL/anything" \
|
|
157
|
+
-H "x-api-key: $ECHOTESTX_API_KEY"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Check `headers.X-Api-Key` in the response.
|
|
161
|
+
|
|
162
|
+
Clean up with `keyward rm echotest -y && keyward rm echotestx -y`.
|
|
163
|
+
|
|
164
|
+
There is also a Python equivalent that uses `keyward.activate()`:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
keyward add echotest --endpoint httpbin.org
|
|
168
|
+
keyward restart
|
|
169
|
+
uv run python scripts/verify_swap.py echotest
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
If you believe you've found a security issue in keyward, please do **not** open
|
|
6
|
+
a public GitHub issue. Instead, email the maintainer at
|
|
7
|
+
**srasal3@gatech.edu** with:
|
|
8
|
+
|
|
9
|
+
- a description of the issue,
|
|
10
|
+
- the keyward version (`keyward --version`) and your OS,
|
|
11
|
+
- minimal reproduction steps,
|
|
12
|
+
- the impact you think it has.
|
|
13
|
+
|
|
14
|
+
You'll get an acknowledgement within a few days. Once a fix is shipped, the
|
|
15
|
+
disclosure will be credited unless you ask otherwise.
|
|
16
|
+
|
|
17
|
+
## Threat model
|
|
18
|
+
|
|
19
|
+
keyward is designed for a specific threat model — see
|
|
20
|
+
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full table. Briefly:
|
|
21
|
+
|
|
22
|
+
- **In scope:** an attacker who can read the source tree, environment
|
|
23
|
+
variables, and `~/.config/keyward/config.toml`. Such an attacker should see
|
|
24
|
+
only opaque tokens, not real keys, and should not be able to use the tokens
|
|
25
|
+
to reach a destination the user has not approved.
|
|
26
|
+
- **Out of scope:** an attacker with root, the ability to attach a debugger to
|
|
27
|
+
the keyward daemon, or write access to `~/.config/keyward/config.toml`. The
|
|
28
|
+
OS keychain is the trust boundary.
|
|
29
|
+
|
|
30
|
+
If you're unsure whether a behavior counts as a vulnerability, email anyway.
|