The envelope
SecretValue, from tai42_contract.secrets, wraps a real value inside a tool’s
return:
reveal()is the only way out.wrapped.reveal()returns the real value; nothing else does.- Its text form is a placeholder.
reprandstrboth yieldSecretValue([secret]), so it cannot leak through a log line or an f-string. - It is not JSON-serializable — on purpose. Any un-audited path that tries to
dump it raises
TypeErrorand fails loudly, rather than quietly emitting the value. This fail-safe is what makes the wrapper safe to carry through code that was never told a secret is inside. - It compares and hashes by identity, not by value, so equality checks and set membership cannot become a timing or logging trap.
What each surface sees
The wrapper travels intact through the platform’s shared tool-result seam. Each door then decides for itself: a live edge reveals the real value once; every recording masks it to the[secret] placeholder.
The one handoff of a real secret is therefore a live, synchronous call — a
direct run-tool response, an MCP tool call, or a preset over one. A detached run
that still executes in-process — a background submit, a hook or trigger fire
— has no live caller to hand the value to, so it only records, and records the
placeholder. A run relocated to a backend worker — a scheduled run, a
backend-worker task — never reaches that in-process recorder: the wrapper does
not serialize onto the wire, so the run fails loudly rather than carry a secret
across the process boundary, by design. Plan a secret-producing tool to be
called synchronously by whoever consumes the value.
A model never sees a secret either: the adapter that feeds an agent’s model,
checkpoint, and callback trace masks the wrapper before the result leaves it.
When a preset pins an output schema and a secret-bearing
result violates it, the error keeps the failing JSON path but replaces the value
with
[secret]. The plaintext never rides a raised validation error into a log.Sensitive questions
ask_user can collect a secret from a human. Pass
sensitive=True and:
- the answer comes back wrapped in a
SecretValue, so the tool reaches it only throughreveal(); - the durable question record keeps only the answered status — the answer body is never persisted;
- if the turn is killed on its timeout while a sensitive question is still
pending, the timeout error names the interaction but shows
[sensitive question]in place of the question text.
Author rules
- Wrap at the return site. Construct the
SecretValuewhere the value is produced and put it straight into the return. One wrap protects the value on every surface downstream; there is nothing else to remember. - Consume a secret where you receive it. Reveal it and act on it — persist it through the config API, hand it to the provider call that needs it — in the same live call that produced it. Do not pass a wrapper onward as a task or chain result: cross-process transit is unsupported and fails loudly (the wrapper will not serialize onto the wire), by design, so a secret cannot silently ride a background handoff.
- Do not wrap shareable values. Ids, endpoint URLs, links, and other non-secret fields stay plain, so callers and models can read and act on them. Wrap only the value that must not be recorded.
Example
A generic tool that mints a one-time secret returns it wrapped, and writes it nowhere itself:secret once. A second tool consumes it in the same synchronous call — revealing
it only to write it into the env under a name the server later reads it back by:

