feat(serialization,flow): harden the untrusted flow-file boundary (#345, #416, #400, #340)#474
Merged
Merged
Conversation
Bundles four tightly-coupled hardening issues that share the flow deserialization path (the issues explicitly cross-reference one another): #416 — parse-size & structural guardrails: add FlowParseLimits (max_bytes/nodes/depth/string_length/steps) applied by default in flow_from_json/yaml/dict; a byte-size precheck plus a bounded structural walk that also caps traversal of YAML alias/anchor expansion. Overridable via limits=, with FlowParseLimits.unlimited() to opt out. #345 — schema-ref module-resolution allowlist: a ContextVar-backed policy (set_schema_ref_policy / schema_ref_policy / SchemaRefAllowlist) consulted in resolve_class_ref BEFORE importlib.import_module, so a denied module's top-level code never runs. New SchemaRefPolicyError (CW-E051). Surfaced on the CLI as `--schema-ref-allow PREFIX` for `run` and `serve` (the commands that actually resolve refs). #400 — adversarial corpus: tests/corpus/flow_files with 29 malformed files + a manifest, driven through the library loaders and `chainweaver validate` (table + JSON), plus generated resource-shaped cases for the #416 limits. #340 — scheduled fuzz workflow: .github/workflows/fuzz.yml runs the existing fuzz harness weekly + on dispatch over a new schema-typed fixture (examples/fuzzable_linear.flow.yaml) against a graceful-handling invariant (examples/fuzz_properties.py); seed echoed for repro, counterexample uploaded on failure. Docs: security.md (untrusted flow-file loading), workflows.md (fuzz job + corpus), error-table.md (CW-E051). Public API + snapshot updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2nffdLkZjypkTDW6rGLeo
The lint job's actionlint/shellcheck flagged SC2153 ("SEED/RUNS may not be
assigned; did you mean seed/runs?") because the run script reassigned the
env-provided vars to lowercase locals. Drop the locals and reference the
all-caps env vars directly — shellcheck treats all-caps names as environment
variables, so neither SC2153 nor SC2154 fires. Behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2nffdLkZjypkTDW6rGLeo
Contributor
There was a problem hiding this comment.
Pull request overview
Hardens ChainWeaver’s untrusted flow-file deserialization boundary by adding configurable parse guardrails, introducing an opt-in schema-ref module allowlist (to prevent unsafe module imports), and adding regression infrastructure (adversarial corpus + scheduled fuzz workflow) to keep these guarantees stable over time.
Changes:
- Add
FlowParseLimits+ default limits applied byflow_from_json/flow_from_yaml/flow_from_dictto bound size/structure before validation. - Add
SchemaRefAllowlist+schema_ref_policy/set_schema_ref_policy(ContextVar-backed) and wire--schema-ref-allowintochainweaver run/serve. - Add adversarial corpus tests + scheduled fuzz workflow + supporting examples/docs/error-table/public-API snapshot updates.
Reviewed changes
Copilot reviewed 48 out of 49 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_schema_ref_policy.py |
Tests schema-ref allowlist matching, pre-import rejection, scoping, and CLI flag behavior. |
tests/test_flow_corpus.py |
Drives a manifest-defined invalid-flow corpus through library loaders and chainweaver validate, plus generated resource-shaped limit cases. |
tests/test_cli_serve.py |
Updates serve CLI test to include new schema_ref_allow param. |
tests/schema_ref_sentinel.py |
Sentinel module used to prove schema-ref rejection happens before import side effects. |
tests/fixtures/public_api.json |
Updates public API snapshot for new exports and loader signatures (limits=). |
tests/corpus/flow_files/README.md |
Documents corpus layout and how to add cases. |
tests/corpus/flow_files/manifest.json |
Manifest enumerating invalid-flow corpus inputs and expected detail substrings. |
tests/corpus/flow_files/invalid/top_level_list.flow.json |
Corpus case: invalid top-level shape (list). |
tests/corpus/flow_files/invalid/top_level_string.flow.json |
Corpus case: invalid top-level shape (string). |
tests/corpus/flow_files/invalid/top_level_number.flow.json |
Corpus case: invalid top-level shape (number). |
tests/corpus/flow_files/invalid/top_level_null.flow.json |
Corpus case: invalid top-level shape (null). |
tests/corpus/flow_files/invalid/top_level_list.flow.yaml |
Corpus case: invalid top-level shape (YAML list). |
tests/corpus/flow_files/invalid/top_level_scalar.flow.yaml |
Corpus case: invalid top-level shape (YAML scalar). |
tests/corpus/flow_files/invalid/trailing_comma.flow.json |
Corpus case: invalid JSON syntax. |
tests/corpus/flow_files/invalid/truncated.flow.json |
Corpus case: truncated JSON. |
tests/corpus/flow_files/invalid/bom_prefixed.flow.json |
Corpus case: UTF-8 BOM-prefixed JSON. |
tests/corpus/flow_files/invalid/unclosed_sequence.flow.yaml |
Corpus case: invalid YAML (unclosed sequence). |
tests/corpus/flow_files/invalid/tab_indentation.flow.yaml |
Corpus case: invalid YAML indentation (tabs). |
tests/corpus/flow_files/invalid/unsafe_python_tag.flow.yaml |
Corpus case: unsafe YAML tag rejected by safe loader. |
tests/corpus/flow_files/invalid/empty.flow.yaml |
Corpus case: empty YAML payload. |
tests/corpus/flow_files/invalid/missing_type.flow.json |
Corpus case: missing type discriminator (JSON). |
tests/corpus/flow_files/invalid/unknown_type.flow.json |
Corpus case: unknown type discriminator (JSON). |
tests/corpus/flow_files/invalid/missing_type.flow.yaml |
Corpus case: missing type discriminator (YAML). |
tests/corpus/flow_files/invalid/future_format_version.flow.json |
Corpus case: unsupported format_version. |
tests/corpus/flow_files/invalid/missing_required_fields.flow.json |
Corpus case: missing required fields (JSON). |
tests/corpus/flow_files/invalid/steps_not_a_list.flow.json |
Corpus case: wrong steps type (string). |
tests/corpus/flow_files/invalid/steps_null.flow.json |
Corpus case: steps is null. |
tests/corpus/flow_files/invalid/name_wrong_type.flow.json |
Corpus case: wrong name type. |
tests/corpus/flow_files/invalid/version_wrong_type.flow.json |
Corpus case: wrong version type. |
tests/corpus/flow_files/invalid/step_missing_tool_name.flow.json |
Corpus case: missing step tool_name. |
tests/corpus/flow_files/invalid/step_not_an_object.flow.json |
Corpus case: step element not an object. |
tests/corpus/flow_files/invalid/step_input_mapping_scalar.flow.json |
Corpus case: wrong input_mapping type. |
tests/corpus/flow_files/invalid/dag_missing_version.flow.json |
Corpus case: DAGFlow missing required version. |
tests/corpus/flow_files/invalid/dag_step_missing_id.flow.json |
Corpus case: DAGFlowStep missing required id. |
tests/corpus/flow_files/invalid/steps_scalar.flow.yaml |
Corpus case: steps scalar in YAML. |
tests/corpus/flow_files/invalid/missing_required_fields.flow.yaml |
Corpus case: missing required fields (YAML). |
examples/README.md |
Documents fuzz fixture/property used by the scheduled fuzz workflow. |
examples/fuzzable_linear.flow.yaml |
Adds a schema-typed example flow intended for fuzzing. |
examples/fuzz_properties.py |
Adds gracefully_handles_input property for fuzz gating. |
docs/security.md |
Documents untrusted flow-file guardrails and schema-ref allowlisting usage. |
docs/reference/error-table.md |
Adds CW-E051 for SchemaRefPolicyError. |
docs/agent-context/workflows.md |
Documents the fuzz workflow and adversarial corpus testing patterns. |
chainweaver/serialization.py |
Implements FlowParseLimits + default parse guardrails across loaders. |
chainweaver/flow.py |
Implements ContextVar-backed schema-ref policy and pre-import enforcement in resolve_class_ref. |
chainweaver/exceptions.py |
Adds SchemaRefPolicyError and assigns stable code CW-E051. |
chainweaver/cli/run.py |
Wires --schema-ref-allow into run and serve. |
chainweaver/cli/_shared.py |
Adds shared --schema-ref-allow option + helper to apply the allowlist. |
chainweaver/__init__.py |
Exposes new public symbols (FlowParseLimits, DEFAULT_PARSE_LIMITS, schema-ref policy helpers, SchemaRefPolicyError). |
.github/workflows/fuzz.yml |
Adds a scheduled/manual fuzz workflow that runs chainweaver fuzz and uploads minimized counterexamples on failure. |
- test_flow_corpus: assert the real >= 25 corpus cases (29 committed satisfy it) instead of >= 20, matching issue #400's acceptance criterion. - cli: fix the --schema-ref-allow help example so the sentence-final period isn't copy-pasted as a trailing dot in the module prefix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2nffdLkZjypkTDW6rGLeo
12 tasks
The `_assert_fast_failure` / library-loader corpus checks assert a guardrail-bounded parse fails in under `_MAX_PARSE_SECONDS`. Guardrail failures are milliseconds-scale, so the 2.0s ceiling only needs to catch an unbounded blowup or hang — not micro-time the parse. Widen it to 5.0s so it does not flake on a saturated CI runner while still proving the #416 fast-failure contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011E7oGoTqo75hWRJwLftdow
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
Hardens the untrusted flow-file input boundary — the primary untrusted
surface (repos, contributor PRs validated by the GitHub Action, generated
drafts). Four tightly-coupled issues that share the flow deserialization path
(
serialization.py+flow.resolve_class_ref+ the CI Action + the fuzzharness) and explicitly cross-reference one another as belonging in one PR:
Changes
chainweaver/serialization.py— addFlowParseLimits(
max_bytes/max_nodes/max_depth/max_string_length/max_steps) +DEFAULT_PARSE_LIMITS, applied automatically inflow_from_json/flow_from_yaml/flow_from_dict. A byte-size precheck runs before parse; abounded, iterative structural walk caps node count (also bounding YAML
alias/anchor expansion), depth, and string length; the step count is checked.
Each violation raises
FlowSerializationErrornaming the limit. Overridablevia
limits=;FlowParseLimits.unlimited()opts out for trusted input.chainweaver/flow.py+exceptions.py— aContextVar-backedschema-ref policy (
set_schema_ref_policy,schema_ref_policycontextmanager,
SchemaRefAllowlist) consulted inresolve_class_refbeforeimportlib.import_module, so a denied module is never imported. NewSchemaRefPolicyError(CW-E051). CLI:--schema-ref-allow PREFIXonrunand
serve.tests/corpus/flow_files/— 29 malformed files +manifest.json+README;
tests/test_flow_corpus.pydrives each through the library loadersand
chainweaver validate(table + JSON), with generated resource-shapedcases (oversized, deep nesting, huge string, 10k steps, YAML alias bomb) for
the Add parse-size and structural guardrails for untrusted flow files #416 limits.
.github/workflows/fuzz.yml— weeklyschedule+workflow_dispatchrunning
chainweaver fuzzover a new schema-typed fixture(
examples/fuzzable_linear.flow.yaml) againstgracefully_handles_input(
examples/fuzz_properties.py); seed = run id (echoed for local repro),minimized counterexample uploaded on failure. Scheduled-only (off the
PR-blocking path).
docs/security.md(loading untrusted flow files),docs/agent-context/workflows.md(fuzz job + corpus),docs/reference/error-table.md(
CW-E051), new public symbols inchainweaver/__init__.py+ regeneratedtests/fixtures/public_api.json.Testing
ruff check chainweaver/ tests/ examples/)ruff format --check chainweaver/ tests/ examples/)python -m mypy chainweaver/ tests/)python -m pytest tests/ -v— 2062 passed, 1 skipped, 93% coverage)test_flow_corpus.py,test_schema_ref_policy.py)The fuzz invocation was verified locally green across 5 seeds; the CLI
--schema-ref-allowflag was verified end-to-end (CW-E051 with a non-allowlistedref, clean run without).
Issues closed by this PR
Closes #345
Closes #416
Closes #400
Closes #340
Related Issues
Checklist
AGENTS.mdanddocs/agent-context/)Scope notes & decisions (Mode B)
implementation path and the issues themselves say to land together.
limits=API; I did notadd 5×N per-limit CLI flags (surface creep) — the conservative defaults
protect the CI Action automatically. Per-limit CLI overrides are an easy
follow-up if wanted.
--schema-ref-allowis onrun/serveonly, notvalidate/check:those commands only deserialize and never import refs, so the flag would be a
misleading no-op there.
flow_succeedspropertyintentionally flags inputs a flow rejects, and the fuzzer corrupts ~50% of
generated inputs — so it can never be green over a strict-schema flow. I added
a schema-typed fixture + a
gracefully_handles_inputinvariant (success withoutput, or a typed recorded failure — never a crash), which is the genuine
robustness contract a fuzz gate should protect.
🤖 Generated with Claude Code
https://claude.ai/code/session_01A2nffdLkZjypkTDW6rGLeo
Generated by Claude Code