Secrets that compose: can a workflow produce a secret, pass it on, and still never write it down?
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Design question raised by the owner, 2026-08-13. Umbrella; research workstreams link from here.
The question
Today a secret is a reference that resolves only inside the activity that needs it, and the value never enters workflow history. That is a strong property and it is why the current design is safe. It is also why the model does not yet compose.
Real workloads need a secret to be produced by one step and used by another:
- a step exchanges a credential for a short-lived access token, and three later steps call APIs with it
- a step reads a database password from a broker, and a
sql:step uses it - a plugin mints a scoped credential that only exists for this run
- a callee workflow returns something its caller must treat as secret
Every one of those wants a value to travel between steps. Every one of those is currently either impossible or done by writing the value into an output, which is exactly the thing the design forbids, because history is durable and broadly readable.
So the question is not "should secrets be allowed in outputs". It is: what is the thing that travels, such that composition works and the value still never gets written down?
What exists today, and what it gives us
${secret('scheme:name')}is a reference resolved inside the task activity that needs it, through a backend interface with tenant scoping.- Values are scrubbed from what a step reports.
sensitive:on an input, with a lint refusing alog:message that interpolates one directly, extended to compensation tasks in #509.- Containment is tested in the shapes that actually leak:
%v,%+v,%#v,%s, the value alone, inside a struct, inside a slice. - Two leak classes already found the hard way and written into CLAUDE.md: reflection through unexported fields, and unwrapping into persisted failures.
- A leak found and fixed today in the telemetry path (#531), and an open question about
${secret(...)}in shaped outputs (#534).
That is a real foundation. It is also entirely about keeping a secret in one place. Nothing in it describes a secret moving.
The design space, to be evaluated rather than assumed
- Handles. An output carries an opaque, unforgeable handle to a value held worker-side; history stores the handle, never the value. Composition works because a later step resolves the handle the same way it resolves a
${secret(...)}reference. The hard parts are lifetime (a durable run outlives a worker, and Continue-As-New crosses process boundaries), replay (a handle must survive or be re-derivable, deterministically), and scope (a handle must be useless to another tenant, another run, or a later replay by someone else). - Sealed values. History carries ciphertext, decrypted only where the key lives. Temporal's own payload codec mechanism is the ecosystem-coherent version of this, and it deserves serious evaluation because it is the answer the surrounding platform already has. The hard parts are key management and rotation, and the fact that history is durable forever: ciphertext written today is decryptable by anyone who obtains that key at any point in the future.
- Taint, as the ergonomic layer over either. A value derived from a secret stays marked, the marking propagates through expressions, and sinks refuse it. This is what makes the system intuitive rather than merely safe: the author writes the obvious thing and the platform refuses the dangerous one with a diagnostic. The hard part is that taint through CEL is a real analysis problem, and the existing lint already documents its boundary honestly,
string(inputs.token)is a known accepted gap. - Do nothing, deliberately. Say composition is out of scope and tell authors to resolve a secret in each step that needs it. This is the honest baseline every other option must beat, and for some of the cases above it is genuinely fine.
These are not exclusive. Handles plus taint is a plausible answer; sealed values plus taint is another.
Redaction and masking as a first-class surface, not an afterthought
Whatever travels, the sinks are the same, and every one of them needs to be enumerated and defended rather than patched when someone notices:
run outputs, run state, flow get and flow watch output, MCP tool responses, LSP hovers, log records, span attributes and events, metric labels, error messages and persisted failures, plugin RPC arguments, transcripts and test fixtures, flow compile output, and anything an agent can read.
The pattern this repo keeps rediscovering applies with force here: a rule enforced in one place and not its sibling. Five instances found this week. A secret model that composes multiplies the number of siblings.
The bar any proposal must clear
- Both drivers agree. Local execution must refuse exactly what durable execution refuses, or local runs stop predicting production.
- Fail closed. An unresolvable handle, an expired lease, a missing key: refuse, never fall back to a plaintext path.
- Bounded. Handle count, lifetime, and resolution cost are resources someone else controls.
- Expressible and testable. A Flowfile must say it,
flow validatemust refuse the incoherent cases with positions,flowtestmust let an author test a workflow that handles secrets without holding a real one, and an example must exercise it. - Legible to an agent. An MCP client reading a run must get the same guarantees a human does, and the refusals must be as clear to a model as to a person.
- Honest about what it does not protect. Any design still trusts the worker process and the task implementation. Say so plainly rather than implying more than is true.
Workstreams
Filed separately and linked as they land: an audit of where secret material can travel today, prior art including Temporal's payload codec and dynamic-secret brokers, and the sink inventory for redaction.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the current secret-reference backend, the containment tests, and the relevant notes in CLAUDE.md, then review the linked workstreams and issues #531 and #534. Compare the design options against the listed safety bar and document a bounded proposal, including affected sinks, validation behavior, testing needs, and what done means for both local and durable execution.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, distributed-systems, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100