Skip to content

fix: make the masking registry per-invocation - #511

Merged
josegonzalez merged 1 commit into
mainfrom
501-per-invocation-masking
Sep 2, 2026
Merged

fix: make the masking registry per-invocation#511
josegonzalez merged 1 commit into
mainfrom
501-per-invocation-masking

Conversation

@josegonzalez

Copy link
Copy Markdown
Member

The set of values docket must not print was process-wide, and the API that filled it replaced the set rather than adding to it. Each command populated it at the top of Run and deferred a clear, so two runs sharing a process had the first one's teardown blank the second one's secrets while it was still writing output. That is fail-open, and it is the last of the three globals #423 named.

It becomes a subprocess.Masker the run owns. The layers that mask text but have no other reason to know about a run - the subprocess transports, a task registering a secret it just read back off the server - reach it through the context they already receive. The layers that render but have no context - the human formatter, the JSON emitter, docket validate - are handed it at construction, which is where they already take the Ui.

There is no Set. Nothing needs one: a run's secrets only accumulate, and a masker goes out of scope with the run that owns it, so there is no teardown left to get wrong. A nil *Masker masks nothing and is safe to call, which is what lets every masking site drop its nil check - a caller that registered no secrets has nothing to hide.

Three places could not take either route. unknownPlayError.Error() has a fixed signature and formats lazily on purpose, because the message must name secrets registered after the error was built (#477); it carries the masker, and since that is a pointer to the run's own, the later registrations still reach it. docket validate has no context at all, being offline by contract, so its masker rides on the command struct. And maskedStrings straddles the emitter boundary, serving both the JSON stream and --list-tasks, so it takes the masker as an argument rather than becoming a method.

Two things from #507 are deleted rather than migrated. The TestMain canary that failed the tasks run when the registry was dirty, and the isolateMaskRegistry helper its soundness depended on, both existed to police a global that no longer exists. The three tasks test files that #502 had to leave serial for this reason are unblocked; turning them parallel belongs with the rest of that migration.

Fixes #501.

Unblocks the commands half of #502, alongside #505 and #506.

The set of values docket must not print was process-wide, and the API that filled it *replaced* the set rather than adding to it. Each command populated it at the top of `Run` and deferred a clear, so two runs sharing a process had the first one's teardown blank the second one's secrets while it was still writing output. That is fail-open, and it is the last of the three globals #423 named.

It becomes a `subprocess.Masker` the run owns. The layers that mask text but have no other reason to know about a run - the subprocess transports, a task registering a secret it just read back off the server - reach it through the context they already receive. The layers that render but have no context - the human formatter, the JSON emitter, `docket validate` - are handed it at construction, which is where they already take the Ui.

There is no `Set`. Nothing needs one: a run's secrets only accumulate, and a masker goes out of scope with the run that owns it, so there is no teardown left to get wrong. A nil `*Masker` masks nothing and is safe to call, which is what lets every masking site drop its nil check - a caller that registered no secrets has nothing to hide.

Three places could not take either route. `unknownPlayError.Error()` has a fixed signature and formats lazily on purpose, because the message must name secrets registered after the error was built (#477); it carries the masker, and since that is a pointer to the run's own, the later registrations still reach it. `docket validate` has no context at all, being offline by contract, so its masker rides on the command struct. And `maskedStrings` straddles the emitter boundary, serving both the JSON stream and `--list-tasks`, so it takes the masker as an argument rather than becoming a method.

Two things from #507 are deleted rather than migrated. The `TestMain` canary that failed the `tasks` run when the registry was dirty, and the `isolateMaskRegistry` helper its soundness depended on, both existed to police a global that no longer exists. The three `tasks` test files that #502 had to leave serial for this reason are unblocked; turning them parallel belongs with the rest of that migration.

Fixes #501.
@josegonzalez
josegonzalez merged commit 4edb2d8 into main Sep 2, 2026
19 checks passed
@josegonzalez
josegonzalez deleted the 501-per-invocation-masking branch September 2, 2026 01:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the masking registry per-invocation

1 participant