flowspec2 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. flowspec2/__init__.py +105 -0
  2. flowspec2/authoring/__init__.py +275 -0
  3. flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
  4. flowspec2/authoring/authoring-evidence.schema.json +455 -0
  5. flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
  6. flowspec2/authoring/benchmark.py +1409 -0
  7. flowspec2/authoring/corpus/await_correction.case.json +220 -0
  8. flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
  9. flowspec2/authoring/corpus/gated_derive.case.json +100 -0
  10. flowspec2/authoring/corpus/linear.case.json +55 -0
  11. flowspec2/authoring/corpus/manifest.json +31 -0
  12. flowspec2/authoring/corpus/subflow.case.json +66 -0
  13. flowspec2/authoring/corpus/terminal.case.json +94 -0
  14. flowspec2/authoring/corpus.py +307 -0
  15. flowspec2/authoring/ctk.py +836 -0
  16. flowspec2/authoring/detached_signature.py +120 -0
  17. flowspec2/authoring/evidence.py +311 -0
  18. flowspec2/authoring/evidence_signature.py +187 -0
  19. flowspec2/authoring/evidence_verification.py +229 -0
  20. flowspec2/authoring/gemini.py +215 -0
  21. flowspec2/authoring/operational-corpus.json +61 -0
  22. flowspec2/authoring/operational.py +956 -0
  23. flowspec2/authoring/operational_providers.py +167 -0
  24. flowspec2/authoring/presentation_review.py +1013 -0
  25. flowspec2/authoring/presentation_review_signature.py +305 -0
  26. flowspec2/authoring/projection.py +299 -0
  27. flowspec2/authoring/provider_prompt.py +83 -0
  28. flowspec2/backends/__init__.py +82 -0
  29. flowspec2/backends/http.py +189 -0
  30. flowspec2/checker.py +238 -0
  31. flowspec2/cli.py +789 -0
  32. flowspec2/cli_parser.py +345 -0
  33. flowspec2/clock.py +23 -0
  34. flowspec2/compat/__init__.py +47 -0
  35. flowspec2/compat/models.py +82 -0
  36. flowspec2/compat/open_workflow.py +616 -0
  37. flowspec2/compat/rasa.py +19 -0
  38. flowspec2/compat/rasa_export.py +876 -0
  39. flowspec2/compat/rasa_import.py +992 -0
  40. flowspec2/compat/rasa_shared.py +270 -0
  41. flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
  42. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
  43. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
  44. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
  45. flowspec2/compat/tool_profiles.py +149 -0
  46. flowspec2/compat/yaml.py +147 -0
  47. flowspec2/compiler.py +957 -0
  48. flowspec2/compiler_contracts.py +17 -0
  49. flowspec2/compiler_resume_contracts.py +594 -0
  50. flowspec2/compiler_schema_relations.py +1830 -0
  51. flowspec2/compiler_tool_contracts.py +245 -0
  52. flowspec2/compiler_value_contracts.py +447 -0
  53. flowspec2/derive.py +32 -0
  54. flowspec2/diagnostics.py +94 -0
  55. flowspec2/domains.py +489 -0
  56. flowspec2/experimental/__init__.py +57 -0
  57. flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
  58. flowspec2/experimental/v3-preview-loss-policy.json +161 -0
  59. flowspec2/experimental/v3_lowering.py +928 -0
  60. flowspec2/experimental/v3_preview.py +2460 -0
  61. flowspec2/flowspec-2.schema.json +635 -0
  62. flowspec2/interactive.py +300 -0
  63. flowspec2/ir.py +1459 -0
  64. flowspec2/json_codec.py +120 -0
  65. flowspec2/llm.py +366 -0
  66. flowspec2/models.py +121 -0
  67. flowspec2/nodes.py +1537 -0
  68. flowspec2/observability.py +151 -0
  69. flowspec2/predicates.py +89 -0
  70. flowspec2/profiles.py +118 -0
  71. flowspec2/py.typed +0 -0
  72. flowspec2/runtime.py +1059 -0
  73. flowspec2/schema.py +54 -0
  74. flowspec2/schema_contracts.py +203 -0
  75. flowspec2/semantic_derive_contracts.py +530 -0
  76. flowspec2/semantic_path_contracts.py +528 -0
  77. flowspec2/semantic_predicate_contracts.py +355 -0
  78. flowspec2/semantic_schema_contracts.py +137 -0
  79. flowspec2/semantic_source_contracts.py +21 -0
  80. flowspec2/semantic_state_contracts.py +450 -0
  81. flowspec2/semantic_support.py +40 -0
  82. flowspec2/semantics.py +410 -0
  83. flowspec2/state_migration.py +1034 -0
  84. flowspec2/subflows/__init__.py +1117 -0
  85. flowspec2/subflows/address.py +328 -0
  86. flowspec2/subflows/identification.py +1031 -0
  87. flowspec2/tools.py +1154 -0
  88. flowspec2-1.0.0.dist-info/METADATA +482 -0
  89. flowspec2-1.0.0.dist-info/RECORD +92 -0
  90. flowspec2-1.0.0.dist-info/WHEEL +4 -0
  91. flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
  92. flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
flowspec2/schema.py ADDED
@@ -0,0 +1,54 @@
1
+ """Load + JSON-Schema-validate a flowspec/2 document."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from pathlib import Path
7
+ from typing import Any, cast
8
+
9
+ import jsonschema
10
+ from jsonschema.exceptions import best_match
11
+
12
+ from .json_codec import strict_json_loads
13
+
14
+ _SCHEMA_PATH = Path(__file__).with_name("flowspec-2.schema.json")
15
+ _schema_cache: dict[str, Any] | None = None
16
+ _validator_cache: jsonschema.Draft202012Validator | None = None
17
+
18
+
19
+ def _cached_schema() -> dict[str, Any]:
20
+ global _schema_cache
21
+ cached = _schema_cache
22
+ if cached is None:
23
+ cached = strict_json_loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
24
+ jsonschema.Draft202012Validator.check_schema(cached)
25
+ _schema_cache = cached
26
+ return cached
27
+
28
+
29
+ def schema() -> dict[str, Any]:
30
+ """Return an owned copy of the cached FlowSpec2 Draft 2020-12 schema."""
31
+ return copy.deepcopy(_cached_schema())
32
+
33
+
34
+ def _cached_validator() -> jsonschema.Draft202012Validator:
35
+ global _validator_cache
36
+ cached = _validator_cache
37
+ if cached is None:
38
+ cached = jsonschema.Draft202012Validator(_cached_schema())
39
+ _validator_cache = cached
40
+ return cached
41
+
42
+
43
+ def validate_flow(doc: dict[str, Any]) -> None:
44
+ """Raise ``jsonschema.ValidationError`` if the document is not valid flowspec/2."""
45
+ if validation_error := best_match(_cached_validator().iter_errors(doc)):
46
+ raise validation_error
47
+
48
+
49
+ def load_flow(path: str | Path, *, validate: bool = True) -> dict[str, Any]:
50
+ """Read a flow document from disk and (by default) validate it."""
51
+ doc = cast(dict[str, Any], strict_json_loads(Path(path).read_text(encoding="utf-8")))
52
+ if validate:
53
+ validate_flow(doc)
54
+ return doc
@@ -0,0 +1,203 @@
1
+ """Closed local JSON Schema reference and resume-token contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from typing import Any, Final
8
+ from urllib.parse import unquote
9
+
10
+ from jsonschema import Draft202012Validator
11
+ from jsonschema.exceptions import SchemaError
12
+
13
+ _DRAFT_2020_12_DIALECTS: Final[frozenset[str]] = frozenset(
14
+ {
15
+ "https://json-schema.org/draft/2020-12/schema",
16
+ "https://json-schema.org/draft/2020-12/schema#",
17
+ }
18
+ )
19
+ _SINGLE_NESTED_SCHEMA_KEYWORDS: Final[tuple[str, ...]] = (
20
+ "additionalProperties",
21
+ "contains",
22
+ "contentSchema",
23
+ "else",
24
+ "if",
25
+ "items",
26
+ "not",
27
+ "propertyNames",
28
+ "then",
29
+ "unevaluatedItems",
30
+ "unevaluatedProperties",
31
+ )
32
+ _ARRAY_NESTED_SCHEMA_KEYWORDS: Final[tuple[str, ...]] = (
33
+ "allOf",
34
+ "anyOf",
35
+ "oneOf",
36
+ "prefixItems",
37
+ )
38
+ _MAPPING_NESTED_SCHEMA_KEYWORDS: Final[tuple[str, ...]] = (
39
+ "$defs",
40
+ "definitions",
41
+ "dependentSchemas",
42
+ "patternProperties",
43
+ "properties",
44
+ )
45
+
46
+
47
+ def _schema_anchor_target(root_schema: Any, anchor: str) -> Any:
48
+ matching_targets: list[Any] = []
49
+ pending_fragments = [root_schema]
50
+ while pending_fragments:
51
+ schema_fragment = pending_fragments.pop()
52
+ if isinstance(schema_fragment, Mapping):
53
+ if schema_fragment.get("$anchor") == anchor:
54
+ matching_targets.append(schema_fragment)
55
+ pending_fragments.extend(schema_fragment.values())
56
+ elif isinstance(schema_fragment, (list, tuple)):
57
+ pending_fragments.extend(schema_fragment)
58
+ if len(matching_targets) != 1:
59
+ raise ValueError(
60
+ f"local tool schema anchor {anchor!r} must resolve exactly once; "
61
+ f"resolved {len(matching_targets)} times"
62
+ )
63
+ return matching_targets[0]
64
+
65
+
66
+ def schema_reference_target(root_schema: Any, reference: str) -> Any:
67
+ """Resolve one decoded local JSON Pointer or anchor against its root schema."""
68
+
69
+ fragment = unquote(reference.removeprefix("#"))
70
+ if not fragment:
71
+ return root_schema
72
+ if not fragment.startswith("/"):
73
+ return _schema_anchor_target(root_schema, fragment)
74
+
75
+ referenced_schema = root_schema
76
+ for encoded_segment in fragment[1:].split("/"):
77
+ reference_segment = encoded_segment.replace("~1", "/").replace("~0", "~")
78
+ if isinstance(referenced_schema, Mapping) and reference_segment in referenced_schema:
79
+ referenced_schema = referenced_schema[reference_segment]
80
+ continue
81
+ if isinstance(referenced_schema, (list, tuple)) and reference_segment.isdigit():
82
+ reference_index = int(reference_segment)
83
+ if reference_index < len(referenced_schema):
84
+ referenced_schema = referenced_schema[reference_index]
85
+ continue
86
+ raise ValueError(f"local tool schema reference does not resolve: {reference!r}")
87
+ return referenced_schema
88
+
89
+
90
+ def validate_safe_local_schema_references(schema: Any, *, location: str) -> None:
91
+ """Reject reference features whose meaning can escape the embedded schema."""
92
+
93
+ reference_edges: dict[int, int] = {}
94
+ schema_nodes: dict[int, Any] = {}
95
+ pending_fragments: list[tuple[Any, bool]] = [(schema, True)]
96
+ while pending_fragments:
97
+ schema_fragment, is_root = pending_fragments.pop()
98
+ if isinstance(schema_fragment, Mapping):
99
+ fragment_identifier = id(schema_fragment)
100
+ schema_nodes[fragment_identifier] = schema_fragment
101
+ if any(
102
+ dynamic_keyword in schema_fragment
103
+ for dynamic_keyword in (
104
+ "$dynamicAnchor",
105
+ "$dynamicRef",
106
+ "$recursiveAnchor",
107
+ "$recursiveRef",
108
+ )
109
+ ):
110
+ raise ValueError(f"{location} does not support dynamic schema references")
111
+ if not is_root and "$id" in schema_fragment:
112
+ raise ValueError(f"{location} does not support nested $id resources")
113
+ if (reference := schema_fragment.get("$ref")) is not None:
114
+ if not isinstance(reference, str):
115
+ raise ValueError(f"{location} $ref values must be strings")
116
+ if not reference.startswith("#"):
117
+ raise ValueError(
118
+ f"{location} only supports local $ref values, got {reference!r}"
119
+ )
120
+ try:
121
+ reference_target = schema_reference_target(schema, reference)
122
+ except ValueError as reference_error:
123
+ raise ValueError(
124
+ f"{location} contains an unresolved $ref: {reference!r}"
125
+ ) from reference_error
126
+ if isinstance(reference_target, Mapping):
127
+ target_identifier = id(reference_target)
128
+ reference_edges[fragment_identifier] = target_identifier
129
+ schema_nodes[target_identifier] = reference_target
130
+ pending_fragments.extend(
131
+ (nested_schema, False) for nested_schema in schema_fragment.values()
132
+ )
133
+ elif isinstance(schema_fragment, (list, tuple)):
134
+ pending_fragments.extend((nested_schema, False) for nested_schema in schema_fragment)
135
+
136
+ visited_identifiers: set[int] = set()
137
+ for schema_identifier in schema_nodes:
138
+ path_identifiers: set[int] = set()
139
+ current_identifier: int | None = schema_identifier
140
+ while current_identifier is not None and current_identifier not in visited_identifiers:
141
+ if current_identifier in path_identifiers:
142
+ raise ValueError(f"{location} does not support cyclic local references")
143
+ path_identifiers.add(current_identifier)
144
+ current_identifier = reference_edges.get(current_identifier)
145
+ visited_identifiers.update(path_identifiers)
146
+
147
+
148
+ def _nested_schema_fragments(schema_fragment: Mapping[str, Any]) -> tuple[Any, ...]:
149
+ nested_fragments: list[Any] = []
150
+ nested_fragments.extend(
151
+ schema_fragment[keyword]
152
+ for keyword in _SINGLE_NESTED_SCHEMA_KEYWORDS
153
+ if keyword in schema_fragment
154
+ )
155
+ for keyword in _ARRAY_NESTED_SCHEMA_KEYWORDS:
156
+ keyword_value = schema_fragment.get(keyword)
157
+ if isinstance(keyword_value, (list, tuple)):
158
+ nested_fragments.extend(keyword_value)
159
+ for keyword in _MAPPING_NESTED_SCHEMA_KEYWORDS:
160
+ keyword_value = schema_fragment.get(keyword)
161
+ if isinstance(keyword_value, Mapping):
162
+ nested_fragments.extend(keyword_value.values())
163
+ return tuple(nested_fragments)
164
+
165
+
166
+ def validate_resume_token_schema(schema: dict[str, Any]) -> None:
167
+ """Validate the closed Draft 2020-12 contract accepted by external resume."""
168
+
169
+ location = "$.capabilities.await_external.resume.schema"
170
+ try:
171
+ json.dumps(schema, ensure_ascii=False, allow_nan=False)
172
+ declared_dialect = schema.get("$schema")
173
+ if not isinstance(declared_dialect, (str, type(None))):
174
+ raise ValueError("$schema must be a string")
175
+ if declared_dialect is not None and declared_dialect not in _DRAFT_2020_12_DIALECTS:
176
+ raise ValueError(f"must use JSON Schema Draft 2020-12, got {declared_dialect!r}")
177
+ Draft202012Validator.check_schema(schema)
178
+ validate_safe_local_schema_references(schema, location=location)
179
+ except (SchemaError, TypeError, ValueError) as schema_error:
180
+ detail = (
181
+ schema_error.message if isinstance(schema_error, SchemaError) else str(schema_error)
182
+ )
183
+ raise ValueError(f"{location} is not a valid closed local JSON Schema: {detail}") from (
184
+ schema_error
185
+ )
186
+ if schema.get("type") != "object":
187
+ raise ValueError(f"{location}.type must be 'object'")
188
+ if schema.get("additionalProperties") is not False:
189
+ raise ValueError(f"{location}.additionalProperties must be false")
190
+
191
+ pending_fragments: list[Any] = [schema]
192
+ while pending_fragments:
193
+ schema_fragment = pending_fragments.pop()
194
+ if not isinstance(schema_fragment, Mapping):
195
+ continue
196
+ declares_object_shape = schema_fragment.get("type") == "object" or isinstance(
197
+ schema_fragment.get("properties"), Mapping
198
+ )
199
+ if declares_object_shape and schema_fragment.get("additionalProperties") is not False:
200
+ raise ValueError(
201
+ f"{location} must close every declared object with additionalProperties:false"
202
+ )
203
+ pending_fragments.extend(_nested_schema_fragments(schema_fragment))