Type scalar ArgsContainer fields strictly so env/config strings cannot bypass flags (v0.1.21) - #64
Merged
Volv-G merged 1 commit intoSep 25, 2026
Conversation
…t bypass flags (v0.1.21) *(AI-assisted)*
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.
(AI-assisted)
What
This is a security fix for a safety-gate bypass that 0.1.20's
_envintroduced (found by Binks on Shopify/discovery#35126)._envvalues are always strings, and so are their stringified defaults. A booleanArgsContainerfield without a converter therefore received the truthy string"false".Example:
tangle sdk secrets deleteloadsforce=(force, False)and prompts unlessargs.forceis truthy. So a config withforce: {_env: DELETE_FORCE, default: false}deleted without confirmation.skip_validation,no_wait,dry_run,trusted_hydration_cliand other flags have the same shape.A quoted YAML literal (
force: "false") had the same latent bug long before_envexisted.How
Scalar fields are typed strictly at the single field-resolution point,
ArgsContainer._resolve.strict_bool,strict_intandstrict_float, intangle_cli.args_container.bool/int/float).0/1, or exactlytrue/false,yes/no,1/0. Matching is case-insensitive, untrimmed, and ASCII-only. Everything else is rejected:"",on/off,true,2, lists, maps.1.0,1e3,0x1f,1_000, full-width digits and bools are rejected.nanandinfare rejected.ConfigFileErrornaming the field and its source, never the value, e.g.Invalid value for force from environment variable DELETE_FORCE (_env at config key 'force'): expected a boolean: …. Getting this wording required a small provenance side channel:_load_config_entriesreturns(config, env_sources), and_load_config_fileis an unchanged wrapper around it._envvalues nested in maps or lists (they stay strings; typing applies to scalar fields only).args._config.cfgpath, which never goes through_resolveand keeps its own typing rules. A test confirms this.YAML string literals: parsed strictly too
The same parser applies to a plain config string (
force: "false"→False), not only to_env/EnvFieldvalues. Reasons:submit-from-pythonrejected a stringtrusted_hydration_clibecause "a truthy string silently enables allow-all hydration"."true"flags and numeric strings (e.g.tangle api … limit: "10"), which used to pass through as strings.The
submit-from-pythontest is updated accordingly:"false"is now a validFalse, and"maybe"or["yes"]are rejected by the typed field.Built-in fields wired
bool | None/int | Noneoptions with aNonedefault can't be inferred, so their specs now name the converter:pipeline_runs_cli:dry_run,trusted_hydration_cli,include_*,stream,local_time,include_pipeline_names,include_execution_stats,dehydrate,limit(strict_int)published_components_cli:dry_run,allow_downgrade,include_deprecated,all_versions,follow_deprecated,full_speccomponents_cli:strip_code,use_legacy_naming,update_timestampFields with typed defaults (
force,hydrate,exit_on_first_failure,allow_failure,max_wait,poll_interval,submit_recovery_attempts, and generictangle apiparameters with bool/int defaults) are covered by inference.Downstream audit (Discovery
oasis/tangle-deploy/src,origin/main)forceinsecrets_manager,quota_group_manager,subscription_managerandairflow_converter;skip_validation/no_wait/graceful_shutdown/hydrateinrunnerandpipeline_run_from_python;dry_runinpublisher,quota_group_managerandsubscription_manager;no_update_tangle_auth_secret,skip_layout/force_layout,enable/disable/unpin,retry, themutex_*fields,concurrency_limit,port/timeout.strict_int/strict_floatin their specs downstream (they haveNonedefaults):capacity,page_size(quota_group_manager)expire_seconds,page_size(subscription_manager)semantic_k(component_search_v2)mutex_overlap_start_percent/mutex_previous_run_min_progress_percent_to_wait(pipeline_scheduler,runner)main) and is covered by this change.Failure modes
force: "on",force: "y"), or a non-integer string in an int field, now fails with a clear error instead of silently misbehaving.strict_booland ints: it accepts only0/1for an int value, soforce: 2is now rejected.Review focus
_defaultsprovenance merge.Tophatting
82 tests:
tangle sdk secrets delete --config:DELETE_FORCEunset /false/FALSE/no/0still prompts,true/yes/1skips, and invalid values are rejected without a prompt, a delete, or an echo._envand YAML literals.None-default fields.cfgpath, the precedence matrix, and_defaultsprovenance.on/offbool parsing; loose int/float parsing; lost provenance; value echo; and un-wiringtrusted_hydration_cli.Checklist
published_components_cli.pyexist onmaster). ruff clean on the changed lines.git diff --checkclean.uv buildfor both packages. The wheel is 0.1.21 andtest_packagingpasses.pyproject.toml,__init__.py,tests/test_packaging.py, and theuv.lockeditable self-entry (a two-line lock diff;uv locknot run)._envandEnvField.ArgsContainer.origin()remains the provisional, unused surface flagged in Read config values from the environment: _env, EnvField, and _select/_env in pipeline cfg (v0.1.20) #63.