Metadata-Version: 2.4
Name: lockersm
Version: 2.0.5
Summary: Official Locker Secrets Python SDK
Author-email: CyStack <contact@locker.io>
License-Expression: Apache-2.0
Project-URL: Homepage, https://locker.io
Project-URL: Documentation, https://pypi.org/project/lockersm/
Project-URL: Support, https://support.locker.io
Project-URL: Source, https://git.cystack.org/locker/secrets-sdk/python
Keywords: locker,secrets,passwords,security
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=44.0.0
Requires-Dist: requests<3,>=2.32.0
Dynamic: license-file

# Locker Secrets Python SDK

[![PyPI](https://img.shields.io/pypi/v/lockersm.svg)](https://pypi.org/project/lockersm/)
[![Python](https://img.shields.io/pypi/pyversions/lockersm.svg)](https://pypi.org/project/lockersm/)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)

The official Python SDK for reading and managing secrets in
[Locker Secrets Manager](https://locker.io).

The PyPI distribution is named `lockersm`; the Python import package is named
`locker`. Version 2 communicates with the Locker CLI through the stable
`locker.sdk` JSON-RPC protocol instead of parsing human-facing command output.
It supports both Locker Cloud and self-hosted Locker deployments.

## Install

```shell
python -m pip install --upgrade lockersm
```

You do not need to install the Locker CLI separately in the standard managed
mode. The SDK downloads a supported binary on first use and verifies the
release signature, SHA-256 digest, platform, architecture, and protocol
compatibility before execution.

### Support matrix

| Component | Supported versions |
| --- | --- |
| Python | 3.10, 3.11, 3.12, 3.13, 3.14 |
| Linux managed CLI | x86-64, ARM64 |
| macOS managed CLI | Intel, Apple silicon |
| Windows managed CLI | x86-64 |
| SDK protocol | `locker.sdk` v1 |

For an air-gapped or centrally managed deployment, provide an explicit Locker
CLI path with `LOCKER_CLI_PATH` or `binary_path`.

## Quick start

Create an access key in your Locker Secrets project, then expose the
credentials to the application environment.

Linux and macOS:

```shell
export LOCKER_ACCESS_KEY_ID="your-access-key-id"
export LOCKER_SECRET_ACCESS_KEY="your-secret-access-key"
```

Windows PowerShell:

```powershell
$env:LOCKER_ACCESS_KEY_ID = "your-access-key-id"
$env:LOCKER_SECRET_ACCESS_KEY = "your-secret-access-key"
```

Read a required secret:

```python
from locker import Locker

client = Locker.from_env()
database_password = client.get_required(
    "DATABASE_PASSWORD",
    environment_name="production",
)

# Pass database_password directly to the component that needs it.
# Never print or log secret values.
```

`get_required()` raises `ResourceNotFoundError` when the key does not exist.
Use `get()` only when a fallback is intentionally safe:

```python
log_level = client.get(
    "LOG_LEVEL",
    environment_name="production",
    default_value="info",
)
```

The default value is returned only for a genuine not-found response.
Authentication, permission, network, protocol, storage, and server failures
are still raised.

## Configure the client

`Locker.from_env()` is the recommended constructor. It recognizes:

| Environment variable | Purpose |
| --- | --- |
| `LOCKER_ACCESS_KEY_ID` | Project access key ID |
| `LOCKER_SECRET_ACCESS_KEY` | Project secret access key |
| `LOCKER_API_BASE` | Cloud or self-hosted API base URL |
| `LOCKER_CLI_PATH` | Absolute path to a deployment-managed CLI |
| `LOCKER_LOG` | `debug`, `info`, `warning`, or `error` |

The default cloud endpoint is
`https://api.locker.io/locker_secrets`.

Self-hosted deployment:

```python
from locker import Locker

client = Locker.from_env(
    api_base="https://secrets.example.com/locker_secrets",
)
```

Deployment-managed CLI:

```python
from locker import Locker

client = Locker.from_env(
    binary_path="/opt/locker/bin/locker",
)
```

The CLI path must be absolute and point to a regular, non-link file. Explicit
paths bypass managed updates and are never resolved through ambient `PATH`.

For migration from version 1, the SDK still recognizes `ACCESS_KEY_ID`,
`SECRET_ACCESS_KEY`, `LOCKER_ACCESS_KEY_SECRET`, and `ACCESS_KEY_SECRET`.
New deployments should use only the canonical `LOCKER_*` names.

### Timeouts, retries, and cancellation

Automatic retries are disabled by default. Applications that need transient
network resilience can opt in:

```python
client = Locker.from_env(
    timeout=5,
    max_network_retries=2,
)
```

Retries use bounded exponential backoff with jitter and occur only when the CLI
marks an error as retryable and the RPC is read-only. Secret and environment
create or update operations are never retried because protocol v1 does not
provide an idempotency key that can resolve an unknown commit outcome.

Signed CLI resolution/update, updater-lock waiting, capability negotiation,
read attempts, and retry backoff share the configured `timeout` as one total
protocol budget. The retry count can also be overridden for one read with
`max_network_retries=...`.

During capability negotiation, SDK 2.x opts into the CLI's `typed-v1` error
contract only when the installed CLI advertises it. This enables precise
conflict, validation, and integrity codes without breaking older CLI releases
or already deployed protocol-v1 clients.

A `threading.Event` can cooperatively cancel a request during CLI resolution,
capability negotiation, optional client/updater-lock waiting, retry backoff, or
an in-flight CLI process:

```python
import threading

cancel = threading.Event()
value = client.get_required("DATABASE_PASSWORD", cancel_event=cancel)
```

Cancellation prevents a subsequent operation from starting after the event is
set and terminates the complete in-flight CLI process tree. As with any
distributed write, cancellation after a create or update has reached the server
can leave its remote commit outcome unknown; the SDK never retries that
mutation automatically. The process also remains bounded by the remaining
request timeout. On Linux, the CLI receives a kernel parent-death guard before
execution, so a terminated Python host cannot leave an orphaned secret
operation running.

The SDK does not store plaintext secrets. `resttime` (default `120` seconds)
and `fetch` are delegated to the CLI's encrypted, revision-aware cache:

```python
client = Locker.from_env(resttime=30, fetch=False)
```

`resttime=0` disables offline reuse. `fetch=True` requires a successful server
refresh and never falls back to cached vault data. A transient outage may use
only a still-fresh cache last validated successfully by the server;
authentication, authorization, TLS, integrity, malformed-response, and local
storage failures always fail closed.

## Secrets

### Read secret metadata

`retrieve()` returns the complete secret resource object:

```python
secret = client.retrieve(
    "DATABASE_PASSWORD",
    environment_name="production",
)
print(secret.id, secret.key, secret.environment_name)
```

Secret objects contain plaintext values. Do not serialize, print, or include
them in logs.

### List secrets

```python
for secret in client.list(environment_name="production"):
    print(secret.id, secret.key, secret.environment_name)
```

For large projects, use bounded cursor pagination:

```python
cursor = None

while True:
    page = client.list_page(
        environment_name="production",
        page_size=100,
        cursor=cursor,
    )
    for secret in page.items:
        print(secret.id, secret.key)
    cursor = page.next_cursor
    if cursor is None:
        break
```

### Create and update secrets

Read secret input without echoing it in a terminal:

```python
from getpass import getpass

created = client.create(
    key="PAYMENT_API_KEY",
    value=getpass("New secret value: "),
    environment_name="staging",
)

updated = client.modify(
    key=created.key,
    value=getpass("Updated secret value: "),
    environment_name="staging",
)
```

Secret values are sent to `locker sdk` in the JSON request on standard input.
They are not placed in process arguments or logs.

### Export secrets

`export()` returns a plaintext `str` in `dotenv` or compact `json` format:

```python
dotenv_payload = client.export(
    environment_name="production",
    output_format="dotenv",
)
```

Treat the returned string as sensitive. Avoid logs, shell arguments, command
history, and unprotected files.

Secret deletion is not part of protocol v1. The canonical `Locker` client does
not expose a delete method; legacy resource-object delete helpers fail closed
with `InvalidRequestError`.

## Environments

```python
environments = client.list_environments()
for environment in environments:
    print(environment.name, environment.external_url)
```

Use `list_environments_page()` for cursor pagination.

The two lookup methods intentionally have different not-found contracts:

- `get_environment("production")` returns `None`.
- `retrieve_environment("production")` raises `ResourceNotFoundError`.

Create or update an environment:

```python
created = client.create_environment(
    name="staging",
    external_url="https://staging.example.com",
)

updated = client.modify_environment(
    name=created.name,
    external_url="https://new-staging.example.com",
)
```

Environment deletion is not part of protocol v1.

## Errors

Locker-defined transport, protocol, authentication, and API failures derive
from `locker.error.LockerError`. Standard Python argument errors can still be
raised for locally invalid values.

```python
import logging
import os

from locker.error import AlreadyExistsError, LockerError, RateLimitError

try:
    created = client.create(
        key="PAYMENT_API_KEY",
        value=os.environ["BOOTSTRAP_PAYMENT_API_KEY"],
    )
except AlreadyExistsError as exc:
    logging.info(
        "The Locker secret already exists (request_id=%s, kind=%s)",
        exc.request_id,
        exc.kind,
    )
except RateLimitError as exc:
    logging.warning(
        "Locker request was rate limited (request_id=%s, retryable=%s)",
        exc.request_id,
        exc.retryable,
    )
    raise
except LockerError as exc:
    logging.error(
        "Locker request failed (type=%s, code=%s, kind=%s, request_id=%s, server_request_id=%s)",
        type(exc).__name__,
        exc.code,
        exc.kind,
        exc.request_id,
        exc.server_request_id,
    )
    raise
```

| RPC code | Exception | Meaning |
| ---: | --- | --- |
| `-32001` | `AuthenticationError` | Credentials were rejected |
| `-32003` | `PermissionDeniedError` | Access is not permitted |
| `-32004` | `ResourceNotFoundError` | Resource does not exist |
| `-32009` | `ConflictError` | Operation conflicts with current resource state |
| `-32009` + `*_already_exists` kind | `AlreadyExistsError` | Secret or environment already exists |
| `-32022` | `ValidationError` | Operation data failed Locker validation |
| `-32029` | `RateLimitError` | Request was rate limited |
| `-32050` | `APIConnectionError` | Locker API could not be reached |
| `-32051` | `APIServerError` | Locker API failed the request |
| `-32060` | `LocalStorageError` | Local secure state failed |
| `-32070` | `IntegrityError` | Cryptographic integrity validation failed |
| `-32000` | `OperationError` | Legacy or unclassified operation failure |
| `-32000` + `cancelled` kind | `OperationCancelledError` | Operation was cancelled; remote mutation outcome may be unknown |
| `-32000` + `request_rejected` kind | `RequestRejectedError` | Legacy request rejection |
| `-32000` + `response_too_large` kind | `ResponseTooLargeError` | Result exceeds the protocol response limit |
| `-32700`, `-32600`..`-32603` | `ProtocolError` | Invalid JSON-RPC exchange |

`AlreadyExistsError` derives from `ConflictError`; all service-operation
exceptions derive from `APIError`, and every Locker-defined exception derives
from `LockerError`. This allows applications to catch either a precise
condition or a stable broader category. Older CLI releases that report
`-32000` with `conflict` or `duplicate_hash` are refined to conflict types for
compatibility. The ambiguous legacy `request_rejected` kind maps to
`RequestRejectedError`, not to a conflict or validation error.

Missing or malformed credential syntax is rejected locally before the CLI is
resolved. A missing half of the pair uses kind `missing_credentials`; the
access key ID must be a UUIDv4 (`invalid_access_key_id`), and the secret access
key must be non-empty canonical Base64 (`malformed_secret_access_key`). These
conditions raise `AuthenticationError` with code `-32001`. A well-formed pair
rejected by Locker raises the same broad exception with kind
`invalid_secret_access_key` and explains that the secret access key does not
match the access key ID. A backend HTTP 401 uses kind `unauthorized` and the
canonical `authentication failed` message.

`request_id` identifies the local SDK-to-CLI JSON-RPC exchange.
`server_request_id` is a separate optional, validated backend correlation ID
for support and activity-log lookup; it never replaces `request_id`.

Each exception exposes a canonical safe message through `user_message` and
retains `code`, `kind`, `retryable`, and `request_id`. The SDK derives that
message from the numeric code and a recognized kind; it never prints arbitrary
wire text returned by a custom or incompatible CLI. Numeric codes select the
primary exception category, while a documented kind only refines it, such as
distinguishing an already-existing resource from another conflict. Integrity
failures, conflicts, already-existing resources, and validation failures are
always non-retryable and fail closed, even if an older CLI reports inconsistent
retry metadata. Avoid logging exception bodies or application data around a
secret operation.

`RateLimitError.retry_after_seconds` exposes the server's bounded
`Retry-After` delay when available (`0..86400`); it is otherwise `None`.
Cancellation raises `OperationCancelledError` and remains non-retryable because
it does not prove that a remote mutation failed to commit.

## CLI management and verification

Importing `locker` and constructing `Locker` are side-effect free. The SDK
does not create a cache directory, access the network, or start a process until
the first operation or an explicit `install_cli()` call.

Managed mode:

1. Checks the
   [signed release channel](https://locker.io/secrets/download) on first use
   and at most once every six hours after a successful check.
2. Verifies the embedded Locker Ed25519 trust root, signed latest document,
   signed manifest, artifact size, SHA-256, detached signature, executable
   header, and protocol range.
3. Cryptographically re-verifies the signed manifest plus the selected
   artifact's size, executable header, and streamed SHA-256 immediately before
   every CLI process execution; file identity metadata is never treated as a
   substitute for the signed hash.
4. Installs releases into immutable
   `~/.locker/sdk-cli/python/bin/releases/<version>/` directories.
5. Switches the current pointer only after complete verification.
6. Rejects rollback attempts and same-version mutation.

Force an immediate signed update check:

```python
installed_path = client.install_cli()
```

If the release service is temporarily unreachable, a previously accepted
binary may be reused only after its cached metadata and artifact are verified
again. Invalid signatures, rollback state, incompatible platforms, and
protocol errors always fail closed.

## Logging

Set `LOCKER_LOG` or the constructor's `log` argument:

```shell
export LOCKER_LOG="info"
```

```python
client = Locker.from_env(log="info")
```

SDK logs contain operational metadata such as method, request ID, duration,
and process status. The SDK never emits credentials, request or response
bodies, custom headers, or secret values.

## Migrating to 2.x

Version 2:

- Requires Python 3.10 or newer.
- Uses the stable `locker.sdk` JSON-RPC protocol v1.
- Downloads only signed Locker CLI releases in managed mode.
- Uses canonical `LOCKER_ACCESS_KEY_ID` and
  `LOCKER_SECRET_ACCESS_KEY` variables.
- Adds `Locker.from_env()`, `get_required()`, typed pagination, and explicit
  CLI lifecycle methods.
- Returns a default value only for `ResourceNotFoundError`.

Applications must use this SDK or the `locker sdk` protocol. Do not parse
human-facing CLI output.

## Versioning

The package follows Semantic Versioning and publishes canonical PEP 440
versions:

- PyPI package: `MAJOR.MINOR.PATCH`
- Source tag: `vMAJOR.MINOR.PATCH`

## Troubleshooting

- `AuthenticationError`: verify that the access key ID is UUIDv4, the secret
  access key is canonical Base64, and both values belong to the same active
  Locker project.
- `PermissionDeniedError`: verify the authenticated key's
  project/environment scope.
- `APIConnectionError`: check the API base, private CA/proxy, and server
  reachability. Opt into bounded read retries only when appropriate.
- Managed CLI installation errors: check system time, access to the
  [Locker Secrets download service](https://locker.io/secrets/download), and
  private ownership below
  `~/.locker/sdk-cli/python`.
- `ProtocolError`: upgrade the SDK and CLI together or remove an incompatible
  explicit `LOCKER_CLI_PATH`.
- Unexpected stale reads: use `fetch=True`; do not loosen managed-cache
  permissions as a workaround.

## Security

Report security vulnerabilities through the
[Locker Bug Bounty program](https://whitehub.net/programs/locker). Do not
disclose vulnerabilities in a public issue.

General product documentation and support are available at
[support.locker.io](https://support.locker.io).

## License

Copyright CyStack Corporation.

Licensed under the
[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
Locker is developed and maintained by CyStack.
