User responsibility¶
kstlib does not see and cannot redact every piece of data that flows through its public APIs. Six surfaces let user-provided content reach a log line, an email body, or a return value that the caller must vet. This page documents each one so library users know what they own and what kstlib already takes care of.
Why this page exists¶
The v2.5.0 audit pass found that kstlib itself is now defensive on the
sanitization axis: secrets in transports / providers carry
field(repr=False), dataclasses with sensitive payloads override
__repr__, and the four HIGH leak gaps (auth callback OAuth code,
WebSocket URL credentials, pipeline shell command, pipeline subprocess
stderr) were all closed. What kstlib cannot decide on its own is
whether the strings, exceptions, and return values you pass through its
APIs contain secrets. Those six cases below are the places where the
contract is shared.
When in doubt, the rule of thumb is redact at the boundary you control: strip credentials from a message before it reaches the kstlib API, or wrap user code with your own sanitizer. Treat any field listed below as if it could land verbatim in a summary email or a DEBUG log.
1. NotifyCollector exception / return_value / traceback¶
Module: kstlib.mail (kstlib.mail.NotifyCollector,
kstlib.mail.NotifyResult)
The @notify decorator captures, for each decorated function call, the
return value, the exception (if raised), and the traceback. Those are
later rendered into the summary email by NotifyCollector.render_html()
/ render_plain() / to_monitor_table().
What kstlib does¶
Since v2.5.0 the collector defaults to redact_user_data=True. In that
mode the Detail column shows:
For failures:
"<ExcType>: [REDACTED] (NotifyCollector(redact_user_data=False) to view)"For successful return values:
"[REDACTED] ..."The tracebacks block in
render_html(include_tracebacks=True)is suppressed entirely, regardless ofinclude_tracebacks.
to_context() surfaces ctx["redact_user_data"] so custom Jinja
templates can decide whether to render {{ result.exception }} or
{{ result.traceback_str }} directly. The raw NotifyResult objects
stay in ctx["results"] so existing template code keeps working.
Where the user must care¶
Set redact_user_data=False only when you control the decorated
functions and know they:
never raise with sensitive content in the exception message,
never return objects whose
repr()exposes a secret,never include credentials in their tracebacks.
Don’t
from kstlib.mail import NotifyCollector
# Decorated function leaks a token in its exception message
@notify(collector=collector)
def fetch():
raise ValueError(f"Auth failed for token={MY_TOKEN}") # leak
# This collector renders the verbatim message in the summary email :
collector = NotifyCollector(redact_user_data=False)
Do
# Default collector hides exception messages :
collector = NotifyCollector() # redact_user_data=True
# Or sanitize the exception ourselves before re-raising :
@notify(collector=collector)
def fetch():
try:
...
except AuthError:
raise ValueError("Auth failed (see provider logs)") from None
2. CallableError propagation in transform/¶
Module: kstlib.transform (kstlib.transform.CallableError,
kstlib.transform.chain)
A transform.chain whose YAML lists a callable: my.module:fn patch
imports and invokes that callable on each forward / patch / backward
step. If the callable raises, the chain wraps the exception in a
CallableError(target_label, str(error_box[0]), chain_name=...). The
str(error_box[0]) part is the user code’s own exception message.
What kstlib does¶
The exception type, the chain name, and the target label are kstlib data. The exception message is forwarded as-is from your callable.
Where the user must care¶
If your callable raises with a payload-derived message, that string
becomes the body of CallableError and bubbles up to whatever logger
or error handler the application uses.
Don’t
def my_patch(data, **kwargs):
if "bad" in data:
# The first 200 chars of the payload now travel inside the
# CallableError message.
raise ValueError(f"Invalid payload: {data[:200]}")
Do
def my_patch(data, **kwargs):
if "bad" in data:
raise ValueError(
f"Invalid payload (length={len(data)}, type={type(data).__name__})"
)
The pattern matches kstlib’s own transform/primitives.py: exception
messages carry type and size, never content slices.
3. StepResult.stdout / StepResult.stderr in pipeline/¶
Module: kstlib.pipeline (kstlib.pipeline.StepResult)
A pipeline ShellStep / PythonStep returns a StepResult whose
stdout and stderr attributes contain the complete captured
output of the subprocess. StepResult.error is also populated with the
stripped stderr on failure.
What kstlib does¶
Since v2.5.0 the failure WARNING dropped the stderr from the log
(“Step ‘
Where the user must care¶
If your pipeline definition runs curl -u user:pass, psql -c "GRANT TO ... IDENTIFIED BY 'pwd'", or any tool that echoes credentials in
its stderr, the secret is sitting in result.stderr. If you forward
that field to a logger, an email, or a dashboard, the secret follows.
Don’t
result = step.execute(config)
if result.status is StepStatus.FAILED:
log.error("Pipeline failed: %s", result.stderr) # may leak
Do
result = step.execute(config)
if result.status is StepStatus.FAILED:
log.error("Pipeline step '%s' failed (rc=%d)", config.name, result.return_code)
# Inspect result.stderr only in interactive debugging, not in
# automated logs.
The same applies to StepConfig.command / StepConfig.args. Prefer
passing credentials via StepConfig.env rather than command-line
flags - the helper _sanitize_command() redacts known shapes
(Authorization: Bearer ..., --password=..., sshpass -p ...,
PGPASSWORD=...) before the DEBUG log, but it is best-effort and only
catches patterns it knows about.
4. NotifyResult fields surfaced to summary builders¶
Module: kstlib.mail (kstlib.mail.NotifyResult)
NotifyResult is the dataclass populated by the @notify decorator
for each call. It exposes exception, return_value, traceback_str,
function_name, started_at, ended_at, duration_ms, success.
What kstlib does¶
NotifyResult.exception is the actual exception instance. Its default
repr() is the exception’s own repr, which kstlib does not control.
return_value is your function’s return - kstlib stores it as-is.
traceback_str is the formatted traceback string at decoration time.
The NotifyCollector rendering helpers respect the redact_user_data
flag (see #1), but direct access to a NotifyResult instance does
not - the dataclass remains a transparent record.
Where the user must care¶
If you build a custom rendering pipeline (Jinja template, audit log,
Slack message) from a list of NotifyResult, you are responsible for
deciding which fields to surface. Treat exception, return_value,
and traceback_str as user-controlled.
Do
# Custom Jinja template guarded by the collector's flag :
template = """
{% for r in results %}
{{ r.function_name }} : {{ "OK" if r.success else "FAILED" }}
{% if not redact_user_data and r.exception %}
{{ r.exception }}
{% endif %}
{% endfor %}
"""
context = collector.to_context()
rendered = jinja_env.from_string(template).render(**context)
5. AlertMessage title and body¶
Module: kstlib.alerts (kstlib.alerts.AlertMessage)
AlertMessage(title, body, level, ...) is the user-composed payload
delivered to channels (Slack, email, …). The title is truncated to
50 characters before logging; the body is treated as the message you
want the channel to display.
What kstlib does¶
AlertManager.send(...) truncates the title before logging, and
channels (SlackChannel, EmailChannel) use mask_webhook_url()
(Slack) or transport-level __repr__ redaction (email transports) to
keep their own configuration out of the logs.
Where the user must care¶
The body is by design what travels to the channel. If you build a body that contains credentials, those credentials reach Slack / your inbox. That is rarely a “secret” issue (you trust the channel) but it is one when the channel is shared, archived, or forwarded.
Do
alert = AlertMessage(
title="Pipeline failure",
body=(
"step=publish-report\n"
"rc=2\n"
"logs: see CI run https://ci.example.com/run/12345\n"
),
level=AlertLevel.WARNING,
)
Avoid embedding raw exception messages, full configs, or anything you would not put in the channel’s persistent history.
6. WebSocket on_message / on_error callbacks¶
Module: kstlib.websocket (kstlib.websocket.WebSocketManager)
WebSocketManager accepts on_connect, on_message, on_disconnect,
on_error callbacks. The data they receive (frame payload, close
reason, exception) is passed to user code unchanged.
What kstlib does¶
Since v2.5.0 the connect log uses mask_url() to redact userinfo and
sensitive query parameters from self._url, and the close-frame
reason is split (short WARNING + redacted TRACE detail). The library
itself never logs the message payloads.
Where the user must care¶
If your on_message does log.debug("Received: %s", message), every
frame lands in your DEBUG stream. WebSocket frames from trading APIs
or chat services routinely include account ids, balances, or session
identifiers.
Do
def on_message(message):
# Process the structured payload, but log only what is safe to log.
payload = json.loads(message)
log.debug("Received frame: type=%s seq=%d", payload.get("type"), payload.get("seq"))
ws = WebSocketManager(url, on_message=on_message)
See also¶
Logging - the logging system itself
Logging introspection guide - 7-level convention, cascade, recipes, and the 12 sanitization model patterns (
field(repr=False), helper redactions,[SECURITY]tag, theHTTPTraceLoggerinfrastructure).