# Plugin Secret Store


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## SecretStore Protocol

Interface any secret backend must satisfy. Distinct from
`PluginConfigStore`: secret **values** are never persisted in the config
DB, never echoed in `config_schema`, and never logged. The substrate
resolves a plugin’s required secrets from a `SecretStore` and injects
them into the worker subprocess env at spawn (CR-12); plugin SDKs read
them from their own process env.

The `scope` parameter is the reserved multi-user seam: the shipped
`LocalSecretStore` ignores it (single-user), while a future per-user /
group store uses it to isolate one principal’s keys from another’s. Same
activation-seam pattern as the host’s `set_session_id` and CR-2’s
workflow-scoped config store.

------------------------------------------------------------------------

### SecretStore

``` python

def SecretStore(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

```

*Protocol for resolving per-plugin secrets (API keys, tokens).*

## LocalSecretStore

Substrate-shipped default. JSON file at `<secrets_dir>/secrets.json`,
with the directory created `0700` and the file `0600`. `secrets_dir`
defaults to `~/.cjm/secrets` (bootstrap fallback); `PluginManager` wires
the project-local `cfg.data_dir / "secrets"` so secrets live alongside
the rest of a project’s substrate data (e.g. `my-plugin/.cjm/secrets/`).

Values are plaintext JSON — encryption-at-rest is a deferred CR-12
follow-up; `0600` is the honest single-user baseline. The on-disk shape
folds the multi-user `scope` dimension so the file format needs no
migration when a scoped store lands:

    {"<scope-or-__default__>": {"<plugin_name>": {"<key>": "<value>"}}}

Keyring / multi-user / workflow-scoped backends implement the same
Protocol and are injected via DI.

------------------------------------------------------------------------

### LocalSecretStore

``` python

def LocalSecretStore(
    secrets_dir:Optional=None, # Directory for secrets.json; None -> ~/.cjm/secrets
):

```

*File-backed default `SecretStore` (0600 JSON under `secrets_dir`).*

------------------------------------------------------------------------

### LocalSecretStore.get_secret

``` python

def get_secret(
    plugin_name:str, # Plugin the secret belongs to
    key:str, # Secret key (typically the env-var name, e.g. GEMINI_API_KEY)
    scope:Optional=None, # Reserved multi-user seam; ignored by the local store
)->Optional: # The secret value, or None if absent

```

*Resolve a secret value.*

------------------------------------------------------------------------

### LocalSecretStore.set_secret

``` python

def set_secret(
    plugin_name:str, # Plugin the secret belongs to
    key:str, # Secret key
    value:str, # Secret value (stored plaintext at 0600)
    scope:Optional=None, # Reserved multi-user seam
)->None:

```

*Persist a secret value.*

------------------------------------------------------------------------

### LocalSecretStore.delete_secret

``` python

def delete_secret(
    plugin_name:str, # Plugin the secret belongs to
    key:str, # Secret key
    scope:Optional=None, # Reserved multi-user seam
)->bool: # True if a secret was removed

```

*Remove a secret, pruning now-empty plugin/scope containers.*

------------------------------------------------------------------------

### LocalSecretStore.list_keys

``` python

def list_keys(
    plugin_name:str, # Plugin to list secrets for
    scope:Optional=None, # Reserved multi-user seam
)->List: # Secret key NAMES (never values)

```

*Return the names of secrets stored for a plugin (never the values).*

``` python
# CR-12: LocalSecretStore satisfies the Protocol; round-trips secrets; enforces
# 0700 dir / 0600 file perms; list_keys never leaks values; scope folds into the
# on-disk structure without affecting the (single-user) local store's default scope.
import tempfile, os as _os, stat as _stat, shutil as _shutil
from pathlib import Path as _Path

_tmp = tempfile.mkdtemp()
try:
    store = LocalSecretStore(_Path(_tmp) / "secrets")
    assert isinstance(store, SecretStore)

    # empty reads
    assert store.get_secret("gemini", "GEMINI_API_KEY") is None
    assert store.list_keys("gemini") == []
    assert store.delete_secret("gemini", "GEMINI_API_KEY") is False

    # round-trip
    store.set_secret("gemini", "GEMINI_API_KEY", "sk-abc123")
    assert store.get_secret("gemini", "GEMINI_API_KEY") == "sk-abc123"
    assert store.list_keys("gemini") == ["GEMINI_API_KEY"]

    # list_keys returns NAMES only (no value leak)
    store.set_secret("gemini", "OTHER", "v")
    assert store.list_keys("gemini") == ["GEMINI_API_KEY", "OTHER"]
    assert "sk-abc123" not in store.list_keys("gemini")

    # perms: dir 0700, file 0600
    assert _stat.S_IMODE(_os.stat(store.secrets_dir).st_mode) == 0o700
    assert _stat.S_IMODE(_os.stat(store.path).st_mode) == 0o600

    # delete prunes
    assert store.delete_secret("gemini", "OTHER") is True
    assert store.list_keys("gemini") == ["GEMINI_API_KEY"]

    # scope folds into structure; named scope isolated from default scope
    store.set_secret("gemini", "GEMINI_API_KEY", "scoped", scope="alice")
    assert store.get_secret("gemini", "GEMINI_API_KEY", scope="alice") == "scoped"
    assert store.get_secret("gemini", "GEMINI_API_KEY") == "sk-abc123"  # default unaffected

    print("CR-12 LocalSecretStore round-trip + perms + scope-fold: PASS")
finally:
    _shutil.rmtree(_tmp, ignore_errors=True)
```

    CR-12 LocalSecretStore round-trip + perms + scope-fold: PASS
