Agent-First Data v0.13: Redaction Fails Closed

by Agent-First Kit Contributors

v0.10 scrubbed secrets inside URLs; v0.11 made redaction a policy. v0.13 closes the seams where a secret could still slip through — a marked container that leaked its non-secret siblings, a key collision that reverted to raw values, a schemeless connection string that no longer looked like a URL. When redaction is unsure, it now redacts.

A redaction rule is only as good as its worst case. The happy path — name a field api_key_secret, watch it become *** — has worked since the start. What matters for a credential is what happens at the edges: the value that is almost a secret, the field that is almost a URL, the collision that almost prints the raw bytes. v0.13 is about those edges. Everywhere redaction used to make a hopeful guess and pass a value through, it now fails closed.

A marked container leaked its siblings

_secret redacts a field. But a field can be an object:

{"credentials_secret": {"api_key": "sk-123", "endpoint": "https://h", "user": "app"}}

The old behavior traversed a marked container: it walked inside, redacted any nested _secret field, and left the rest. Here, nothing inside ends in _secret, so the whole object — key, endpoint, username — printed in the clear, even though the author had explicitly marked the container as a secret.

That is backwards. If you name something _secret, you are not asking AFDATA to go hunting for secrets inside it — you are telling it the whole thing is one. In v0.13 a _secret value is replaced wholesale, whatever its shape:

{"credentials_secret": "***"}

Scalar, object, array — a _secret value becomes the scalar string ***. A container you marked can no longer leak a single byte of its contents through any format.

A collision could revert a redacted value

YAML and Plain strip suffixes from keys, and when two keys collide after stripping (response_ms and response_bytes both → response) the formatter backs off and restores both to their original key and raw value. Necessary for correctness — but if one of those keys was _secret, “raw value” meant the real secret.

The fix is ordering. Redaction now runs before collision handling, so the value the fallback restores is already ***. There is no path — no suffix clash, no format quirk — where the collision logic can hand back a secret it was supposed to hide.

A connection string stopped looking like a URL

The _url suffix scrubs secrets inside a value that is a URL — userinfo password, secret-named query parameters — and leaves anything that isn’t a clean, scheme-prefixed, whitespace-free URL untouched. That no-op rule is what keeps it from mangling prose. But it had a dangerous blind spot:

{"database_url": "app:hunter2@db.internal:5432/prod"}

A schemeless connection string. No scheme://, so the surgical span logic has no anchor — and under the old rule, “not a clean URL” meant “pass through unchanged.” The password printed in full.

v0.13 makes that case fail closed. A _url value that isn’t a clean scheme-prefixed URL but still carries a credential signal — an @ userinfo sigil or internal whitespace — is now redacted wholesale to *** rather than waved through:

{"database_url": "***"}

Benign schemeless values still pass through: a relative URL like /cb?page=2 has no @ and no whitespace, so it is left alone. The rule only tightens around values that look like they are hiding something.

The same secret list now reaches your logs

secret_names lets you redact legacy field names you can’t rename — exact-match, no globbing, the same opt-in list everywhere. In v0.13 “everywhere” finally includes the logging modules. A secret_names entry now redacts the matching field by name inside emitted log lines across all four languages, not just in the output_* helpers. The redaction policy you configure for output and the one your logs obey are the same policy.

The honest caveat, stated loudly

Failing closed does not mean magic. _url scrubs the userinfo password and parameters that are named as secrets — suffixed _secret or listed in secret_names. It does not know that ?access_token=, ?api_key=, ?code=, or ?sig= are sensitive. Those pass through unchanged unless you say otherwise, and v0.13 documents that in bold in the spec and skill rather than letting anyone assume a _url field makes an arbitrary URL safe to log.

The rule is still: when you own the URL, rename the parameter to the _secret suffix; when you don’t, list it in secret_names. No built-in table of “sensitive-looking” names — inventing one would be exactly the guessing the convention exists to replace. What changed in v0.13 is that the structural cases AFDATA can be sure about — a marked subtree, a credential-bearing connection string — stop guessing in the unsafe direction.

Also in v0.13

As always, identical in Rust, Go, Python, and TypeScript, pinned by shared fixtures. The theme of the release is a single sentence: when redaction is unsure, it redacts.