This release is a pre-release and may not be stable for production use.
redact-secret-vault (Python)
Swap secrets for random tokens before text leaves your server (for example, to an LLM), then put the original values back, but only for the user, tenant, purpose, and field your policy allows. It is the Python counterpart of @redact-secret/vault-server.
Research-grade. The base install is the in-memory server; nothing in it is persistent. Persistent modules ship in the same wheel behind extras (below) and are not supported. Detection runs in @redact-secret/core, which has no Python build, so this package talks to it through a small Node.js child process.
Python persistence is not supported. The wheel also ships persistent modules (below), behind the crypto, postgres, and aws-kms extras, since 0.1.0b4. They are verified only for the cells of the qualification record states that its gates are not all passed: the Node.js bridge (G5: the bridge is not named qualified for any cell) and the support matrix (G9) are not passed in full. Nothing on this page claims support for them.
Requirements
- Python 3.10 to 3.13 for the in-memory server (the classifiers list these; CI runs 3.10, 3.12, and 3.13). The persistent modules need Python 3.11+ and refuse to import on 3.10 (CI runs them on 3.11 and 3.12)
- Node.js 20, 22, or 24 on
PATH @redact-secret/coreat exactly0.1.0-beta.13, installed with npm in a directory your application owns
Install
pip install redact-secret-vault==0.1.0b5
# In a directory of your choice, for example /srv/myapp/core:
npm install @redact-secret/core@0.1.0-beta.13
Tell the bridge where that node_modules is, in code or through the environment:
bridge = NodeCoreBridge(node_modules="/srv/myapp/core/node_modules")
export REDACT_SECRET_VAULT_NODE_MODULES=/srv/myapp/core/node_modules
Then check the setup. doctor is new in 0.1.0b4; 0.1.0b3 does not have it:
python -m redact_secret_vault doctor --node-modules /srv/myapp/core/node_modules
ok node: v22.16.0
ok core location: /srv/myapp/core/node_modules (from --node-modules)
ok core: @redact-secret/core 0.1.0-beta.13 loaded (addon)
ok scan: 1 finding(s) in the synthetic input
A failing check prints FAIL, the reason, and a fix: line, and the command exits 1.
Use
import asyncio
from redact_secret_vault import (
CaptureGrant,
CaptureOptions,
InMemoryVaultServer,
NodeCoreBridge,
PolicyDecision,
Principal,
RestoreRequest,
)
def resolve_principal(context):
# The consuming application's own authentication — never a mandated
# identity provider. Must raise, not return a partial Principal, when
# trust cannot be established.
return Principal(id=context["user_id"], tenant=context["tenant"])
def same_tenant_only(decision_input):
if decision_input.tenant == decision_input.source.issued_tenant:
return PolicyDecision(allow=True)
from redact_secret_vault import ServerDenialReason
return PolicyDecision(allow=False, reason=ServerDenialReason.TENANT_MISMATCH)
async def main() -> None:
server = InMemoryVaultServer(
core_client=NodeCoreBridge(),
principal_resolver=resolve_principal,
release_policy=same_tenant_only,
)
captured = server.capture(
"deploy with ghp_EXAMPLE_SYNTHETIC_TOKEN_0000000000 now",
CaptureOptions(
issued_tenant="tenant-acme-synthetic",
release=(CaptureGrant(sink="reply", paths=("body",)),),
),
)
result = await server.restore(
RestoreRequest(
sink="reply",
captures=(captured.capture_id,),
fields={"body": f"Use {captured.tokens[0].token} please"},
purpose="support-reply-purpose-synthetic",
tenant="tenant-acme-synthetic",
context={"user_id": "user-synthetic-1", "tenant": "tenant-acme-synthetic"},
)
)
print(result.fields["body"])
asyncio.run(main())
A complete version that also shows a denied restore: examples/05-python-server.py.
The rules
- You supply two functions.
principal_resolverturns your already-authenticated request context into aPrincipal; raise when it cannot.release_policydecides each restore. A failure in either one denies. capturegrants,restorechecks. A value returns only into thesinkandpathsthe capture granted, for the capture'sissued_tenant, with a non-emptypurpose.- A restore is all or nothing. One failing token denies the whole request and returns no values.
- Close the bridge. Use
NodeCoreBridgeas a context manager, or callclose(). Threads sharing one bridge are served one at a time; use one bridge per worker for parallel scans. - PII is off by default and never retained unless a capture names the exact type.
Persistent modules (shipped in 0.1.0b4 behind extras, not supported)
pip install redact-secret-vault==0.1.0b5 puts these modules in the environment; 0.1.0b3 does not contain them. Each is behind an extra that brings its dependency (for example pip install "redact-secret-vault[postgres]==0.1.0b4"), and the base install keeps no runtime dependency. They need Python 3.11 or later. The API is async only (Store, KeyProvider, and RecordCrypto are protocols with async def methods); there is no synchronous twin. Status words follow CONVENTIONS.md: implemented here means the code exists and passed the runs named in the record, not supported means no support claim is made.
| Import path | Extra | What it is | Status |
|---|---|---|---|
redact_secret_vault.persistent |
none | Contracts, errors, validators, canonical encoding, digests, the volatile reference store_memory, and create_persistent_server_vault (the persistent server profile) |
Implemented; not supported. The persistent profile, the vectors, and the schedule corpus passed on the cells of the record |
redact_secret_vault.crypto |
crypto |
Record crypto and a local key provider over cryptography. Key material is bytes in process memory, so the profile is local-bytes-hkdf-aes-256-gcm-v1, not the JavaScript profile |
Implemented; not supported. Vectors and interoperation with the JavaScript crypto passed on the cells of the record |
redact_secret_vault.stores.postgres |
postgres |
A PostgreSQL store over psycopg 3, against the schema @redact-secret/store-postgres owns (Python creates no table) |
Implemented; not supported. Run against PostgreSQL 17.11, a single primary, with psycopg 3.3.6 (binary build) only |
redact_secret_vault.keys.aws_kms |
aws-kms |
An AWS KMS key provider over an injected boto3 client |
Implemented; not supported. One real-service run in us-east-1 with two symmetric keys; throttling not provoked |
- Install variant. The
postgresextra names plainpsycopg, which cannot be imported at all without a systemlibpq. Installlibpqor addpsycopg[binary](the variant tested). The adapter takes a pool the application owns (psycopg_pool.AsyncConnectionPoolworks; it is not a dependency) and never opens a connection from a URL. - WSGI and other synchronous hosts. Call the async API through one long-lived event-loop thread per process. Do not use
asyncio.runper request: a connection pool is bound to its loop. - Fork. A store created before
os.fork()raisesSTORE_CLOSEDin the child. Construct it after the fork. - No default key. The key material, the digest key, the pool, and the KMS client are all supplied by the application. A bytes key in Python memory cannot be cleared; the package overwrites the buffers it owns and says so, and does not claim more.
- At rest is not everywhere. Encryption at rest covers what the store holds. The whole capture input, every secret in it and not only the retained values, still goes to the Node.js bridge and stays in its heap until it is collected or the process exits. The bridge is research-grade and not qualified, and the Python qualification does not include it: any deployment claim is for an application-supplied, separately qualified
CoreClient(decision). Usemax_scans_per_process=1, one bridge per tenant or trust domain, or your ownCoreClientif that residual risk is not acceptable. - Bridge limits, measured (the record, section 4.10). The child's memory still held a copy of an input's secret after the scan, and one copy stayed through thousands of later scans, until the process exited; only
max_scans_per_process=1leaves no live process holding it. One bridge serves a few hundred to a few thousand small scans per second (the measured range depends on the host's load) however many threads wait. The first scan after a start pays for the Node.js start-up, the core load, and hashing the core (the integrity pin).timeout_sis end to end: a caller that waits for the bridge longer thantimeout_sgetsBRIDGE_TIMEOUTand nothing is sent, so a queue longer thantimeout_sfails closed. The bridge refuses a core that is not the pinned release byte for byte (CORE_INTEGRITY_MISMATCH), and refuses an error code or field from the child that does not have its fixed shape (BRIDGE_BAD_OUTPUT); after repeated start failures it backs off (0.1 s to 5 s) and fails at once with the same code. The persistent server's scans run on two threads the bridge owns, not on the loop's default executor. Inputs near the 64 MiB ceiling can exceed the defaulttimeout_sof 10 s. - Not tested, so not stated: Windows, macOS in CI, free-threaded or PyPy builds, Python 3.14 and 3.10 for the persistent modules, a synchronous standby or failover, a connection pooler, managed PostgreSQL, two hosts, TLS, and power loss. Linux x86-64 ran only as a labeled subset under emulation, and in the GitHub-hosted jobs the record cites. The details are in the qualification record, which is the only document that may state support for a cell.
- Logging. With a real key,
botocoreatDEBUGwas observed writing the plaintext data key and the key ARN (botocore.parsers, the response body) and the wrapped key (botocore.endpoint, the request parameters). The KMS provider therefore fails closed without touching your logging configuration: at construction and before every KMS call it checks whetherDEBUGis enabled (Logger.isEnabledFor, so inherited levels, the root logger, andlogging.disablecount) forbotocore,botocore.parsers,botocore.hooks,botocore.endpoint,boto3, andurllib3.connectionpool. If it is, construction raisesKEY_INVALID_ARGUMENTand a call raisesKEY_UNAVAILABLEbefore any request is made. Passallow_sdk_debug_logging=Trueonly if you accept that the SDK writes key material to your logs; the provider then works as before. A cache hit makes no SDK call and is not refused. Not covered: a level raised while a call is in flight, a call abandoned by its timeout that is still running, a logger the SDK adds later, and a client you configured to log by another route.psycopgatDEBUGwrites the host, port, user, and database of each connection, never a statement or a value.
Common problems
Run python -m redact_secret_vault doctor first: it names the failing part and the fix.
| Error | Cause |
|---|---|
CORE_FAILURE / BRIDGE_CORE_NOT_FOUND |
The bridge cannot find the core. Pass node_modules= or set REDACT_SECRET_VAULT_NODE_MODULES |
CORE_VERSION_MISMATCH |
The installed core is not the pinned version |
CORE_FAILURE / BRIDGE_TIMEOUT |
A scan ran past timeout_s (default 10 s) |
UNREDACTED_FINDINGS |
The input has findings the core left visible. Pass a policy that redacts them, or unredacted="pass-through" |
More
- Troubleshooting: every error code with its fix.
- Reference: how the core is located, the bridge process and its limits, PII, tests, and how this package compares with the JavaScript one.
- Threat model for the bridge.
- Release status and RELEASING.md.
Development
From this repository (npm ci at the root installs the core):
cd packages/vault-py
pip install -e ".[test]"
pytest
The persistent tests need the extras (pip install -e ".[test,lint,crypto,postgres,aws-kms]" and psycopg[binary]) and Python 3.11+; those that need a database or AWS skip with their reason when it is not configured (RSV_PG_APP_URL and RSV_PG_ADMIN_URL for PostgreSQL, applied with node packages/vault-py/tests/pg_prepare.mjs; RSV_KMS_TEST_KEY_ARN and RSV_KMS_TEST_OLD_KEY_ARN for KMS). RSV_REQUIRE_POSTGRES=1 makes a missing database a failure.
Qualification tools, none of them in the wheel: python tests/bridge_qualification.py --help runs the bridge qualification harness (adversarial and fuzzed frames, lifetime bounds, plaintext left in the child, timeouts, concurrency, limits); tests/test_persistent_server_postgres_leaks.py is the server-level leak test over PostgreSQL (set RSV_PG_CONTAINER to a Docker container name to search the server's own log too); scripts/qualify-python-matrix.sh runs one Linux cell of the matrix in Docker.
Metadata
Release files for redact-secret-vault 0.1.0b5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| redact_secret_vault-0.1.0b5.tar.gz | 326.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| redact_secret_vault-0.1.0b5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 469.0 kB
Release files / redact_secret_vault-0.1.0b5.tar.gz
| Download URL | redact_secret_vault-0.1.0b5.tar.gz |
|---|---|
| Size | 326.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c61d382a89c7f66968ea7961911cc5607efc945cadf11c67d6ec4d5b99414162
|
|
BLAKE2b-256 checksum How to use checksums |
8190b033774a4eda9d18f3d4997a807b45169ed07de43bdd370ce612ad68c662
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / redact_secret_vault-0.1.0b5-py3-none-any.whl
| Download URL | redact_secret_vault-0.1.0b5-py3-none-any.whl |
|---|---|
| Size | 142.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7e3b5e89e0e575d96e92691fb4d976c61ca746ef2558af0d55c7ac3047a2154a
|
|
BLAKE2b-256 checksum How to use checksums |
0e08c9e71342f7e97abfb815256873c84da0a6851782ff523b0b6dcbeb8c2b3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log