agentconfigsafe 0.1.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,41 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions: {}
8
+
9
+ jobs:
10
+ build:
11
+ name: Build distribution
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: python -m pip install --upgrade build
19
+ - run: python -m build
20
+ - run: python -m pip install --upgrade twine
21
+ - run: twine check dist/*
22
+ - uses: actions/upload-artifact@v4
23
+ with:
24
+ name: python-package-distributions
25
+ path: dist/
26
+
27
+ publish-to-pypi:
28
+ name: Publish Python distribution to PyPI
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/p/agentconfigsafe
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: python-package-distributions
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ appconfig
9
+ appconfig.lock
@@ -0,0 +1,349 @@
1
+ # agentsafe
2
+
3
+ ## What this is
4
+
5
+ `agentsafe` is a Python library for securely storing and retrieving configuration
6
+ parameters (API keys, connection strings, credentials, etc.) used by AI agent
7
+ applications. It never invents or holds its own encryption keys — every
8
+ encrypt/decrypt operation is delegated to a customer-managed key in a KMS
9
+ backend. **OCI Key Management Service (KMS)** is the first backend; AWS KMS,
10
+ GCP Cloud KMS, and Azure Key Vault are supported as pluggable alternatives
11
+ (see "KMS provider plugin architecture"). Encrypted values are persisted to a
12
+ local file named `appconfig`.
13
+
14
+ Agent developers use `agentsafe` two ways:
15
+ - **CLI** — `init`, `set`/`store`, `get`/`retrieve`, `remove` config parameters
16
+ from a terminal or setup script.
17
+ - **SDK** — import `agentsafe` in a Python application to fetch decrypted
18
+ config values at runtime.
19
+
20
+ > Status: this repo is currently empty. This file is both the working
21
+ > architecture guide and the spec to scaffold against. All decisions below
22
+ > were made deliberately (see rationale inline) — update this doc, don't
23
+ > silently drift from it, if a decision changes during implementation.
24
+
25
+ ## Core concepts
26
+
27
+ - **KMS-backed encryption, no local keys.** `agentsafe` calls the configured
28
+ provider's Encrypt/Decrypt API for every operation. It stores key/vault
29
+ identifiers, never key material.
30
+ - **`appconfig` file.** The on-disk artifact holding all encrypted parameters
31
+ for a project — a **JSON** document mapping each config name to a
32
+ provider-tagged ciphertext envelope (see schema below). No plaintext value
33
+ is ever written into this file. Chosen over YAML/TOML because it needs no
34
+ extra dependency and the file is machine-written/machine-read only — no one
35
+ hand-edits a file full of opaque ciphertext.
36
+ - **OCI profile-based auth (for the OCI provider).** Relies on the standard
37
+ OCI SDK/CLI config file (`~/.oci/config`) and profiles — `agentsafe` does
38
+ not implement its own auth. Each provider owns its own auth mechanism (see
39
+ below); "profile" is an OCI-specific concept, not a cross-provider one.
40
+ - **Four required OCI settings**, independently configurable by the end
41
+ user (not hardcoded): **profile name**, **KMS compartment OCID** (OCID
42
+ only for v1 — no name-to-OCID resolution; see rationale below), **KMS vault
43
+ crypto endpoint** (the per-vault URL used for Encrypt/Decrypt, distinct
44
+ from the KMS management endpoint), and **KMS key OCID**. The key OCID is
45
+ required for Encrypt; it is also recorded in each OCI ciphertext envelope
46
+ so the correct key can be used for Decrypt.
47
+ - **Pluggable KMS backend via entry-point plugins.** See the dedicated
48
+ section below — this is a first-class, public-API-level design constraint,
49
+ not an implementation detail.
50
+
51
+ ## KMS provider plugin architecture
52
+
53
+ OCI KMS is the first backend, not the only one, and the plugin model is
54
+ **fully open to third parties** — someone can `pip install
55
+ agentsafe-kms-hashicorp-vault` and it registers itself without any agentsafe
56
+ core change. This is a deliberate, documented public API, not just an
57
+ internal abstraction:
58
+
59
+ ```python
60
+ # kms/base.py
61
+ class KMSProvider(Protocol):
62
+ def encrypt(self, plaintext: str) -> EncryptedBlob: ...
63
+ def decrypt(self, blob: EncryptedBlob) -> str: ...
64
+ ```
65
+
66
+ **Discovery mechanism: `importlib.metadata` entry points**, group name
67
+ `agentsafe.kms_providers`. Each entry point maps a provider name (`"oci"`,
68
+ `"aws"`, ...) to a class implementing `KMSProvider`.
69
+
70
+ - **OCI is the only bundled and registered provider in v1.**
71
+ `oci_provider.py` lives inside `agentsafe/kms/` and registers in the
72
+ `agentsafe.kms_providers` entry-point group via an entry declared in
73
+ `agentsafe`'s own `pyproject.toml` — dogfooding the exact mechanism a
74
+ third party would use. AWS, GCP, and Azure are future providers and have no
75
+ v1 modules or entry points.
76
+ - **Lazy loading — a provider SDK import happens only when that provider is selected.**
77
+ Discovery (`importlib.metadata.entry_points(group="agentsafe.kms_providers")`)
78
+ only reads entry-point *names*; it never calls `.load()` until that
79
+ specific provider is chosen (via `AGENTSAFE_KMS_PROVIDER` or equivalent).
80
+ Selecting OCI without its optional SDK installed raises a clear
81
+ `ConfigError` (for example, `"provider 'oci' requires oci: pip install
82
+ agentconfigsafe[oci]"`) at selection time.
83
+ - **Name collisions are a hard error at discovery time.** If two distinct
84
+ entry points claim the same provider name (e.g. a third-party package
85
+ also registers `"aws"`), raise a `ConfigError` naming both conflicting
86
+ packages and require the user to uninstall one. Never silently shadow one
87
+ provider with another — this is a security-critical selection, not a
88
+ cosmetic conflict, so it must never resolve by implicit import order.
89
+ - **Each provider owns its own auth/config.** OCI: profile + compartment +
90
+ crypto endpoint + key OCID. AWS: profile/region + key ARN. GCP: service account/ADC +
91
+ key resource name. Azure: `DefaultAzureCredential` chain + vault URL +
92
+ key name. `store.py`/`sdk.py`/`cli.py` never know these details.
93
+ - **The OCI SDK is an optional extra**: `agentconfigsafe[oci]`. Installing bare
94
+ `agentsafe` pulls in no cloud-provider SDK. Future providers will add their
95
+ own optional extras when implemented.
96
+ - **Adding a backend (built-in or third-party) never requires touching**
97
+ `store.py`, `sdk.py`, or `cli.py` — only a new module implementing
98
+ `KMSProvider` plus an entry-point registration.
99
+
100
+ ### `EncryptedBlob` schema (the plugin contract)
101
+
102
+ Each `appconfig` entry is a **provider-owned opaque envelope** —
103
+ `store.py` never inspects or validates provider-specific fields, only routes
104
+ by the `provider` tag:
105
+
106
+ ```json
107
+ {
108
+ "schema_version": 1,
109
+ "entries": {
110
+ "OPENAI_API_KEY": {
111
+ "provider": "oci",
112
+ "ciphertext": "<base64>",
113
+ "metadata": { "key_id": "...", "key_version": "..." }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ - `schema_version` is a top-level field on the *file*, independent of any
120
+ provider's `metadata` shape. V1 records this version but does not perform
121
+ whole-file or provider-envelope schema validation; providers remain the
122
+ authority for their own opaque envelopes. Future releases may use it for
123
+ explicit migration or compatibility handling.
124
+ - `metadata` is a free-form dict whose shape is entirely up to the provider
125
+ that wrote it (OCI: `key_id`/`key_version`; AWS: `key_arn`; GCP: `key_name`;
126
+ Azure: `key_id`/`key_version`). Centralizing a typed schema per provider in
127
+ core was rejected — it would mean every new provider, including
128
+ third-party ones, requires a core code change, defeating the plugin model.
129
+
130
+ ## Architecture
131
+
132
+ Target package layout (`src/` layout):
133
+
134
+ ```
135
+ agentsafe/
136
+ __init__.py # public SDK surface: AgentSafe class + convenience functions
137
+ config.py # resolves agentsafe's own settings: profile, compartment, crypto endpoint, key ID, kms provider
138
+ store.py # appconfig file: JSON schema, atomic write (temp file + os.replace), advisory file lock
139
+ sdk.py # AgentSafe class: init(), set(key, value), get(key), remove(key), list_keys()
140
+ cli.py # Typer CLI: init, set/store, get/retrieve, remove/rm, list
141
+ exceptions.py # AgentSafeError hierarchy
142
+ kms/
143
+ __init__.py # entry-point discovery + factory: get_provider(name) -> KMSProvider
144
+ base.py # KMSProvider Protocol + EncryptedBlob type
145
+ oci_provider.py # OCI KMS implementation (oci.key_management + oci.kms_crypto), extra: agentconfigsafe[oci]
146
+ tests/
147
+ test_kms_contract.py # fake in-memory KMSProvider; exercises store.py/sdk.py/cli.py logic, no real crypto
148
+ test_kms_oci.py # unittest.mock.patch on the oci client; verifies request/response mapping only
149
+ test_store.py # appconfig schema/round-trip, concurrency/locking
150
+ test_sdk.py
151
+ test_cli.py
152
+ pyproject.toml
153
+ ```
154
+
155
+ Data flow for a `get`:
156
+ `SDK/CLI call` → `store.py` reads `appconfig`, finds the entry for the key →
157
+ `kms/__init__.py` resolves the entry's `provider` tag to a `KMSProvider`
158
+ (loading its entry point lazily) → `provider.decrypt(blob)` calls the
159
+ backend's decrypt API using that provider's own credentials/endpoint →
160
+ plaintext returned to the caller only, never written back to disk or logged.
161
+
162
+ ## Settings & file locations
163
+
164
+ - **`~/.agentsafe/config`** (INI-style) — machine-wide default settings:
165
+ `kms_provider`, `profile`, `compartment`, `crypto_endpoint`, `key_id`, and future
166
+ per-provider settings. Written by `agentsafe init`, analogous to
167
+ `~/.oci/config`/`~/.aws/config`.
168
+ - **No project-local settings file in v1.** A project needing a different
169
+ provider/profile/compartment than the machine default overrides via env
170
+ vars or CLI flags — no second settings-file tier to design/maintain until
171
+ real usage shows env vars aren't enough.
172
+ - **`appconfig` is per-project, cwd-based** (e.g. `./appconfig`) — every
173
+ project keeps its own secret set. The global settings file only supplies
174
+ *which KMS to talk to*; it never determines *which secrets* are visible.
175
+
176
+ Settings resolution order (first match wins):
177
+ 1. Explicit constructor/CLI arguments
178
+ 2. Environment variables: `AGENTSAFE_KMS_PROVIDER`, `AGENTSAFE_PROFILE`, `AGENTSAFE_COMPARTMENT`, `AGENTSAFE_CRYPTO_ENDPOINT`, `AGENTSAFE_KEY_ID`
179
+ 3. `~/.agentsafe/config`
180
+ 4. For `kms_provider` only, select `oci` when it remains unspecified.
181
+ For every other required setting, raise a clear `ConfigError` — never
182
+ silently fall back to defaults for security-relevant settings.
183
+
184
+ ## SDK usage (target shape)
185
+
186
+ ```python
187
+ from agentsafe import AgentSafe
188
+
189
+ safe = AgentSafe(
190
+ kms_provider="oci", # selected by default when omitted
191
+ profile="DEFAULT",
192
+ compartment="ocid1.compartment.oc1..xxxx", # OCID only, no name resolution (see below)
193
+ crypto_endpoint="https://<vault>-crypto.kms.<region>.oraclecloud.com",
194
+ key_id="ocid1.key.oc1..<key-ocid>",
195
+ )
196
+
197
+ safe.set("OPENAI_API_KEY", "sk-...") # value: str only (see "Value types")
198
+ value = safe.get("OPENAI_API_KEY") # raises KeyNotFoundError if absent
199
+ safe.remove("OPENAI_API_KEY")
200
+ ```
201
+
202
+ `profile`/`compartment`/`crypto_endpoint`/`key_id` are OCI-provider settings; other
203
+ providers take their own equivalent kwargs (e.g. AWS: `region`, `key_arn`).
204
+ `AgentSafe.__init__` forwards whatever settings are relevant to
205
+ `kms.get_provider(kms_provider, **settings)` rather than hardcoding OCI's
206
+ argument names into the public SDK surface.
207
+
208
+ **Value types: strings only.** `set(key, value)` requires `value: str`.
209
+ Structured secrets (JSON blobs, etc.) are the caller's responsibility to
210
+ serialize first. This matches the primary use case (API keys, tokens,
211
+ connection strings) and avoids a serialization-format decision and CLI
212
+ argument-parsing question that arbitrary JSON values would force.
213
+
214
+ **Config names: arbitrary non-empty strings.** `set`, `get`, and `remove`
215
+ accept any non-empty string as a key name; names need not be valid environment
216
+ variable identifiers.
217
+
218
+ **Missing key: raise, don't return `None`.** `get()` on an absent key raises
219
+ `KeyNotFoundError`. A silent `None` for a missing secret is exactly the kind
220
+ of thing that turns into a confusing downstream failure (e.g. an agent
221
+ calling an API with `api_key=None`) instead of a clear error where the
222
+ problem actually is.
223
+
224
+ **Compartment: OCID only, no name resolution in v1.** Resolving a friendly
225
+ name would need an extra `oci.identity` `list_compartments` call, IAM
226
+ permissions the caller might not have, and is ambiguous across nested
227
+ compartments. The OCID is what the Encrypt/Decrypt calls need anyway, and
228
+ it's a one-time copy from the OCI console during `init`.
229
+
230
+ ## CLI usage (target shape)
231
+
232
+ Built with **Typer** (type-hint-driven, built on Click so hidden-input
233
+ prompts etc. are still available; fits a type-hint-first codebase with less
234
+ boilerplate than raw Click).
235
+
236
+ ```
237
+ agentsafe init --profile DEFAULT --compartment <ocid> --crypto-endpoint <url> --key-id <ocid>
238
+ agentsafe set OPENAI_API_KEY [VALUE] # prompts (hidden input) if VALUE omitted
239
+ agentsafe get OPENAI_API_KEY
240
+ agentsafe remove OPENAI_API_KEY
241
+ agentsafe list # key names only — see below
242
+ ```
243
+
244
+ `init` writes the resolved settings into `~/.agentsafe/config` and creates an
245
+ empty `appconfig` in the current directory. It fails if either target already
246
+ exists; it never overwrites an existing configuration or secret store. It does not create cloud
247
+ resources (vault, key, compartment) — those are assumed to already exist and
248
+ be reachable via the given profile/credentials.
249
+
250
+ **`list` shows key names only, never decrypted values.** It does not call
251
+ Decrypt at all — fast, and it never puts plaintext secrets on a
252
+ terminal/screen-recording/CI log just because someone wanted to see what's
253
+ configured.
254
+
255
+ ## Security requirements (non-negotiable for any change)
256
+
257
+ - Plaintext values must never be written to disk, logs, shell history files,
258
+ or exception messages/tracebacks.
259
+ - `appconfig` contains ciphertext and metadata only — never plaintext, never
260
+ raw key material.
261
+ - Decryption happens only in memory at the moment the SDK/CLI caller requests
262
+ a value; do not cache decrypted plaintext beyond that call's return.
263
+ - Key material and key versions live only in the KMS backend (whichever
264
+ provider is configured); `agentsafe` stores only identifiers (key
265
+ OCID/ARN/resource name, vault/endpoint), never key bytes, regardless of
266
+ provider.
267
+ - `set`/`store` should support a hidden-input prompt or stdin piping as an
268
+ alternative to a bare CLI argument, since arguments are visible via `ps`
269
+ and shell history — document this tradeoff wherever the argument form is
270
+ offered.
271
+ - Never swallow a KMS provider's authentication/authorization errors — wrap
272
+ them in a typed `KMSError`/`ConfigError` using exception chaining, retaining
273
+ the original provider exception as the cause. Do not fall back to an
274
+ insecure path. This applies uniformly across providers, not just OCI.
275
+ - `list` never decrypts (see CLI usage above).
276
+ - Provider name collisions are a hard error, never silently resolved (see
277
+ plugin architecture above).
278
+ - `~/.agentsafe/config`, `appconfig`, temporary files used to replace
279
+ `appconfig`, and lock files must be created owner-readable/writable only
280
+ (mode `0600`) where the operating system supports POSIX file permissions.
281
+
282
+ ## Out of scope for v1 (deliberately deferred)
283
+
284
+ - **Key rotation / re-encryption.** No `agentsafe rotate` command. Decrypt
285
+ already works regardless of which key version encrypted a given entry (as
286
+ long as that version isn't disabled/deleted), so this is a hygiene
287
+ improvement to add later once there's real usage to inform the design, not
288
+ a correctness blocker now.
289
+ - **Bulk import/export** (e.g. `.env` file import, bulk env-var export).
290
+ Single-key `set`/`get`/`remove` covers the core use case; bulk import also
291
+ raises its own security UX question (a plaintext `.env` source file is
292
+ exactly what agentsafe exists to replace) that shouldn't block v1.
293
+ - **Compartment name → OCID resolution.** OCID only for now (see SDK usage
294
+ section).
295
+ - **Project-local settings file.** Global `~/.agentsafe/config` plus env
296
+ vars/CLI flags only (see "Settings & file locations").
297
+
298
+ ## Concurrency & durability
299
+
300
+ - **Atomic writes**: write to a temp file, then `os.replace()` — never leave
301
+ `appconfig` half-written even on a crash mid-write.
302
+ - **Advisory file lock** (e.g. the `filelock` library) held across the full
303
+ read-modify-write cycle of any `set`/`remove`, so two concurrent CLI/SDK
304
+ writers (e.g. a CLI `set` racing an SDK write from a running app) can't
305
+ silently clobber each other's change.
306
+
307
+ ## Dependencies (expected)
308
+
309
+ - `oci` — the OCI Python SDK (`oci.key_management` + `oci.kms_crypto`). Used
310
+ by `kms/oci_provider.py` only, behind the `agentconfigsafe[oci]` extra.
311
+ - Future provider SDKs are added as optional extras only when their providers
312
+ are implemented (for example, `boto3` for AWS).
313
+ - `typer` — CLI framework.
314
+ - `filelock` — advisory locking for `appconfig` writes.
315
+ - No custom cryptography implementation — all encrypt/decrypt is delegated to
316
+ the configured KMS provider.
317
+ - PyPI distribution name **`agentconfigsafe`**; the Python import package and
318
+ CLI command remain **`agentsafe`**.
319
+ - **Python 3.10+** — modern `X | Y` union hints without `__future__` imports,
320
+ and the clean keyword-arg form of `importlib.metadata.entry_points(group=...)`.
321
+
322
+ ## Dev commands
323
+
324
+ Fill these in as the project is scaffolded; keep this section accurate.
325
+
326
+ - Install (editable, with dev deps): `pip install -e ".[dev]"`
327
+ - Run tests: `pytest`
328
+ - Lint/format: `ruff check .` and `ruff format .`
329
+ - Type-check: `mypy src/agentsafe`
330
+
331
+ ## Conventions
332
+
333
+ - Type hints on all public functions/methods; docstrings on the public SDK
334
+ surface (`AgentSafe` and its methods).
335
+ - Raise typed exceptions from `agentsafe.exceptions` (`KeyNotFoundError`,
336
+ `KMSError`, `ConfigError`, ...) rather than bare `Exception`/`ValueError`.
337
+ - **Test the provider-agnostic logic once, against the `KMSProvider`
338
+ contract** (`test_kms_contract.py`), using a trivial fake in-memory
339
+ provider — no real crypto, no cloud-specific mocking library. Each
340
+ concrete provider then gets its own thin test file that only verifies it
341
+ maps correctly to/from its cloud SDK's request/response shape
342
+ (`unittest.mock.patch` on that SDK's client — no `moto` or other
343
+ provider-specific mocking framework needed).
344
+ - Reserve real KMS calls for a separate integration test suite gated behind
345
+ an explicit marker/env var (e.g. `AGENTSAFE_RUN_INTEGRATION=1`), since
346
+ those require live cloud credentials and a real vault/key.
347
+ - Keep `store.py` (file format) and `kms/` (provider calls) decoupled from
348
+ `sdk.py`/`cli.py` (user-facing surface) so either can be tested and
349
+ evolved independently.