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.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|
flowspec2/compat/yaml.py
ADDED
|
@@ -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
|
+
)
|