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.
@@ -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
@@ -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).
@@ -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.