Skip to content

feat(python-pipeline): support dynamic_secret runtime-secret arguments#32

Merged
Silin144 merged 2 commits into
TangleML:masterfrom
Silin144:feat/dynamic-secret
Jul 18, 2026
Merged

feat(python-pipeline): support dynamic_secret runtime-secret arguments#32
Silin144 merged 2 commits into
TangleML:masterfrom
Silin144:feat/dynamic-secret

Conversation

@Silin144

@Silin144 Silin144 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

What

Ports the dynamic_secret / DynamicData authoring primitive into OSS tangle-cli so Python-authored pipelines can express a runtime-resolved secret argument natively, without reaching into a downstream package.

Tangle runnable arguments can be literals, graph/task edges, or dynamic data resolved by the runtime (e.g. a secret reference). Until now the Python emitter had no explicit wrapper for the dynamic-data case, so author code couldn't request a dynamicData argument without opening support for arbitrary dict constants. This adds a narrow, typed primitive for exactly that.

Surface

from tangle_cli.python_pipeline import dynamic_secret

@task
def call_model(openai_api_key: In[str] = dynamic_secret("OPENAI_API_KEY")):
    ...

Emits:

openai_api_key:
  dynamicData:
    secret:
      name: OPENAI_API_KEY
  • dynamic_secret(name) — the public authoring entry point; raises ValueError on a non-empty-string name. Re-exported from tangle_cli.python_pipeline (added to __all__, not # noqa).
  • DynamicData(value) — internal frozen-dataclass emit primitive, emitted as {"dynamicData": <value>}. Deliberately not part of the public surface: value is a general mapping (the runtime resolves several kinds — secrets, run IDs, loop indices), but the only public producer today is dynamic_secret, which always builds the schema-valid secret shape. Any other shape fails closed at compile validation until the constructor and schema are extended together, so the public API never promises more than the schema enforces.

Changes

  • python_pipeline/dynamic_data.py — new DynamicData + dynamic_secret.
  • python_pipeline/__init__.py — re-export dynamic_secret via __all__ (DynamicData stays internal).
  • python_pipeline/emit.py — emit DynamicData args as {"dynamicData": ...}.
  • schema_validation.py + schemas/dehydrated_pipeline_schema.json — the dehydrated (strict) schema now recognizes dynamicData.secret.name. The runnable (pipeline_schema.json, API-generated) schema stays intentionally permissive.
  • Tests: tests/test_python_pipeline_dynamic_data.py (new, 79 lines) covers the wrapper contract, emit shape, validation, and the non-empty-name guard; one import-surface assertion added to tests/test_python_pipeline_dsl.py.

Testing

  • Full OSS suite green; ruff clean.

🤖 Generated with Claude Code

Adds a narrow Python-pipeline authoring primitive for runtime secret
arguments, porting the reusable infrastructure from the downstream
tangle-deploy repo (discovery #32848) into OSS tangle-cli:

- dynamic_secret(name) → a DynamicData wrapper emitted as
  {"dynamicData": {"secret": {"name": ...}}}.
- The emitter dispatches DynamicData before the non-string-constant
  rejection, so a dynamicData argument is no longer rejected as a bare
  dict constant.
- The dehydrated-pipeline schema gains a strict DynamicDataArgument
  (non-empty secret.name only; arbitrary dynamic data stays rejected).
  The runnable pipeline_schema.json is left untouched: it already carries
  an API-generated, intentionally permissive DynamicDataArgument that
  mirrors the real runtime contract (secrets, run IDs, loop indices).
- schema_validation's shape detector recognizes the dynamicData wrapper.

Tests cover emit shape, a compiling fixture, strict-schema rejection of
malformed secret references, and the non-empty-name guard. Full suite:
780 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Silin144
Silin144 requested review from Ark-kun and Volv-G as code owners July 17, 2026 14:04
Comment on lines +14 to +18
@dataclass(frozen=True)
class DynamicData:
"""A task argument value emitted as ``{"dynamicData": ...}``."""

value: Mapping[str, Any]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI-assisted) [nit] DynamicData is a general wrapper but the schema only accepts the secret.name shape. value: Mapping[str, Any] accepts any mapping, and emit renders it verbatim as {"dynamicData": value} — but the dehydrated schema (DynamicSecretData, additionalProperties:false, required:["secret"]) only allows the secret shape. So a DynamicData({"someOtherKind": ...}) would emit yet fail compile-validation, even though the runtime resolves other dynamic-data kinds (run IDs, loop indices — per this PR's own test docstring). Consider either narrowing the class to secrets-only, or broadening the schema to match the documented runtime generality, so the type doesn't promise more than the schema enforces.

Minor also: @dataclass(frozen=True) with a dict field leaves instances effectively unhashable (unlike sibling Raw, which defines __hash__). Harmless today (never hashed), just inconsistent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 4949b4a. Rather than loosen the strict schema, I documented DynamicData on the class as an internal emit primitive (kept out of __all__): its value stays a general mapping since the runtime resolves several kinds, but the only public producer dynamic_secret always builds the schema-valid secret shape, and any other shape fails closed at compile validation until the constructor + schema are extended together. So the public API never promises more than the schema enforces.

On the hash: since it holds a dict and is never hashed, I left it unhashable rather than fabricate a canonical form — happy to add a __hash__ for parity with Raw if you would prefer the consistency.

"registered",
"ref",
"raw",
"dynamic_secret",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI-assisted) [nit] Only dynamic_secret is re-exported here, not DynamicData. The PR description says "Both re-exported from tangle_cli.python_pipeline (added to __all__)", but DynamicData — the type users would annotate or isinstance-check against — is reachable only via the .dynamic_data submodule. Either add DynamicData to the import + __all__ too, or adjust the description.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept DynamicData internal (only dynamic_secret is in __all__) and corrected the PR description to match — the "both re-exported" line was the inaccurate part. DynamicData is only an emit primitive, so users never need to annotate or isinstance-check against it.

@Volv-G Volv-G left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI-assisted) Clean, narrow, well-tested port of the dynamic_secret / DynamicData primitive — emit dispatch, strict dehydrated schema (secret.name, non-empty, no extra props), and good negative tests. Left two nits inline (class generality vs. schema narrowness; DynamicData not top-level exported). Neither blocks. LGTM.

@Silin144 Silin144 self-assigned this Jul 18, 2026
Address review nit: the class read as a general public wrapper whose
`value: Mapping` promised more than the strict dehydrated schema
(secret.name only) enforces. Clarify on the class that DynamicData is an
internal emit primitive (not in __all__), that the only public producer
`dynamic_secret` always builds the schema-valid secret shape, and that
any other kind fails closed at compile validation until constructor and
schema are extended together — so the public API never over-promises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Silin144
Silin144 merged commit ae9d502 into TangleML:master Jul 18, 2026
1 check passed
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.

2 participants