RFC 103: std.secrets — Secret strings, secret bytes, and redaction-safe values¶
- Status: Draft
- Created: 2026-05-24
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 017 (validated newtypes with implicit coercion)
- RFC 033 (
ctxtyped configuration context) - RFC 066 (
std.httpHTTP client surface) - RFC 072 (
std.loggingstructured logging) - RFC 078 (tool execution and typed workflow actions)
- RFC 089 (
std.environruntime environment access) - RFC 090 (typed CLI framework)
- RFC 093 (
std.telemetryobservability) - RFC 102 (semantic layer inspection surface)
- Issue: https://github.com/dannys-code-corner/incan/issues/661
- RFC PR: -
- Written against: v0.3
- Shipped in: —
Summary¶
This RFC proposes std.secrets as Incan's standard library home for secret value wrappers, beginning with SecretStr and SecretBytes. Secret values are ordinary typed values that can flow through config, CLI, environment, HTTP, logging, telemetry, workflow actions, and generated reports without revealing their plaintext through unauthorized display, debug, structured logs, diagnostics, default serialization, or inspection surfaces. The goal is not to pretend secrets become impossible to copy or exfiltrate inside a compromised process; the goal is to make plaintext exposure deny-by-default, keep raw access scoped and intentional, and allow stronger protected storage such as encrypted idle memory where the backend can provide it.
Core model¶
- Secrets are values, not logging conventions: secrecy must travel with the value's type so redaction is not rebuilt separately by every caller.
- Plaintext exposure is deny-by-default: Incan-owned display, debug output, logs, telemetry attributes, diagnostics, semantic inspection, reports, and default serialization must not reveal secret contents.
- Reveal is scoped and intentional: APIs that need raw bytes or strings should consume
SecretStrorSecretBytesdirectly, or require an intentionally named scoped reveal operation that tooling can recognize. - Protected idle storage is preferred: implementations should keep secret contents encrypted or otherwise protected while idle when a backend can do so meaningfully, and decrypt only inside a scoped reveal operation.
- Memory guarantees are honest: protected idle storage and zeroization reduce exposure, but the public contract must not promise that every intermediate copy made by encoders, transport backends, operating systems, foreign APIs, crash handlers, or the process itself is erased.
- Specific types come first:
SecretStrandSecretBytesare the initial stable surface. A genericSecret[T]may come later if it does not weaken the concrete-string and concrete-bytes contracts. - Tooling preserves sensitivity metadata: CLI, LSP, semantic inspection, workflow action output, generated docs, and reports should know that a value exists and what type it has without seeing the raw payload.
Motivation¶
Python ecosystems often represent secrets with wrapper classes, Pydantic field flags, logging filters, and framework-specific conventions. Those mechanisms help, but they remain easy to bypass because Python string interpolation, repr, dictionaries, serializers, exception traces, and third-party clients can all treat the wrapped value as just another object unless every boundary cooperates perfectly.
Incan has a better opportunity because its stdlib, typechecker, generated Rust, structured logging, HTTP surface, CLI framework, environment access, action metadata, and semantic inspection model can agree on one value-level contract. A SecretStr used as a CLI option, loaded from an environment variable, passed to an HTTP authorization helper, logged as a structured field, or surfaced in an action report should remain recognizably present but redacted all the way through those boundaries. The core promise should be stronger than "nice repr": plaintext must not leave a secret wrapper through an Incan-owned surface unless the code has made an explicit reveal decision or passed the value to a trusted API that owns a scoped reveal internally.
This RFC also closes a design gap left deliberately open by RFC 017. Validated newtypes can model domain-specific string and byte constraints, but secret handling is more than a validation constraint: it changes display, debug, logging, diagnostic serialization, wire-boundary APIs, equality, cloning, and drop behavior expectations.
Goals¶
- Add a
std.secretsmodule withSecretStrandSecretBytes. - Make redaction a property of the value type rather than a per-logger or per-HTTP-client convention.
- Prevent plaintext secret emission through Incan-owned display, debug, diagnostic, logging, telemetry, semantic inspection, generated-report, and default serialization paths.
- Require safe default behavior for display, debug, structured logs, telemetry, diagnostics, semantic inspection, and generated reports.
- Provide intentionally named, tooling-visible APIs for scoped exposure of raw secret material at trusted boundaries.
- Prefer encrypted or otherwise protected idle memory for secret storage where the target backend can provide it meaningfully.
- Let stdlib consumers such as
std.http,std.environ, typed CLI surfaces,ctx, workflow actions, logging, and telemetry accept or preserve secret values without converting them to plainstrorbytes. - Define a conservative serialization contract that prevents accidental JSON, TOML, YAML, CLI, or report emission of raw secret contents.
- Define honest memory-handling expectations, including scoped plaintext lifetimes and best-effort zeroization for plaintext buffers where the backend can support it.
- Leave room for future secret providers, vault integrations, redaction policies, and generic secret wrappers without blocking the concrete
SecretStrandSecretBytessurface.
Non-Goals¶
- This RFC does not define a password manager, vault, keyring, or secrets backend.
- This RFC does not define encryption at rest for source files, manifests, lockfiles, logs, reports, or generated artifacts.
- This RFC does not provide full information-flow control, taint tracking, or a data-loss-prevention system.
- This RFC does not guarantee that all process memory, operating-system buffers, network buffers, allocator copies, panic payloads, crash dumps, foreign library copies, or compiler temporaries are erased.
- This RFC does not claim that encrypted idle storage protects against arbitrary code execution inside the same process; any implementation must still hold or derive decryption material somewhere.
- This RFC does not make secrets safe to expose to untrusted code.
- This RFC does not define random secret generation; a future
std.randomor expandedstd.secretssurface may do that separately. - This RFC does not define identity protocols such as SAML, OAuth, OIDC, JWT validation, service-account exchange, or single sign-on workflows.
- This RFC does not standardize every sensitive-data class such as PII, payment data, access tokens, API keys, passwords, and private keys as distinct semantic categories in the initial surface.
- This RFC does not replace access control, capability checks, sandboxing, policy approval, or runtime permission boundaries.
Guide-level explanation¶
Users should be able to load a secret value and pass it through normal code without turning it into a plain string just to keep working.
from std.environ import env
from std.secrets import SecretStr
token: SecretStr = env.secret_str("SERVICE_TOKEN")?
println(token)
The printed value is redacted. The exact placeholder is a design detail, but it must not include the token.
HTTP clients and other stdlib APIs should accept secret values directly:
from std.environ import env
from std.http import Client, bearer
from std.secrets import SecretStr
token: SecretStr = env.secret_str("SERVICE_TOKEN")?
client = Client(default_headers={"Authorization": bearer(token)})
response = client.get("https://api.example.com/items")?
The caller does not reveal the token manually. The HTTP boundary may perform a scoped internal reveal when constructing the wire request, but diagnostics, debug output, retries, telemetry, and action reports must preserve sensitivity.
When a raw value is genuinely needed, the operation should read as intentional and scoped:
from std.secrets import SecretBytes
def sign_with_key(raw_key: bytes) -> Signature:
return hmac.sign(raw_key, payload)
key: SecretBytes = SecretBytes.from_hex(env.secret_str("SIGNING_KEY_HEX")?)?
signature = key.with_exposed_bytes(sign_with_key)
The exact reveal method names remain open in this Draft. The important property is that code review, search, LSP, and policy tooling can recognize raw-secret exposure sites, and that the preferred shape does not hand an ordinary string or byte buffer back to the caller for uncontrolled storage.
Secret values should also compose with typed configuration and CLIs:
from std.secrets import SecretStr
ctx Deploy:
api_token: SecretStr = env("API_TOKEN")
endpoint: str = "https://api.example.com"
An inspection view can show that api_token exists, is required, and has type SecretStr, without showing the token itself.
Reference-level explanation¶
Module surface¶
std.secrets must expose SecretStr and SecretBytes.
SecretStr must represent owned UTF-8 secret text. SecretBytes must represent owned binary secret material.
The module may expose helper types such as redaction placeholders, reveal guards, redacted serialization adapters, or sensitivity metadata, but SecretStr and SecretBytes are the required initial surface.
Construction¶
SecretStr must be constructible from a str through an explicit constructor or conversion path. SecretBytes must be constructible from bytes through an explicit constructor or conversion path.
Construction APIs should make plain-to-secret conversion visible in source. Implicit conversion from str to SecretStr or from bytes to SecretBytes should be avoided unless a surrounding API already declares that an input position is secret, such as a typed CLI option, an environment accessor, or a ctx field.
SecretStr should support conversion to SecretBytes using an explicit encoding operation. SecretBytes should support UTF-8 decoding into SecretStr through a fallible operation.
std.environ should provide secret-returning helpers, such as a secret_str shape, so callers do not need to load an environment variable as plain text and then wrap it manually.
Display and debug behavior¶
SecretStr and SecretBytes must redact their contents in display, debug, assertion failure, panic, diagnostic, and structured-inspection contexts owned by the Incan standard library and toolchain.
The redacted representation must communicate that the value is secret and present. It must not include the secret contents, prefix, suffix, length, checksum, entropy estimate, or other derived value unless a later RFC defines an explicit policy for such metadata.
String interpolation and formatting protocols must use the redacted representation by default. Formatting a secret must not implicitly call the reveal operation.
Plaintext leakage boundary¶
The normative security boundary for this RFC is Incan-owned plaintext emission. SecretStr and SecretBytes must not reveal raw contents through Incan-owned display, debug, panic formatting, assertion messages, diagnostics, structured logs, telemetry attributes, semantic inspection, generated reports, CLI help, CLI echo, default serialization, or action metadata.
This boundary also applies to nested structures. A model, list, dict, result, error, request, response, action input, or telemetry event containing a secret value must preserve redaction when formatted or serialized through Incan-owned mechanisms.
Trusted stdlib APIs may reveal plaintext internally only for the duration of the operation that requires it, such as computing an HMAC or sending an HTTP authorization header. That internal reveal must not become observable through error values, debug payloads, telemetry attributes, retry reports, or generated artifacts.
Reveal operations¶
SecretStr must provide an intentionally named operation for exposing the raw str value. SecretBytes must provide an intentionally named operation for exposing the raw bytes value.
Reveal operations must be easy for tooling to identify. They should use names that communicate risk, such as expose_secret, expose_secret_str, or expose_secret_bytes, rather than neutral names like value, get, or as_str.
The preferred reveal shape is scoped: a callback, guard, or equivalent API that makes plaintext available only for a bounded lexical or dynamic lifetime. Owned plaintext copies should either be unavailable by default or exposed through a more explicit and noisier escape hatch than scoped reveal.
The reveal operation may return a borrowed view, a scoped guard, a backend-specific safe-access wrapper, or an owned copy only when the API name makes the copying behavior explicit. The accepted design must document the lifetime, copying behavior, and zeroization behavior of every reveal path.
APIs that genuinely need raw material should prefer accepting SecretStr or SecretBytes directly instead of forcing user code to reveal the secret first.
Serialization¶
Default data serialization of SecretStr and SecretBytes must not emit raw secret contents.
For diagnostic serialization, generated reports, semantic inspection, logs, telemetry, and CLI output, the value must serialize as a redacted secret marker or an equivalent structured redaction object.
For data formats that are intended to leave the process as user data, such as JSON request bodies, TOML files, YAML files, or generated artifacts, default serialization should fail unless the caller chooses an explicit redacted adapter or an explicit reveal operation. This avoids accidentally sending placeholder text where a real secret was expected, and avoids accidentally persisting the raw value.
Equality, ordering, and hashing¶
SecretStr and SecretBytes should not expose ordering operations by default.
Equality is an open design question. If equality is exposed, it should avoid timing behavior that is obviously inappropriate for token, password, or key comparison, and the docs must state whether the comparison is constant-time. If the implementation cannot provide a meaningful constant-time guarantee for a given storage representation, it should prefer an explicit comparison helper over ordinary equality.
Hashing secret values should be avoided by default because hash maps and debug tooling often make key material harder to reason about. If hash support is needed later, it should be introduced deliberately with documented semantics.
Cloning and copying¶
SecretStr and SecretBytes must not be trivially copyable value types.
Cloning may be supported when the language's ownership model requires it for ordinary value flow, but clone operations must preserve secrecy metadata and must not reveal raw contents. The docs must state that cloning creates another copy of the secret material.
Protected storage and memory handling¶
Implementations should keep secret contents encrypted or otherwise protected while idle when the target backend can provide a meaningful protected-storage implementation. Plaintext should be produced only inside scoped reveal operations or trusted stdlib internals that need raw bytes or text for a bounded operation.
Any protected-storage implementation must document its threat model. Encrypting a buffer while idle can reduce accidental plaintext retention and may help with some memory disclosure scenarios, but it does not protect against arbitrary code execution in the same process, a compromised runtime, a debugger with full process access, or backend APIs that must receive plaintext.
Plaintext buffers created during reveal should be zeroized as soon as their scoped use ends when the backend can support that. SecretBytes should zeroize owned plaintext memory on drop when generated code can do so without weakening correctness. SecretStr may also zeroize owned storage when implemented over a mutable owned buffer, but the public contract must not imply that all UTF-8 string copies are erased.
Both types must document that redaction is an exposure-control guarantee for standard display, debug, logging, telemetry, diagnostics, and serialization paths. Protected idle storage and zeroization strengthen that guarantee, but they are not full memory-forensics or same-process-compromise guarantees.
The implementation should avoid unnecessary copies in stdlib APIs that consume or forward secret values, especially HTTP authorization helpers, cryptographic helpers, and secret-provider integrations.
Logging, telemetry, diagnostics, and inspection¶
std.logging, std.telemetry, diagnostics, and semantic inspection must treat SecretStr and SecretBytes as sensitive fields by type.
Structured outputs should preserve the fact that a field exists, its declared type, and relevant non-sensitive metadata such as source kind when appropriate. They must not include the raw value.
Tooling should mark explicit reveal operations as searchable and inspectable sites. LSP hover, semantic inspection, and policy checks may use those sites to explain where secret material leaves the protected wrapper.
HTTP and wire-boundary APIs¶
std.http authorization helpers, header builders, request diagnostics, retry reporting, and telemetry should preserve secret sensitivity. Header values constructed from SecretStr or SecretBytes must be redacted in debug-facing output even if the header name is not in a built-in sensitive-header list.
std.http may expose raw secret material internally when sending a request. That internal exposure must not change the public Request, Response, HttpError, log, telemetry, or action-output redaction contract.
Typed actions, CLIs, and configuration¶
Typed action inputs, CLI options, and ctx fields should be able to declare SecretStr and SecretBytes directly.
Machine-readable action metadata should distinguish a required secret input from a plain string input. Action output must not include raw secret values unless a future policy system defines an explicit, user-approved reveal path.
CLI help may show that an option expects a secret. It must not echo secret defaults or environment-derived values.
Higher-level identity protocols¶
Identity and federation protocols such as SAML, OAuth, OIDC, JWT validation, service-account exchange, and single sign-on workflows should be built above std.secrets, not inside it. Those protocols have their own security models: XML or JSON token formats, signatures, certificates, issuer and audience validation, replay windows, metadata discovery, clock skew, session state, and provider-specific policy.
std.secrets should provide the primitive secret value contract those packages consume. A future identity or platform library may store private keys, bearer tokens, client secrets, SAML assertions, or signed credentials in SecretStr or SecretBytes, and may use scoped reveal internally when validating or transmitting them. That does not make std.secrets responsible for the protocol semantics.
Design details¶
Syntax¶
This RFC does not introduce new parser syntax. SecretStr and SecretBytes are stdlib types.
Semantics¶
Secret values have ordinary type identity and can be passed, returned, stored in models, and used in containers according to the language's normal value rules. Their special behavior is attached to display, debug, formatting, serialization, logging, telemetry, diagnostics, inspection, equality, hashing, cloning, reveal, protected storage, and drop semantics.
Implicit downcast from SecretStr to str and from SecretBytes to bytes must not be allowed. Raw exposure must require either an explicit scoped reveal operation or a trusted stdlib API that accepts a secret type directly and owns the scoped reveal internally.
Interaction with existing features¶
- RFC 017 (validated newtypes): secret values may use newtype-like machinery internally, but their display, debug, serialization, and memory expectations are a separate contract.
- RFC 033 (
ctx): typed configuration can declare secret fields and source them from environment or future secret providers without exposing raw values in inspection. - RFC 066 (
std.http): HTTP auth helpers and headers should accept secret values and preserve redaction through request diagnostics, retries, telemetry, and workflow output. - RFC 072 (
std.logging): structured logging should redact secret-typed fields by default. - RFC 078 (typed workflow actions): action inputs and outputs should preserve sensitivity metadata so reports can describe secret use without exposing values.
- RFC 089 (
std.environ): environment access should provide secret-returning helpers that avoid plain-string staging. - RFC 090 (typed CLI framework): CLI options can use
SecretStrandSecretBytesas declared types. - RFC 093 (
std.telemetry): telemetry attributes and events must redact secret-typed values. - RFC 102 (semantic layer inspection surface): semantic inspection should represent secret facts as redacted facts with stable type and source metadata.
Compatibility / migration¶
This feature is additive. Existing code that stores tokens in plain strings remains valid, but docs and examples should prefer SecretStr and SecretBytes at configuration, CLI, environment, HTTP, and action boundaries once the types exist.
Migration helpers may wrap existing str or bytes values explicitly. Such helpers should not hide the fact that code still created a plain value before wrapping it.
Alternatives considered¶
- Plain
newtype strandnewtype bytesonly - Rejected because newtypes alone do not define formatting, debug, serialization, logging, telemetry, equality, cloning, and memory behavior.
- Logging-only redaction
- Rejected because secrets leak through more than logs: debug strings, exception messages, assertions, generated reports, telemetry, HTTP diagnostics, CLI echo, and semantic inspection all matter.
- HTTP-only secret headers
- Rejected because the same token often starts in environment or CLI config, flows through
ctx, enters an HTTP client, appears in telemetry, and may be referenced by typed actions. - One generic
Secret[T]as the first surface - Rejected for the initial version because strings and bytes have distinct encoding, display, comparison, and memory concerns. A generic wrapper may still be useful later.
- Always serialize redacted placeholders
- Rejected for data serialization because silently writing
<redacted>into JSON payloads, config files, or generated artifacts can create corrupt data and hide bugs. - Unscoped raw getters
- Rejected because a method that returns an ordinary
strorbytesas the primary reveal path makes it too easy to store, log, serialize, or return plaintext accidentally. - Always require manual reveal before wire use
- Rejected because it pushes raw exposure into user code and makes the safe path noisier than the risky path.
Drawbacks¶
- Secret wrappers add friction when code genuinely needs raw strings or bytes.
- Redaction can create a false sense of security if users interpret it as encryption, access control, or memory-forensics protection.
- Encrypted idle storage has key-management and performance costs, and it cannot protect against every same-process threat.
- Equality, hashing, and serialization need conservative choices that may surprise users expecting string-like behavior.
- Stdlib modules and tooling must consistently honor the secret contract or the abstraction becomes unreliable.
- The exact reveal API needs careful design because it becomes the standard searchable marker for sensitive exposure.
Implementation architecture¶
(Non-normative.) The Rust-backed implementation should use owned storage with redacting display and debug implementations. Where practical, secret payloads should be stored encrypted while idle with process-local key material and decrypted only inside scoped reveal guards. Plaintext buffers created by reveal guards should be zeroized when the guard closes. SecretBytes should use a zeroizing buffer where available. SecretStr may store UTF-8 in a protected byte buffer with fallible UTF-8 views, or use another representation that preserves the public contract. Stdlib consumers should pass secret wrappers through typed APIs and reveal internally only at the final trusted boundary.
Layers affected¶
- Stdlib / Runtime (
incan_stdlib): must providestd.secrets,SecretStr,SecretBytes, redaction behavior, construction helpers, scoped reveal operations, protected-storage behavior where supported, and integration hooks for stdlib consumers. - Typechecker / Symbol resolution: must preserve the distinct types and reject implicit conversion from secret wrappers to plain
strorbytes. - Emission: generated Rust must preserve redacting display/debug behavior and best-effort zeroization where promised.
- Formatter: no syntax changes are required, but examples and generated code should preserve readable secret-type annotations.
- LSP / Tooling: hover, completion, diagnostics, semantic inspection, action metadata, generated docs, and policy checks should preserve sensitivity metadata and make reveal operations discoverable.
- Docs / Examples: environment, CLI, HTTP, logging, telemetry, and workflow examples should demonstrate secret values instead of plain string tokens.
Unresolved questions¶
- What are the exact reveal method names for
SecretStrandSecretBytes? - Should reveal operations return borrowed views, owned copies, scoped guards, or multiple variants?
- Should scoped reveal be the only stable v1 reveal surface, with owned plaintext extraction left for a later explicit escape hatch?
- Should encrypted idle storage be mandatory for all v1 targets, or a documented target capability with redaction and zeroization as the portable floor?
- How should process-local encryption keys be generated, stored, rotated, and destroyed?
- Should ordinary equality be available, or should secret comparison require explicit constant-time helper functions?
- Should
SecretStrattempt to provide the same zeroization behavior asSecretBytes, or should the docs makeSecretStrstrictly a redaction-first wrapper? - What exact redaction placeholder should display, debug, and diagnostic serialization use?
- Should default data serialization of secrets fail everywhere, or should some stdlib-owned formats serialize structured redaction objects by default?
- Should
std.secretseventually expose a genericSecret[T], and if so, what protocol mustTsatisfy? - Should secret provenance metadata distinguish environment variables, CLI input, config files, secret providers, and generated values in the initial surface?
- How should reveal sites interact with future policy approval, sandboxing, and capability checks?
- Should secret values participate in model field metadata automatically, or should fields still require an explicit
secret=truemarker for generated schema and docs?