syntara-plugin-contracts 0.1.0a1__tar.gz
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.
- syntara_plugin_contracts-0.1.0a1/.gitignore +66 -0
- syntara_plugin_contracts-0.1.0a1/PKG-INFO +37 -0
- syntara_plugin_contracts-0.1.0a1/README.md +13 -0
- syntara_plugin_contracts-0.1.0a1/hatch_build.py +31 -0
- syntara_plugin_contracts-0.1.0a1/pyproject.toml +44 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/__init__.py +45 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/bundle.py +277 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/canonical.py +60 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/descriptor.py +154 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/action.invalid.json +17 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/broken-predecessor.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/duplicate-identity.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/expired.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/replayed.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/unsupported-contract.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/valid-20.json +36 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog/wrong-source.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog-fixture-bundle.json +26 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog-source-status.valid.json +10 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/catalog-source.valid.json +17 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/control.invalid.json +15 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/control.valid.json +37 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/credential-recipe.valid.json +12 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/installed-identity.valid.json +11 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/integration.valid.json +11 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/plugin-descriptor.valid.json +118 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/plugin.valid.json +15 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/runtime-result-failure.valid.json +11 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/runtime-result-reference.valid.json +9 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/runtime-result.valid.json +9 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/trigger-driver.valid.json +6 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/trigger-event-mapping.valid.json +11 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/trigger.invalid.json +14 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures/trigger.valid.json +32 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/fixtures.py +112 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/image.py +19 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/proto/syntara_plugin/runtime/protocol/container.proto +99 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/py.typed +1 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/action.schema.json +64 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/catalog-index.schema.json +57 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/catalog-source-status.schema.json +41 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/catalog-source.schema.json +29 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/common.schema.json +56 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/control.schema.json +24 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/credential-recipe.schema.json +30 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/http-operation.schema.json +59 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/http-runner-context.schema.json +30 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/installed-identity.schema.json +41 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/integration.schema.json +23 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/plugin-build-settings.schema.json +181 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/plugin-descriptor.schema.json +284 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/plugin.schema.json +57 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/provider.schema.json +20 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/runtime.schema.json +59 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/step.schema.json +64 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/trigger-driver.schema.json +22 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/trigger-event-mapping.schema.json +23 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/contracts/schemas/v1alpha1/trigger.schema.json +25 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/__init__.py +192 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/abi.py +95 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/adapter.py +287 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/bindings.py +688 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/container.py +714 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/context.py +36 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/contract_validation.py +2002 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/execution.py +97 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/http.py +234 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/manifest_validation.py +103 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/outcome.py +56 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/protocol/__init__.py +1 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/protocol/container_pb2.py +54 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/protocol/container_pb2_grpc.py +192 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/py.typed +1 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/server.py +334 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/step.py +169 -0
- syntara_plugin_contracts-0.1.0a1/src/syntara_plugin/runtime/validation.py +97 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Virtual environments
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
env/
|
|
30
|
+
.venv
|
|
31
|
+
|
|
32
|
+
# IDEs
|
|
33
|
+
.vscode/
|
|
34
|
+
.idea/
|
|
35
|
+
*.swp
|
|
36
|
+
*.swo
|
|
37
|
+
*~
|
|
38
|
+
.DS_Store
|
|
39
|
+
|
|
40
|
+
# Testing
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
.coverage
|
|
43
|
+
htmlcov/
|
|
44
|
+
.tox/
|
|
45
|
+
.nox/
|
|
46
|
+
|
|
47
|
+
# uv
|
|
48
|
+
.uv/
|
|
49
|
+
|
|
50
|
+
# Step SDK specific
|
|
51
|
+
*.compiled.json
|
|
52
|
+
step-definition.json
|
|
53
|
+
examples/*/step-definition.json
|
|
54
|
+
|
|
55
|
+
# Databases
|
|
56
|
+
*.db
|
|
57
|
+
*.sqlite
|
|
58
|
+
*.sqlite3
|
|
59
|
+
|
|
60
|
+
# Logs
|
|
61
|
+
*.log
|
|
62
|
+
|
|
63
|
+
# Environment variables
|
|
64
|
+
.env
|
|
65
|
+
.env.local
|
|
66
|
+
coverage.xml
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: syntara-plugin-contracts
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Versioned contracts and deterministic bundles for Syntara plugins
|
|
5
|
+
Project-URL: Homepage, https://github.com/syntara-orchestration/syntara-plugin-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/syntara-orchestration/syntara-plugin-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/syntara-orchestration/syntara-plugin-sdk/issues
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: grpcio>=1.78.0
|
|
19
|
+
Requires-Dist: jsonschema>=4.23
|
|
20
|
+
Requires-Dist: protobuf>=6.31.1
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Requires-Dist: referencing>=0.37
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Syntara Plugin Contracts
|
|
26
|
+
|
|
27
|
+
`syntara-plugin-contracts` distributes the versioned Syntara Plugin contract
|
|
28
|
+
bundle and runtime APIs. It contains the canonical JSON Schemas, conformance
|
|
29
|
+
fixtures, protocol definitions, and the Python APIs used by plugin workload
|
|
30
|
+
implementers and the SDK. It also installs the `syntara-plugin-runtime-server`
|
|
31
|
+
console script, which a final Python workload image uses to serve the container
|
|
32
|
+
ABI.
|
|
33
|
+
|
|
34
|
+
The source of truth is maintained in the
|
|
35
|
+
[Syntara Plugin SDK repository](https://github.com/syntara-orchestration/syntara-plugin-sdk).
|
|
36
|
+
Install `syntara-plugin-sdk` to author plugins. Depend on this package directly
|
|
37
|
+
when implementing another contract-aware tool or plugin workload runtime.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Syntara Plugin Contracts
|
|
2
|
+
|
|
3
|
+
`syntara-plugin-contracts` distributes the versioned Syntara Plugin contract
|
|
4
|
+
bundle and runtime APIs. It contains the canonical JSON Schemas, conformance
|
|
5
|
+
fixtures, protocol definitions, and the Python APIs used by plugin workload
|
|
6
|
+
implementers and the SDK. It also installs the `syntara-plugin-runtime-server`
|
|
7
|
+
console script, which a final Python workload image uses to serve the container
|
|
8
|
+
ABI.
|
|
9
|
+
|
|
10
|
+
The source of truth is maintained in the
|
|
11
|
+
[Syntara Plugin SDK repository](https://github.com/syntara-orchestration/syntara-plugin-sdk).
|
|
12
|
+
Install `syntara-plugin-sdk` to author plugins. Depend on this package directly
|
|
13
|
+
when implementing another contract-aware tool or plugin workload runtime.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Stage canonical SDK contracts in both wheels and source distributions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomBuildHook(BuildHookInterface):
|
|
12
|
+
"""Include the one canonical contract tree without a checked-in copy.
|
|
13
|
+
|
|
14
|
+
A source distribution must rebuild without a checkout of the whole SDK
|
|
15
|
+
workspace. During an sdist build this hook maps canonical repository files
|
|
16
|
+
into the Python package path. During a wheel build from that sdist those
|
|
17
|
+
staged files already exist in the package, so no external path is needed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
21
|
+
package_contracts = Path(self.root) / "src/syntara_plugin/contracts"
|
|
22
|
+
required = ("schemas", "fixtures", "proto")
|
|
23
|
+
if all((package_contracts / name).is_dir() for name in required):
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
canonical_contracts = Path(self.root).parents[3] / "contracts"
|
|
27
|
+
for name in required:
|
|
28
|
+
source = canonical_contracts / name
|
|
29
|
+
if not source.is_dir():
|
|
30
|
+
raise RuntimeError(f"canonical contract directory is missing: {source}")
|
|
31
|
+
build_data["force_include"][str(source)] = f"src/syntara_plugin/contracts/{name}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "syntara-plugin-contracts"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "Versioned contracts and deterministic bundles for Syntara plugins"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"grpcio>=1.78.0",
|
|
10
|
+
"jsonschema>=4.23",
|
|
11
|
+
"protobuf>=6.31.1",
|
|
12
|
+
"pydantic>=2.0",
|
|
13
|
+
"referencing>=0.37",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
syntara-plugin-runtime-server = "syntara_plugin.runtime.server:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/syntara-orchestration/syntara-plugin-sdk"
|
|
31
|
+
Repository = "https://github.com/syntara-orchestration/syntara-plugin-sdk"
|
|
32
|
+
Issues = "https://github.com/syntara-orchestration/syntara-plugin-sdk/issues"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
# Keep release builds reproducible; upgrades are reviewed like any other
|
|
36
|
+
# build-tool supply-chain change.
|
|
37
|
+
requires = ["hatchling==1.32.4"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/syntara_plugin"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.hooks.custom]
|
|
44
|
+
path = "hatch_build.py"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Normative, versioned Syntara plugin contracts."""
|
|
2
|
+
|
|
3
|
+
from .bundle import (
|
|
4
|
+
CatalogCheckpoint,
|
|
5
|
+
CatalogDiagnostic,
|
|
6
|
+
ContractBundle,
|
|
7
|
+
bundle_digest,
|
|
8
|
+
load_bundle,
|
|
9
|
+
validate_catalog_index,
|
|
10
|
+
validate_document,
|
|
11
|
+
)
|
|
12
|
+
from .canonical import (
|
|
13
|
+
CanonicalJsonError,
|
|
14
|
+
canonical_json_bytes,
|
|
15
|
+
canonical_json_digest,
|
|
16
|
+
container_workload_request_digest,
|
|
17
|
+
)
|
|
18
|
+
from .descriptor import (
|
|
19
|
+
materialized_schemas,
|
|
20
|
+
schema_digests,
|
|
21
|
+
validate_plugin_descriptor_semantics,
|
|
22
|
+
)
|
|
23
|
+
from .fixtures import FixtureBundle, fixture_content_digest, load_fixture_bundle
|
|
24
|
+
from .image import is_digest_image_reference
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CatalogCheckpoint",
|
|
28
|
+
"CatalogDiagnostic",
|
|
29
|
+
"CanonicalJsonError",
|
|
30
|
+
"ContractBundle",
|
|
31
|
+
"FixtureBundle",
|
|
32
|
+
"bundle_digest",
|
|
33
|
+
"canonical_json_bytes",
|
|
34
|
+
"canonical_json_digest",
|
|
35
|
+
"container_workload_request_digest",
|
|
36
|
+
"fixture_content_digest",
|
|
37
|
+
"load_bundle",
|
|
38
|
+
"load_fixture_bundle",
|
|
39
|
+
"materialized_schemas",
|
|
40
|
+
"schema_digests",
|
|
41
|
+
"validate_plugin_descriptor_semantics",
|
|
42
|
+
"is_digest_image_reference",
|
|
43
|
+
"validate_catalog_index",
|
|
44
|
+
"validate_document",
|
|
45
|
+
]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Load and validate the self-contained v1alpha1 contract bundle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
16
|
+
from referencing import Registry, Resource
|
|
17
|
+
|
|
18
|
+
ContractKind = Literal[
|
|
19
|
+
"plugin",
|
|
20
|
+
"plugin_build_settings",
|
|
21
|
+
"action",
|
|
22
|
+
"control",
|
|
23
|
+
"trigger",
|
|
24
|
+
"step",
|
|
25
|
+
"plugin_descriptor",
|
|
26
|
+
"runtime",
|
|
27
|
+
"provider",
|
|
28
|
+
"catalog_index",
|
|
29
|
+
"catalog_source",
|
|
30
|
+
"catalog_source_status",
|
|
31
|
+
"integration",
|
|
32
|
+
"http_mapping",
|
|
33
|
+
"http_runner_context",
|
|
34
|
+
"credential_recipe",
|
|
35
|
+
"trigger_driver",
|
|
36
|
+
"trigger_event_mapping",
|
|
37
|
+
"installed_identity",
|
|
38
|
+
]
|
|
39
|
+
_SCHEMA_FILES: dict[ContractKind, str] = {
|
|
40
|
+
"plugin": "plugin.schema.json",
|
|
41
|
+
"plugin_build_settings": "plugin-build-settings.schema.json",
|
|
42
|
+
"action": "action.schema.json",
|
|
43
|
+
"control": "control.schema.json",
|
|
44
|
+
"trigger": "trigger.schema.json",
|
|
45
|
+
"step": "step.schema.json",
|
|
46
|
+
"plugin_descriptor": "plugin-descriptor.schema.json",
|
|
47
|
+
"runtime": "runtime.schema.json",
|
|
48
|
+
"provider": "provider.schema.json",
|
|
49
|
+
"catalog_index": "catalog-index.schema.json",
|
|
50
|
+
"catalog_source": "catalog-source.schema.json",
|
|
51
|
+
"catalog_source_status": "catalog-source-status.schema.json",
|
|
52
|
+
"integration": "integration.schema.json",
|
|
53
|
+
"http_mapping": "http-operation.schema.json",
|
|
54
|
+
"http_runner_context": "http-runner-context.schema.json",
|
|
55
|
+
"credential_recipe": "credential-recipe.schema.json",
|
|
56
|
+
"trigger_driver": "trigger-driver.schema.json",
|
|
57
|
+
"trigger_event_mapping": "trigger-event-mapping.schema.json",
|
|
58
|
+
"installed_identity": "installed-identity.schema.json",
|
|
59
|
+
}
|
|
60
|
+
_RFC3339_DATETIME = re.compile(
|
|
61
|
+
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
|
|
62
|
+
)
|
|
63
|
+
_FORMAT_CHECKER = FormatChecker()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@_FORMAT_CHECKER.checks("date-time", raises=ValueError)
|
|
67
|
+
def _is_rfc3339_datetime(value: object) -> bool:
|
|
68
|
+
"""Accept the RFC 3339 subset used by Syntara runtime envelopes."""
|
|
69
|
+
|
|
70
|
+
if not isinstance(value, str) or not _RFC3339_DATETIME.fullmatch(value):
|
|
71
|
+
return False
|
|
72
|
+
datetime.fromisoformat(value.removesuffix("Z") + "+00:00" if value.endswith("Z") else value)
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class ContractBundle:
|
|
78
|
+
"""An immutable in-process view of the packaged contract documents."""
|
|
79
|
+
|
|
80
|
+
version: str
|
|
81
|
+
digest: str
|
|
82
|
+
documents: dict[str, dict[str, Any]]
|
|
83
|
+
|
|
84
|
+
def validator(self, kind: ContractKind) -> Draft202012Validator:
|
|
85
|
+
"""Return a validator with every local schema in this bundle registered."""
|
|
86
|
+
target = self.documents[_SCHEMA_FILES[kind]]
|
|
87
|
+
registry = Registry().with_resources(
|
|
88
|
+
(document["$id"], Resource.from_contents(document))
|
|
89
|
+
for document in self.documents.values()
|
|
90
|
+
)
|
|
91
|
+
return Draft202012Validator(
|
|
92
|
+
target,
|
|
93
|
+
registry=registry,
|
|
94
|
+
format_checker=_FORMAT_CHECKER,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _schema_directory() -> Any:
|
|
99
|
+
packaged = files("syntara_plugin.contracts").joinpath("schemas", "v1alpha1")
|
|
100
|
+
if packaged.is_dir():
|
|
101
|
+
return packaged
|
|
102
|
+
# Editable source installs consume the repository's language-neutral source.
|
|
103
|
+
for parent in Path(__file__).resolve().parents:
|
|
104
|
+
source = parent / "contracts" / "schemas" / "v1alpha1"
|
|
105
|
+
if source.is_dir():
|
|
106
|
+
return source
|
|
107
|
+
raise RuntimeError("cannot locate packaged or repository contract schemas")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _canonical_bytes(documents: dict[str, dict[str, Any]]) -> bytes:
|
|
111
|
+
payload = {path: documents[path] for path in sorted(documents)}
|
|
112
|
+
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode(
|
|
113
|
+
"utf-8"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def bundle_digest(documents: dict[str, dict[str, Any]]) -> str:
|
|
118
|
+
"""Return the stable SHA-256 digest of a complete contract bundle."""
|
|
119
|
+
return "sha256:" + hashlib.sha256(_canonical_bytes(documents)).hexdigest()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_bundle() -> ContractBundle:
|
|
123
|
+
"""Load the packaged, local-only `syntara.io/v1alpha1` contract bundle."""
|
|
124
|
+
directory = _schema_directory()
|
|
125
|
+
documents: dict[str, dict[str, Any]] = {}
|
|
126
|
+
for entry in sorted(directory.iterdir(), key=lambda item: item.name):
|
|
127
|
+
if entry.name.endswith(".schema.json"):
|
|
128
|
+
documents[entry.name] = json.loads(entry.read_text(encoding="utf-8"))
|
|
129
|
+
if set(_SCHEMA_FILES.values()) - set(documents):
|
|
130
|
+
raise RuntimeError("contract bundle is missing a required manifest or runtime schema")
|
|
131
|
+
return ContractBundle(
|
|
132
|
+
version="syntara.io/v1alpha1",
|
|
133
|
+
digest=bundle_digest(documents),
|
|
134
|
+
documents=documents,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_document(bundle: ContractBundle, kind: ContractKind, document: object) -> list[str]:
|
|
139
|
+
"""Return stable, path-prefixed JSON Schema validation errors."""
|
|
140
|
+
errors = sorted(
|
|
141
|
+
bundle.validator(kind).iter_errors(document), key=lambda error: list(error.path)
|
|
142
|
+
)
|
|
143
|
+
return [
|
|
144
|
+
f"{'/'.join(str(part) for part in error.path) or '<root>'}: {error.message}"
|
|
145
|
+
for error in errors
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class CatalogCheckpoint:
|
|
151
|
+
"""Last accepted immutable catalog state for one configured source.
|
|
152
|
+
|
|
153
|
+
The caller persists this state. The contracts package only compares a
|
|
154
|
+
candidate index to it; it does not fetch, verify signatures, or write data.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
generation: int = 0
|
|
158
|
+
index_digest: str | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, order=True)
|
|
162
|
+
class CatalogDiagnostic:
|
|
163
|
+
"""Stable, non-secret reason returned when a catalog candidate is rejected."""
|
|
164
|
+
|
|
165
|
+
code: str
|
|
166
|
+
path: str
|
|
167
|
+
message: str
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_catalog_index(
|
|
171
|
+
bundle: ContractBundle,
|
|
172
|
+
document: Mapping[str, Any],
|
|
173
|
+
*,
|
|
174
|
+
source_id: str,
|
|
175
|
+
now: datetime,
|
|
176
|
+
checkpoint: CatalogCheckpoint | None = None,
|
|
177
|
+
) -> tuple[CatalogDiagnostic, ...]:
|
|
178
|
+
"""Validate a catalog index against one source's replay checkpoint.
|
|
179
|
+
|
|
180
|
+
This is intentionally a pure control-plane contract check. OCI resolution,
|
|
181
|
+
signature verification, repository-scope checks, and persistence are owned
|
|
182
|
+
by the catalog service that calls it.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
checkpoint = checkpoint or CatalogCheckpoint()
|
|
186
|
+
schema_errors = validate_document(bundle, "catalog_index", document)
|
|
187
|
+
if schema_errors:
|
|
188
|
+
return tuple(
|
|
189
|
+
CatalogDiagnostic(
|
|
190
|
+
code="CATALOG_INDEX_SCHEMA_INVALID",
|
|
191
|
+
path=message.partition(":")[0],
|
|
192
|
+
message=message,
|
|
193
|
+
)
|
|
194
|
+
for message in schema_errors
|
|
195
|
+
)
|
|
196
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
197
|
+
raise ValueError("now must be timezone-aware")
|
|
198
|
+
|
|
199
|
+
metadata = document["metadata"]
|
|
200
|
+
specification = document["spec"]
|
|
201
|
+
diagnostics: list[CatalogDiagnostic] = []
|
|
202
|
+
if metadata["sourceId"] != source_id:
|
|
203
|
+
diagnostics.append(
|
|
204
|
+
CatalogDiagnostic(
|
|
205
|
+
code="CATALOG_INDEX_SOURCE_MISMATCH",
|
|
206
|
+
path="metadata/sourceId",
|
|
207
|
+
message="Catalog index sourceId does not match the configured source.",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
if specification["contractVersion"] != bundle.version:
|
|
211
|
+
diagnostics.append(
|
|
212
|
+
CatalogDiagnostic(
|
|
213
|
+
code="CATALOG_INDEX_CONTRACT_UNSUPPORTED",
|
|
214
|
+
path="spec/contractVersion",
|
|
215
|
+
message=f"Catalog index contract must be {bundle.version!r}.",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
issued_at = _parse_rfc3339(metadata["issuedAt"])
|
|
220
|
+
expires_at = _parse_rfc3339(metadata["expiresAt"])
|
|
221
|
+
if expires_at <= issued_at:
|
|
222
|
+
diagnostics.append(
|
|
223
|
+
CatalogDiagnostic(
|
|
224
|
+
code="CATALOG_INDEX_TIME_WINDOW_INVALID",
|
|
225
|
+
path="metadata/expiresAt",
|
|
226
|
+
message="Catalog index expiresAt must be later than issuedAt.",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if expires_at <= now:
|
|
230
|
+
diagnostics.append(
|
|
231
|
+
CatalogDiagnostic(
|
|
232
|
+
code="CATALOG_INDEX_EXPIRED",
|
|
233
|
+
path="metadata/expiresAt",
|
|
234
|
+
message="Catalog index is expired at the supplied reconciliation time.",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
if metadata["generation"] <= checkpoint.generation:
|
|
238
|
+
diagnostics.append(
|
|
239
|
+
CatalogDiagnostic(
|
|
240
|
+
code="CATALOG_INDEX_GENERATION_REPLAY",
|
|
241
|
+
path="metadata/generation",
|
|
242
|
+
message="Catalog index generation must be greater than the accepted generation.",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
if (
|
|
246
|
+
checkpoint.index_digest is not None
|
|
247
|
+
and metadata.get("previousIndexDigest") != checkpoint.index_digest
|
|
248
|
+
):
|
|
249
|
+
diagnostics.append(
|
|
250
|
+
CatalogDiagnostic(
|
|
251
|
+
code="CATALOG_INDEX_PREDECESSOR_MISMATCH",
|
|
252
|
+
path="metadata/previousIndexDigest",
|
|
253
|
+
message="Catalog index predecessor does not match the accepted index digest.",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
seen_identities: set[tuple[str, str, str]] = set()
|
|
258
|
+
for index, entry in enumerate(specification["entries"]):
|
|
259
|
+
identity = (entry["namespace"], entry["name"], entry["version"])
|
|
260
|
+
if identity in seen_identities:
|
|
261
|
+
diagnostics.append(
|
|
262
|
+
CatalogDiagnostic(
|
|
263
|
+
code="CATALOG_ENTRY_IDENTITY_DUPLICATE",
|
|
264
|
+
path=f"spec/entries/{index}",
|
|
265
|
+
message="Catalog index entries must not repeat namespace, name, and version.",
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
seen_identities.add(identity)
|
|
269
|
+
return tuple(sorted(diagnostics))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _parse_rfc3339(value: str) -> datetime:
|
|
273
|
+
"""Parse a value already checked by the bundle's strict date-time format."""
|
|
274
|
+
|
|
275
|
+
return datetime.fromisoformat(
|
|
276
|
+
value.removesuffix("Z") + "+00:00" if value.endswith("Z") else value
|
|
277
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Normative canonical JSON and generic-container request hashing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CanonicalJsonError(ValueError):
|
|
13
|
+
"""A value cannot be represented by the Syntara canonical JSON form."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def canonical_json_bytes(value: object) -> bytes:
|
|
17
|
+
"""Return the strict deterministic JSON form used by versioned contracts.
|
|
18
|
+
|
|
19
|
+
The format is UTF-8 JSON with sorted object keys, compact separators, and
|
|
20
|
+
no non-finite numbers. Other language SDKs must implement this exact form
|
|
21
|
+
before they calculate a compatible request hash.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
return json.dumps(
|
|
26
|
+
value,
|
|
27
|
+
allow_nan=False,
|
|
28
|
+
ensure_ascii=False,
|
|
29
|
+
separators=(",", ":"),
|
|
30
|
+
sort_keys=True,
|
|
31
|
+
).encode("utf-8")
|
|
32
|
+
except (TypeError, ValueError) as error:
|
|
33
|
+
raise CanonicalJsonError("value is not canonical JSON") from error
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def canonical_json_digest(value: object) -> str:
|
|
37
|
+
"""Return the SHA-256 digest of one canonical JSON value."""
|
|
38
|
+
|
|
39
|
+
return f"sha256:{hashlib.sha256(canonical_json_bytes(value)).hexdigest()}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def container_workload_request_digest(request: Mapping[str, Any]) -> str:
|
|
43
|
+
"""Hash a container workload request without its two mirrored hash fields.
|
|
44
|
+
|
|
45
|
+
The work-item and policy snapshot each carry the resulting digest. Removing
|
|
46
|
+
both fields before hashing avoids a circular representation while ensuring
|
|
47
|
+
every other execution, policy, and runtime value is committed.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
canonical_request = deepcopy(dict(request))
|
|
52
|
+
for section_name in ("workItem", "policySnapshot"):
|
|
53
|
+
section = canonical_request.get(section_name)
|
|
54
|
+
if isinstance(section, Mapping):
|
|
55
|
+
section_copy = dict(section)
|
|
56
|
+
section_copy.pop("requestHash", None)
|
|
57
|
+
canonical_request[section_name] = section_copy
|
|
58
|
+
except (TypeError, ValueError) as error:
|
|
59
|
+
raise CanonicalJsonError("container workload request is not canonical JSON") from error
|
|
60
|
+
return canonical_json_digest(canonical_request)
|