Support secret attribute on @Option across all language ports#7736
Open
jkschneider wants to merge 1 commit into
Open
Support secret attribute on @Option across all language ports#7736jkschneider wants to merge 1 commit into
secret attribute on @Option across all language ports#7736jkschneider wants to merge 1 commit into
Conversation
Recipes that accept credentials (API tokens, passwords, etc.) can now mark those options with `secret = true`. The flag flows through `OptionDescriptor` and the RPC bridge so every language port (Java, Python, JavaScript, Go, C#) exposes it consistently. Downstream consumers (CLI traces, recipe-run history) use it to redact values at their persistence boundaries. A key invariant: `OptionDescriptor.value` is NOT redacted at the source. `RpcRecipe` and the recipe `withOptions` clone path read it directly to forward option values into recipe execution, and nulling at the source would silently break secret options for RPC peers. Redaction is opt-in via `OptionDescriptor.withRedactedSecretValue()` and is applied at each persistence boundary (e.g. `RecipeMarketplaceWriter.optionsToJson`). One source-level fix is needed regardless: `Recipe.getInstanceName()` composed single-required-option values into a user-visible string, so a secret-required option would have leaked the credential into dashboards. The filter now also drops `secret` options. Per port: - rewrite-core: `Option.secret()`, `OptionDescriptor.secret` + `withRedactedSecretValue()`, propagation through the 3 reflection sites in `Recipe.getOptionDescriptors()`, `getInstanceName()` filter fix, marketplace writer redaction. 9 new tests in `SecretOptionTest`. - rewrite-python: `secret: bool = False` on `OptionDescriptor` + `option()` factory; emitted in `_recipe_descriptor_to_dict`. 4 new tests. - rewrite-javascript: `secret?: boolean` on `OptionDescriptor` interface; spread carries it through `descriptor()`. 2 new tests. - rewrite-go: `Secret bool` on `OptionDescriptor` + `AsSecret()` fluent builder; emitted in `marketplaceOption` wire struct. 3 new tests. - rewrite-csharp: `Secret` on `OptionAttribute`, positional record param on `OptionDescriptor` + `WithRedactedSecretValue()`, propagated through reflection in `Recipe.GetOptionDescriptors()` and through the RPC `OptionDescriptorDto`. 2 new tests.
42c8c41 to
4df0469
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
secret = trueattribute on@Option(and equivalent across Python/JS/Go/C#) that flows throughOptionDescriptorand the RPC bridge to all language ports.Recipe.getInstanceName()now drops secret options from the composed name so single-required-secret recipes don't leak the value into user-visible strings.RecipeMarketplaceWriter.optionsToJsonredacts secret option values via the newOptionDescriptor.withRedactedSecretValue()helper.Why redact at persistence boundaries, not at
getDescriptor()OptionDescriptor.valuestays raw at the source.RpcRecipe.java:127-135readsOptionDescriptor::getValueto forward option values across RPC to Python/JS/Go/C# peers, andRecipe.withOptions(the clone path) reads it during Jackson rebinding. Nulling at the source would silently break secret options for every RPC recipe. Redaction is opt-in viawithRedactedSecretValue()and is called at each persistence/external boundary. The boundaries are few and well-defined — easier to audit than special-casing a source-level null.The exception:
Recipe.getInstanceName()reads option fields directly via reflection and composes them into a user-visible string. A required+secret-only recipe would leak the credential into dashboards, so theremoveIffilter atRecipe.java:148is extended to also drop secret options.Per-port summary
Option.java,OptionDescriptor.java,Recipe.java,RecipeMarketplaceWriter.java,SecretOptionTest.javasecret()attribute,withRedactedSecretValue()helper, propagation through 3 reflection sites ingetOptionDescriptors(), instance-name filter, marketplace-writer redaction.recipe.py,rpc/server.py,tests/test_secret_option.pysecret: bool = FalseonOptionDescriptor+option()factory; emitted in_recipe_descriptor_to_dict.recipe.ts,test/secret-option.test.tssecret?: booleanonOptionDescriptorinterface; spread carries it throughdescriptor().pkg/recipe/recipe.go,cmd/rpc/main.go,pkg/recipe/secret_option_test.goSecret boolfield +AsSecret()fluent builder; emitted inmarketplaceOptionwire struct.OptionAttribute.cs,OptionDescriptor.cs,Recipe.cs,RewriteRpcServer.cs,RecipeTest.csSecretonOptionAttribute, record param onOptionDescriptor+WithRedactedSecretValue()usingwithexpression, propagated through reflection and the RPCOptionDescriptorDto.Test plan
./gradlew :rewrite-core:test— full suite green, 9 newSecretOptionTesttests pass including the regression test that assertsgetInstanceName()does not leak a secret valuecd rewrite-python/rewrite && uv run pytest tests/test_secret_option.py tests/test_marketplace.py tests/test_data_table.py— 31 passedcd rewrite-javascript/rewrite && npm run testhelper -- test/secret-option.test.ts test/recipe.test.ts && npm run typecheck— green, typecheck cleancd rewrite-go && go test ./pkg/recipe/... && go build ./...— green, build cleancd rewrite-csharp/csharp && dotnet test --filter RecipeTest && dotnet build— 7 passed, build cleanFollow-up PRs
This is the foundation PR. Two consumer PRs depend on it:
trace.json, passsecretKeysto the SaaS via the GraphQL mutation.secretKeysat the event-log write boundary, exposeOption.secret: Boolean!in the GraphQL marketplace schema, makeRecipeOptionValue.valuenullable in recipe-run history.