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
@@ -0,0 +1,149 @@
1
+ """Synthetic tool contracts used only while compatibility artifacts are checked."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any, cast
7
+
8
+ from flowspec2.subflows import default_subflows
9
+ from flowspec2.tools import ToolDefinition, ToolEffects
10
+
11
+ _OPEN_OBJECT_SCHEMA = {"type": "object", "additionalProperties": True}
12
+ _TERMINAL_STATUSES = ("success", "retryable", "fatal")
13
+
14
+
15
+ def _enrichment_tool_name(flow_document: Mapping[str, Any]) -> object:
16
+ capabilities = flow_document.get("capabilities")
17
+ await_external = (
18
+ capabilities.get("await_external") if isinstance(capabilities, Mapping) else None
19
+ )
20
+ on_resume = await_external.get("on_resume") if isinstance(await_external, Mapping) else None
21
+ enrichment = on_resume.get("enrich") if isinstance(on_resume, Mapping) else None
22
+ return enrichment.get("tool") if isinstance(enrichment, Mapping) else enrichment
23
+
24
+
25
+ def compatibility_tool_names(flow_document: Mapping[str, Any]) -> tuple[str, ...]:
26
+ """Return direct and transitively required tools for compile-only checking."""
27
+
28
+ tool_names: set[str] = set()
29
+ for section_name in ("entry", "terminal"):
30
+ section = flow_document.get(section_name)
31
+ if isinstance(section, Mapping) and isinstance(section.get("tool"), str):
32
+ tool_names.add(section["tool"])
33
+ enrichment_tool_name = _enrichment_tool_name(flow_document)
34
+ if isinstance(enrichment_tool_name, str):
35
+ tool_names.add(enrichment_tool_name)
36
+
37
+ uses = flow_document.get("uses")
38
+ subflow_registry = default_subflows()
39
+ if isinstance(uses, list):
40
+ for use_contract in uses:
41
+ if not isinstance(use_contract, Mapping):
42
+ continue
43
+ subflow_reference = use_contract.get("ref")
44
+ if not isinstance(subflow_reference, str) or not subflow_registry.has(
45
+ subflow_reference
46
+ ):
47
+ continue
48
+ tool_names.update(subflow_registry.definition(subflow_reference).required_tools)
49
+ return tuple(sorted(tool_names))
50
+
51
+
52
+ def _add_required_output_path(
53
+ output_schema: dict[str, Any],
54
+ result_path: str,
55
+ ) -> None:
56
+ if not result_path.startswith("result."):
57
+ raise ValueError(f"invalid terminal result path in compatibility profile: {result_path!r}")
58
+ path_segments = result_path.removeprefix("result.").split(".")
59
+ if any(not path_segment for path_segment in path_segments):
60
+ raise ValueError(f"invalid terminal result path in compatibility profile: {result_path!r}")
61
+
62
+ current_schema = output_schema
63
+ for path_index, path_segment in enumerate(path_segments):
64
+ required_properties = cast(list[str], current_schema.setdefault("required", []))
65
+ if path_segment not in required_properties:
66
+ required_properties.append(path_segment)
67
+ property_schemas = cast(
68
+ dict[str, Any],
69
+ current_schema.setdefault("properties", {}),
70
+ )
71
+ is_leaf = path_index == len(path_segments) - 1
72
+ if is_leaf:
73
+ property_schemas.setdefault(path_segment, {})
74
+ continue
75
+ nested_schema = property_schemas.get(path_segment)
76
+ if nested_schema == {}:
77
+ nested_schema = None
78
+ if nested_schema is None:
79
+ nested_schema = {
80
+ "type": "object",
81
+ "additionalProperties": True,
82
+ "properties": {},
83
+ }
84
+ property_schemas[path_segment] = nested_schema
85
+ if not isinstance(nested_schema, dict) or nested_schema.get("type") != "object":
86
+ raise ValueError(
87
+ f"terminal result paths conflict in compatibility profile: {result_path!r}"
88
+ )
89
+ current_schema = nested_schema
90
+
91
+
92
+ def _terminal_output_schema(
93
+ terminal_contract: Mapping[str, Any],
94
+ ) -> dict[str, Any]:
95
+ output_schema: dict[str, Any] = {
96
+ "type": "object",
97
+ "additionalProperties": True,
98
+ "properties": {
99
+ "status": {
100
+ "type": "string",
101
+ "enum": list(_TERMINAL_STATUSES),
102
+ }
103
+ },
104
+ }
105
+ output_bindings = terminal_contract.get("outputs")
106
+ if isinstance(output_bindings, Mapping):
107
+ for result_path in output_bindings.values():
108
+ if isinstance(result_path, str):
109
+ _add_required_output_path(output_schema, result_path)
110
+ return output_schema
111
+
112
+
113
+ def synthetic_compatibility_tool_definition(
114
+ tool_name: str,
115
+ flow_document: Mapping[str, Any],
116
+ *,
117
+ version: str,
118
+ description: str,
119
+ ) -> ToolDefinition:
120
+ """Derive the minimum truthful compile-only contract from authored roles."""
121
+
122
+ entry_contract = flow_document.get("entry")
123
+ entry_tool_name = entry_contract.get("tool") if isinstance(entry_contract, Mapping) else None
124
+ terminal_contract = flow_document.get("terminal")
125
+ terminal_tool_name = (
126
+ terminal_contract.get("tool") if isinstance(terminal_contract, Mapping) else None
127
+ )
128
+ read_only = tool_name in {entry_tool_name, _enrichment_tool_name(flow_document)}
129
+ terminal_idempotent = (
130
+ bool(terminal_contract.get("idempotent", False))
131
+ if isinstance(terminal_contract, Mapping) and terminal_tool_name == tool_name
132
+ else False
133
+ )
134
+ return ToolDefinition(
135
+ name=tool_name,
136
+ version=version,
137
+ description=description,
138
+ input_schema=_OPEN_OBJECT_SCHEMA,
139
+ output_schema=(
140
+ _terminal_output_schema(terminal_contract)
141
+ if isinstance(terminal_contract, Mapping) and terminal_tool_name == tool_name
142
+ else _OPEN_OBJECT_SCHEMA
143
+ ),
144
+ effects=ToolEffects(
145
+ read_only=read_only,
146
+ idempotent=read_only or terminal_idempotent,
147
+ open_world=True,
148
+ ),
149
+ )
@@ -0,0 +1,147 @@
1
+ """Safe YAML I/O shared by compatibility profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any, Mapping
8
+
9
+ import yaml
10
+ from yaml.nodes import MappingNode, ScalarNode
11
+
12
+
13
+ class YamlDocumentError(ValueError):
14
+ """Raised when a compatibility YAML document is unsafe or malformed."""
15
+
16
+
17
+ class DuplicateYamlKeyError(YamlDocumentError):
18
+ """Raised when a YAML mapping repeats a key."""
19
+
20
+
21
+ class _UniqueKeySafeLoader(yaml.SafeLoader):
22
+ """SafeLoader variant that rejects PyYAML's last-key-wins behavior."""
23
+
24
+
25
+ _YAML_BOOLEAN_TAG = "tag:yaml.org,2002:bool"
26
+ _YAML_FLOAT_TAG = "tag:yaml.org,2002:float"
27
+ _YAML_INTEGER_TAG = "tag:yaml.org,2002:int"
28
+ _YAML_TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp"
29
+ _YAML_1_1_SCALAR_TAGS = {
30
+ _YAML_BOOLEAN_TAG,
31
+ _YAML_FLOAT_TAG,
32
+ _YAML_INTEGER_TAG,
33
+ _YAML_TIMESTAMP_TAG,
34
+ }
35
+ _UniqueKeySafeLoader.yaml_implicit_resolvers = {
36
+ initial_character: [
37
+ resolver for resolver in resolvers if resolver[0] not in _YAML_1_1_SCALAR_TAGS
38
+ ]
39
+ for initial_character, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items()
40
+ }
41
+ _UniqueKeySafeLoader.add_implicit_resolver( # type: ignore[no-untyped-call]
42
+ _YAML_BOOLEAN_TAG,
43
+ re.compile(r"^(?:true|True|TRUE|false|False|FALSE)$"),
44
+ list("tTfF"),
45
+ )
46
+ _UniqueKeySafeLoader.add_implicit_resolver( # type: ignore[no-untyped-call]
47
+ _YAML_INTEGER_TAG,
48
+ re.compile(r"^[-+]?(?:0o[0-7]+|0x[0-9a-fA-F]+|[0-9]+)$"),
49
+ list("-+0123456789"),
50
+ )
51
+ _UniqueKeySafeLoader.add_implicit_resolver( # type: ignore[no-untyped-call]
52
+ _YAML_FLOAT_TAG,
53
+ re.compile(
54
+ r"^(?:"
55
+ r"[-+]?(?:(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][-+]?[0-9]+))"
56
+ r"|[-+]?(?:[0-9]+\.[0-9]*|\.[0-9]+)"
57
+ r"|[-+]?\.(?:inf|Inf|INF)"
58
+ r"|\.(?:nan|NaN|NAN)"
59
+ r")$"
60
+ ),
61
+ list("-+0123456789."),
62
+ )
63
+
64
+
65
+ def _construct_yaml_1_2_integer(
66
+ loader: _UniqueKeySafeLoader,
67
+ node: ScalarNode,
68
+ ) -> int:
69
+ scalar = loader.construct_scalar(node)
70
+ sign = -1 if scalar.startswith("-") else 1
71
+ unsigned_scalar = scalar[1:] if scalar[:1] in {"-", "+"} else scalar
72
+ if unsigned_scalar.startswith("0o"):
73
+ return sign * int(unsigned_scalar[2:], 8)
74
+ if unsigned_scalar.startswith("0x"):
75
+ return sign * int(unsigned_scalar[2:], 16)
76
+ return sign * int(unsigned_scalar, 10)
77
+
78
+
79
+ _UniqueKeySafeLoader.add_constructor(
80
+ _YAML_INTEGER_TAG,
81
+ _construct_yaml_1_2_integer,
82
+ )
83
+
84
+
85
+ def _construct_unique_mapping(
86
+ loader: _UniqueKeySafeLoader,
87
+ node: MappingNode,
88
+ deep: bool = False,
89
+ ) -> dict[Any, Any]:
90
+ loader.flatten_mapping(node)
91
+ mapping: dict[Any, Any] = {}
92
+ for key_node, value_node in node.value:
93
+ key = loader.construct_object(key_node, deep=deep)
94
+ try:
95
+ duplicate = key in mapping
96
+ except TypeError as exc:
97
+ raise YamlDocumentError(
98
+ f"unhashable YAML mapping key at line {key_node.start_mark.line + 1}"
99
+ ) from exc
100
+ if duplicate:
101
+ raise DuplicateYamlKeyError(
102
+ f"duplicate YAML key {key!r} at line {key_node.start_mark.line + 1}"
103
+ )
104
+ mapping[key] = loader.construct_object(value_node, deep=deep)
105
+ return mapping
106
+
107
+
108
+ _UniqueKeySafeLoader.add_constructor(
109
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
110
+ _construct_unique_mapping,
111
+ )
112
+
113
+
114
+ def loads_yaml_mapping(text: str, *, source: str = "<string>") -> dict[str, Any]:
115
+ """Parse one YAML mapping without object construction or duplicate keys."""
116
+
117
+ try:
118
+ document = yaml.load(text, Loader=_UniqueKeySafeLoader)
119
+ except YamlDocumentError:
120
+ raise
121
+ except yaml.YAMLError as exc:
122
+ raise YamlDocumentError(f"invalid YAML document {source}: {exc}") from exc
123
+ if not isinstance(document, dict):
124
+ raise YamlDocumentError(f"YAML document {source} must be a non-empty mapping")
125
+ if not document:
126
+ raise YamlDocumentError(f"YAML document {source} must be a non-empty mapping")
127
+ return document
128
+
129
+
130
+ def load_yaml_mapping(path: str | Path) -> dict[str, Any]:
131
+ """Read one UTF-8 YAML mapping from disk."""
132
+
133
+ document_path = Path(path)
134
+ return loads_yaml_mapping(
135
+ document_path.read_text(encoding="utf-8"),
136
+ source=str(document_path),
137
+ )
138
+
139
+
140
+ def dumps_yaml_mapping(document: Mapping[str, Any]) -> str:
141
+ """Serialize a mapping as stable, Unicode-preserving safe YAML."""
142
+
143
+ return yaml.safe_dump(
144
+ dict(document),
145
+ allow_unicode=True,
146
+ sort_keys=False,
147
+ )