bhanuj-governance-plugin-api 1.0.0__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.
- bhanuj_governance_plugin_api-1.0.0/.gitignore +44 -0
- bhanuj_governance_plugin_api-1.0.0/PKG-INFO +46 -0
- bhanuj_governance_plugin_api-1.0.0/README.md +29 -0
- bhanuj_governance_plugin_api-1.0.0/pyproject.toml +27 -0
- bhanuj_governance_plugin_api-1.0.0/src/bhanuj_governance_plugin_api/__init__.py +43 -0
- bhanuj_governance_plugin_api-1.0.0/src/bhanuj_governance_plugin_api/contracts.py +35 -0
- bhanuj_governance_plugin_api-1.0.0/src/bhanuj_governance_plugin_api/metadata.py +57 -0
- bhanuj_governance_plugin_api-1.0.0/src/bhanuj_governance_plugin_api/plugin.py +39 -0
- bhanuj_governance_plugin_api-1.0.0/src/bhanuj_governance_plugin_api/replay.py +204 -0
- bhanuj_governance_plugin_api-1.0.0/tests/test_public_contract.py +80 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
|
|
5
|
+
# Virtual Environment
|
|
6
|
+
.venv/
|
|
7
|
+
|
|
8
|
+
# Packaging
|
|
9
|
+
*.egg-info/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
# `build/` ignores generated package output. Keep launcher scripts trackable.
|
|
13
|
+
!scripts/build/
|
|
14
|
+
!scripts/build/*.sh
|
|
15
|
+
.DS_Store
|
|
16
|
+
.pnpm-store/
|
|
17
|
+
|
|
18
|
+
# Testing
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/
|
|
24
|
+
|
|
25
|
+
default.sqlite
|
|
26
|
+
|
|
27
|
+
# Env
|
|
28
|
+
.env.*
|
|
29
|
+
*.env
|
|
30
|
+
!**/.env.example
|
|
31
|
+
!.env.local.example
|
|
32
|
+
!.env.platform.example
|
|
33
|
+
!keycloak-postgres/.env.keycloak.example
|
|
34
|
+
|
|
35
|
+
# Database
|
|
36
|
+
*.sqlite
|
|
37
|
+
*.db
|
|
38
|
+
|
|
39
|
+
# walkthrough
|
|
40
|
+
.ai-governance/walkthroughs/*
|
|
41
|
+
.governance/
|
|
42
|
+
|
|
43
|
+
# misc
|
|
44
|
+
logs/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bhanuj-governance-plugin-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Public, provider-neutral plugin contracts for BHANUJ AI Governance.
|
|
5
|
+
Project-URL: Homepage, https://governance.bhanuj.ai
|
|
6
|
+
Project-URL: Documentation, https://governance.bhanuj.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Bhanuj-AI/ai-governance-oss
|
|
8
|
+
Author: BHANUJ AI
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# BHANUJ Governance Plugin API
|
|
19
|
+
|
|
20
|
+
`bhanuj-governance-plugin-api` is the small, versioned extension contract for
|
|
21
|
+
external BHANUJ runtime plugins. It is not the BHANUJ application SDK: it does
|
|
22
|
+
not contain a control plane, persistence, workers, settings, RBAC, or provider
|
|
23
|
+
implementations.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
[project]
|
|
29
|
+
dependencies = ["bhanuj-governance-plugin-api>=1.0,<2"]
|
|
30
|
+
|
|
31
|
+
[project.entry-points."bhanuj.governance.plugins"]
|
|
32
|
+
my-runtime = "my_runtime.plugin:MyRuntimePlugin"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Boundary
|
|
36
|
+
|
|
37
|
+
The package supplies lifecycle metadata, the replay-adapter contribution,
|
|
38
|
+
bounded replay context, and the governed intervention envelope. The host owns
|
|
39
|
+
replay persistence, the canonical workflow execution record, and policy
|
|
40
|
+
evaluation. Plugins return `ReplayExecutionResult`; the host materialises its
|
|
41
|
+
own execution record.
|
|
42
|
+
|
|
43
|
+
The package has no dependency on `ai-governance` and must never import Core.
|
|
44
|
+
`SPI_VERSION` is currently `"1"`. Plugins declare the major version they
|
|
45
|
+
implement with `PluginMetadata(spi_version="1")`; an unsupported major fails
|
|
46
|
+
host startup explicitly.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# BHANUJ Governance Plugin API
|
|
2
|
+
|
|
3
|
+
`bhanuj-governance-plugin-api` is the small, versioned extension contract for
|
|
4
|
+
external BHANUJ runtime plugins. It is not the BHANUJ application SDK: it does
|
|
5
|
+
not contain a control plane, persistence, workers, settings, RBAC, or provider
|
|
6
|
+
implementations.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```toml
|
|
11
|
+
[project]
|
|
12
|
+
dependencies = ["bhanuj-governance-plugin-api>=1.0,<2"]
|
|
13
|
+
|
|
14
|
+
[project.entry-points."bhanuj.governance.plugins"]
|
|
15
|
+
my-runtime = "my_runtime.plugin:MyRuntimePlugin"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Boundary
|
|
19
|
+
|
|
20
|
+
The package supplies lifecycle metadata, the replay-adapter contribution,
|
|
21
|
+
bounded replay context, and the governed intervention envelope. The host owns
|
|
22
|
+
replay persistence, the canonical workflow execution record, and policy
|
|
23
|
+
evaluation. Plugins return `ReplayExecutionResult`; the host materialises its
|
|
24
|
+
own execution record.
|
|
25
|
+
|
|
26
|
+
The package has no dependency on `ai-governance` and must never import Core.
|
|
27
|
+
`SPI_VERSION` is currently `"1"`. Plugins declare the major version they
|
|
28
|
+
implement with `PluginMetadata(spi_version="1")`; an unsupported major fails
|
|
29
|
+
host startup explicitly.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bhanuj-governance-plugin-api"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Public, provider-neutral plugin contracts for BHANUJ AI Governance."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
authors = [{ name = "BHANUJ AI" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"License :: OSI Approved :: Apache Software License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Typing :: Typed",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://governance.bhanuj.ai"
|
|
19
|
+
Documentation = "https://governance.bhanuj.ai/docs"
|
|
20
|
+
Repository = "https://github.com/Bhanuj-AI/ai-governance-oss"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling>=1.25"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/bhanuj_governance_plugin_api"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Public, dependency-free contracts for BHANUJ runtime plugins."""
|
|
2
|
+
|
|
3
|
+
from bhanuj_governance_plugin_api.contracts import ReplayExecutionAdapterContribution
|
|
4
|
+
from bhanuj_governance_plugin_api.metadata import (
|
|
5
|
+
CANONICAL_PLUGIN_ENTRY_POINT_GROUP,
|
|
6
|
+
LEGACY_CONTRACT_VERSION,
|
|
7
|
+
LEGACY_PLUGIN_ENTRY_POINT_GROUP,
|
|
8
|
+
SPI_VERSION,
|
|
9
|
+
PluginMetadata,
|
|
10
|
+
PluginStatus,
|
|
11
|
+
)
|
|
12
|
+
from bhanuj_governance_plugin_api.plugin import (
|
|
13
|
+
AIGovernancePlugin,
|
|
14
|
+
PluginContext,
|
|
15
|
+
ReplayAdapterContributionRegistry,
|
|
16
|
+
)
|
|
17
|
+
from bhanuj_governance_plugin_api.replay import (
|
|
18
|
+
REPLAY_INTERVENTION_ENVELOPE_SCHEMA_VERSION,
|
|
19
|
+
ReplayCancellationToken,
|
|
20
|
+
ReplayExecutionAdapter,
|
|
21
|
+
ReplayExecutionContext,
|
|
22
|
+
ReplayExecutionResult,
|
|
23
|
+
ReplayInterventionEnvelope,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CANONICAL_PLUGIN_ENTRY_POINT_GROUP",
|
|
28
|
+
"LEGACY_CONTRACT_VERSION",
|
|
29
|
+
"LEGACY_PLUGIN_ENTRY_POINT_GROUP",
|
|
30
|
+
"REPLAY_INTERVENTION_ENVELOPE_SCHEMA_VERSION",
|
|
31
|
+
"SPI_VERSION",
|
|
32
|
+
"AIGovernancePlugin",
|
|
33
|
+
"PluginContext",
|
|
34
|
+
"PluginMetadata",
|
|
35
|
+
"PluginStatus",
|
|
36
|
+
"ReplayAdapterContributionRegistry",
|
|
37
|
+
"ReplayCancellationToken",
|
|
38
|
+
"ReplayExecutionAdapter",
|
|
39
|
+
"ReplayExecutionAdapterContribution",
|
|
40
|
+
"ReplayExecutionContext",
|
|
41
|
+
"ReplayExecutionResult",
|
|
42
|
+
"ReplayInterventionEnvelope",
|
|
43
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Public plugin contributions with no BHANUJ Core imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from bhanuj_governance_plugin_api.replay import ReplayExecutionAdapter
|
|
9
|
+
|
|
10
|
+
_REPLAY_ADAPTER_COMPONENT = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ReplayExecutionAdapterContribution:
|
|
15
|
+
"""One versioned external adapter contributed by a runtime plugin."""
|
|
16
|
+
|
|
17
|
+
adapter_id: str
|
|
18
|
+
adapter_version: str
|
|
19
|
+
adapter: ReplayExecutionAdapter
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if not _REPLAY_ADAPTER_COMPONENT.fullmatch(
|
|
23
|
+
self.adapter_id
|
|
24
|
+
) or not _REPLAY_ADAPTER_COMPONENT.fullmatch(self.adapter_version):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"Replay adapter contribution requires lowercase, hyphen-delimited adapter ID and version."
|
|
27
|
+
)
|
|
28
|
+
if self.adapter.name != self.name:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"Replay adapter contribution identity must match adapter.name."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return f"{self.adapter_id}/{self.adapter_version}"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Versioned identity and compatibility declarations for runtime plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
SPI_VERSION = "1"
|
|
9
|
+
"""The supported major version of the independently released Plugin API."""
|
|
10
|
+
|
|
11
|
+
LEGACY_CONTRACT_VERSION = "v1"
|
|
12
|
+
"""Compatibility spelling retained for diagnostics emitted by existing hosts."""
|
|
13
|
+
|
|
14
|
+
CANONICAL_PLUGIN_ENTRY_POINT_GROUP = "bhanuj.governance.plugins"
|
|
15
|
+
"""The Python packaging entry-point group for new BHANUJ plugins."""
|
|
16
|
+
|
|
17
|
+
LEGACY_PLUGIN_ENTRY_POINT_GROUP = "ai_governance.plugins"
|
|
18
|
+
"""Temporarily supported entry-point group for pre-Plugin-API extensions."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PluginStatus(StrEnum):
|
|
22
|
+
"""Lifecycle state reported by a BHANUJ plugin host."""
|
|
23
|
+
|
|
24
|
+
DISCOVERED = "discovered"
|
|
25
|
+
REGISTERED = "registered"
|
|
26
|
+
ACTIVE = "active"
|
|
27
|
+
DISABLED = "disabled"
|
|
28
|
+
FAILED = "failed"
|
|
29
|
+
INCOMPATIBLE = "incompatible"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class PluginMetadata:
|
|
34
|
+
"""Immutable identity, host compatibility, and Plugin API compatibility."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
version: str
|
|
38
|
+
required_ai_governance_version: str
|
|
39
|
+
capabilities: tuple[str, ...] = ()
|
|
40
|
+
spi_version: str = SPI_VERSION
|
|
41
|
+
contract_version: str = LEGACY_CONTRACT_VERSION
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if not self.name.strip() or not self.version.strip():
|
|
45
|
+
raise ValueError("Plugin metadata requires a name and version.")
|
|
46
|
+
if not self.required_ai_governance_version.strip():
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"Plugin metadata requires an AI Governance Control Plane version range."
|
|
49
|
+
)
|
|
50
|
+
if not self.spi_version.isdigit() or int(self.spi_version) < 1:
|
|
51
|
+
raise ValueError("Plugin metadata spi_version must be a positive major.")
|
|
52
|
+
if self.contract_version != f"v{self.spi_version}":
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Plugin metadata contract_version must match its SPI major version."
|
|
55
|
+
)
|
|
56
|
+
if len(set(self.capabilities)) != len(self.capabilities):
|
|
57
|
+
raise ValueError("Plugin capabilities must be unique.")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Structural lifecycle contracts implemented by external runtime plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from bhanuj_governance_plugin_api.contracts import ReplayExecutionAdapterContribution
|
|
9
|
+
from bhanuj_governance_plugin_api.metadata import PluginMetadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReplayAdapterContributionRegistry(Protocol):
|
|
13
|
+
"""The bounded host registration surface needed by runtime plugins."""
|
|
14
|
+
|
|
15
|
+
def replay_execution_adapters(
|
|
16
|
+
self, items: Iterable[ReplayExecutionAdapterContribution]
|
|
17
|
+
) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PluginContext(Protocol):
|
|
21
|
+
"""Plugin-bound context exposed by a host during lifecycle callbacks."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def contributions(self) -> ReplayAdapterContributionRegistry: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AIGovernancePlugin(Protocol):
|
|
28
|
+
"""Stable lifecycle protocol for an independently installed plugin."""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def metadata(self) -> PluginMetadata: ...
|
|
32
|
+
|
|
33
|
+
def validate(self, context: PluginContext) -> None: ...
|
|
34
|
+
|
|
35
|
+
def register(self, context: PluginContext) -> None: ...
|
|
36
|
+
|
|
37
|
+
def start(self, context: PluginContext) -> None: ...
|
|
38
|
+
|
|
39
|
+
def stop(self, context: PluginContext) -> None: ...
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Provider-neutral, governed replay contracts for external runtime plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
REPLAY_INTERVENTION_ENVELOPE_SCHEMA_VERSION = "replay-intervention-envelope/v1"
|
|
12
|
+
_STRATEGIES = frozenset({"NULLIFY", "REPLACE", "PERTURB"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReplayCancellationToken(Protocol):
|
|
16
|
+
"""Cooperative cancellation visible to an adapter at safe boundaries."""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def is_cancelled(self) -> bool: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ReplayInterventionEnvelope:
|
|
24
|
+
"""The fixed, reference-only policy authorization for a controlled replay."""
|
|
25
|
+
|
|
26
|
+
policy_id: str
|
|
27
|
+
policy_version: int
|
|
28
|
+
external_execution_id: str
|
|
29
|
+
runtime_tool_call_id: str
|
|
30
|
+
intervention_provider: str
|
|
31
|
+
intervention_provider_version: str
|
|
32
|
+
strategy: str
|
|
33
|
+
original_evidence_digest: str
|
|
34
|
+
counterfactual_reference: str
|
|
35
|
+
counterfactual_digest: str
|
|
36
|
+
intervention_digest: str
|
|
37
|
+
schema_version: str = REPLAY_INTERVENTION_ENVELOPE_SCHEMA_VERSION
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
for name, value in (
|
|
41
|
+
("policy_id", self.policy_id),
|
|
42
|
+
("external_execution_id", self.external_execution_id),
|
|
43
|
+
("runtime_tool_call_id", self.runtime_tool_call_id),
|
|
44
|
+
("intervention_provider", self.intervention_provider),
|
|
45
|
+
("intervention_provider_version", self.intervention_provider_version),
|
|
46
|
+
("original_evidence_digest", self.original_evidence_digest),
|
|
47
|
+
("counterfactual_reference", self.counterfactual_reference),
|
|
48
|
+
("counterfactual_digest", self.counterfactual_digest),
|
|
49
|
+
("intervention_digest", self.intervention_digest),
|
|
50
|
+
):
|
|
51
|
+
if not isinstance(value, str) or not value.strip():
|
|
52
|
+
raise ValueError(f"Replay intervention envelope {name} is required.")
|
|
53
|
+
if (
|
|
54
|
+
not isinstance(self.policy_version, int)
|
|
55
|
+
or isinstance(self.policy_version, bool)
|
|
56
|
+
or self.policy_version < 1
|
|
57
|
+
):
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"Replay intervention envelope policy_version must be positive."
|
|
60
|
+
)
|
|
61
|
+
if self.strategy not in _STRATEGIES:
|
|
62
|
+
raise ValueError("Replay intervention envelope strategy is unsupported.")
|
|
63
|
+
if self.schema_version != REPLAY_INTERVENTION_ENVELOPE_SCHEMA_VERSION:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Unsupported Replay intervention envelope schema version: {self.schema_version!r}."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def to_payload(self) -> dict[str, object]:
|
|
69
|
+
"""Serialise precisely the v1 reference-only wire shape."""
|
|
70
|
+
return {
|
|
71
|
+
"schema_version": self.schema_version,
|
|
72
|
+
"policy_id": self.policy_id,
|
|
73
|
+
"policy_version": self.policy_version,
|
|
74
|
+
"external_execution_id": self.external_execution_id,
|
|
75
|
+
"runtime_tool_call_id": self.runtime_tool_call_id,
|
|
76
|
+
"intervention_provider": self.intervention_provider,
|
|
77
|
+
"intervention_provider_version": self.intervention_provider_version,
|
|
78
|
+
"strategy": self.strategy,
|
|
79
|
+
"original_evidence_digest": self.original_evidence_digest,
|
|
80
|
+
"counterfactual_reference": self.counterfactual_reference,
|
|
81
|
+
"counterfactual_digest": self.counterfactual_digest,
|
|
82
|
+
"intervention_digest": self.intervention_digest,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_payload(cls, payload: Mapping[str, object]) -> ReplayInterventionEnvelope:
|
|
87
|
+
"""Parse only a complete v1 envelope and reject unknown fields."""
|
|
88
|
+
expected = set(cls.__dataclass_fields__)
|
|
89
|
+
if set(payload) != expected:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"Replay intervention envelope payload has an unsupported shape."
|
|
92
|
+
)
|
|
93
|
+
return cls(
|
|
94
|
+
policy_id=_required_string(payload, "policy_id"),
|
|
95
|
+
policy_version=_required_version(payload),
|
|
96
|
+
external_execution_id=_required_string(payload, "external_execution_id"),
|
|
97
|
+
runtime_tool_call_id=_required_string(payload, "runtime_tool_call_id"),
|
|
98
|
+
intervention_provider=_required_string(payload, "intervention_provider"),
|
|
99
|
+
intervention_provider_version=_required_string(
|
|
100
|
+
payload, "intervention_provider_version"
|
|
101
|
+
),
|
|
102
|
+
strategy=_required_string(payload, "strategy"),
|
|
103
|
+
original_evidence_digest=_required_string(
|
|
104
|
+
payload, "original_evidence_digest"
|
|
105
|
+
),
|
|
106
|
+
counterfactual_reference=_required_string(
|
|
107
|
+
payload, "counterfactual_reference"
|
|
108
|
+
),
|
|
109
|
+
counterfactual_digest=_required_string(payload, "counterfactual_digest"),
|
|
110
|
+
intervention_digest=_required_string(payload, "intervention_digest"),
|
|
111
|
+
schema_version=_required_string(payload, "schema_version"),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class ReplayExecutionContext:
|
|
117
|
+
"""Host-owned attempt metadata exposed to a runtime adapter."""
|
|
118
|
+
|
|
119
|
+
replay_id: str
|
|
120
|
+
source_execution_id: str
|
|
121
|
+
new_execution_id: str
|
|
122
|
+
organization_id: str
|
|
123
|
+
project_id: str
|
|
124
|
+
actor_id: str
|
|
125
|
+
request_id: str
|
|
126
|
+
correlation_id: str | None
|
|
127
|
+
attempt: int
|
|
128
|
+
cancellation_token: ReplayCancellationToken
|
|
129
|
+
metadata: Mapping[str, Any]
|
|
130
|
+
controlled_evidence_intervention: object | None = None
|
|
131
|
+
intervention_envelope: ReplayInterventionEnvelope | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class ReplayExecutionResult:
|
|
136
|
+
"""Runtime-produced data that Core materialises as its workflow execution."""
|
|
137
|
+
|
|
138
|
+
execution_status: str
|
|
139
|
+
input: Mapping[str, Any] = field(default_factory=dict)
|
|
140
|
+
final_state: Mapping[str, Any] = field(default_factory=dict)
|
|
141
|
+
events: Sequence[Mapping[str, Any]] = ()
|
|
142
|
+
artifact_refs: Sequence[str] = ()
|
|
143
|
+
runtime_parameters: Mapping[str, Any] = field(default_factory=dict)
|
|
144
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
145
|
+
created_at: datetime | None = None
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
if (
|
|
149
|
+
not isinstance(self.execution_status, str)
|
|
150
|
+
or not self.execution_status.strip()
|
|
151
|
+
):
|
|
152
|
+
raise ValueError("Replay execution result status is required.")
|
|
153
|
+
object.__setattr__(self, "input", MappingProxyType(dict(self.input)))
|
|
154
|
+
object.__setattr__(
|
|
155
|
+
self, "final_state", MappingProxyType(dict(self.final_state))
|
|
156
|
+
)
|
|
157
|
+
object.__setattr__(
|
|
158
|
+
self, "events", tuple(MappingProxyType(dict(item)) for item in self.events)
|
|
159
|
+
)
|
|
160
|
+
object.__setattr__(self, "artifact_refs", tuple(self.artifact_refs))
|
|
161
|
+
object.__setattr__(
|
|
162
|
+
self, "runtime_parameters", MappingProxyType(dict(self.runtime_parameters))
|
|
163
|
+
)
|
|
164
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ReplayExecutionAdapter(Protocol):
|
|
168
|
+
"""A runtime adapter selected by its versioned persisted name.
|
|
169
|
+
|
|
170
|
+
The source execution and frozen replay configuration are host-owned values.
|
|
171
|
+
Plugin code must not import Core models to implement this protocol. External
|
|
172
|
+
adapters return :class:`ReplayExecutionResult`; hosts may retain temporary
|
|
173
|
+
compatibility for their own legacy in-process adapters.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def name(self) -> str: ...
|
|
178
|
+
|
|
179
|
+
def validate_configuration(
|
|
180
|
+
self, source_execution: object, configuration: object
|
|
181
|
+
) -> None: ...
|
|
182
|
+
|
|
183
|
+
def replay(
|
|
184
|
+
self,
|
|
185
|
+
source_execution: object,
|
|
186
|
+
configuration: object,
|
|
187
|
+
context: ReplayExecutionContext,
|
|
188
|
+
) -> ReplayExecutionResult: ...
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _required_string(payload: Mapping[str, object], name: str) -> str:
|
|
192
|
+
value = payload.get(name)
|
|
193
|
+
if not isinstance(value, str) or not value.strip():
|
|
194
|
+
raise ValueError(f"Replay intervention envelope {name} is required.")
|
|
195
|
+
return value
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _required_version(payload: Mapping[str, object]) -> int:
|
|
199
|
+
value = payload.get("policy_version")
|
|
200
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
"Replay intervention envelope policy_version must be positive."
|
|
203
|
+
)
|
|
204
|
+
return value
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from bhanuj_governance_plugin_api import (
|
|
7
|
+
SPI_VERSION,
|
|
8
|
+
PluginMetadata,
|
|
9
|
+
ReplayExecutionResult,
|
|
10
|
+
ReplayInterventionEnvelope,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_plugin_api_has_no_core_imports() -> None:
|
|
15
|
+
source_root = Path(__file__).parents[1] / "src" / "bhanuj_governance_plugin_api"
|
|
16
|
+
|
|
17
|
+
for source in source_root.glob("*.py"):
|
|
18
|
+
imports = ast.parse(source.read_text()).body
|
|
19
|
+
assert not any(
|
|
20
|
+
isinstance(node, ast.Import)
|
|
21
|
+
and any(
|
|
22
|
+
alias.name == "ai_governance" or alias.name.startswith("ai_governance.")
|
|
23
|
+
for alias in node.names
|
|
24
|
+
)
|
|
25
|
+
or isinstance(node, ast.ImportFrom)
|
|
26
|
+
and node.module is not None
|
|
27
|
+
and (
|
|
28
|
+
node.module == "ai_governance"
|
|
29
|
+
or node.module.startswith("ai_governance.")
|
|
30
|
+
)
|
|
31
|
+
for node in imports
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_plugin_metadata_declares_the_supported_spi_major() -> None:
|
|
36
|
+
metadata = PluginMetadata(
|
|
37
|
+
name="external-runtime",
|
|
38
|
+
version="1.0.0",
|
|
39
|
+
required_ai_governance_version=">=1.1,<2",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert metadata.spi_version == SPI_VERSION == "1"
|
|
43
|
+
assert metadata.contract_version == "v1"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_governed_envelope_rejects_unknown_fields_and_raw_payload_growth() -> None:
|
|
47
|
+
envelope = ReplayInterventionEnvelope(
|
|
48
|
+
policy_id="policy-1",
|
|
49
|
+
policy_version=1,
|
|
50
|
+
external_execution_id="execution-1",
|
|
51
|
+
runtime_tool_call_id="call-1",
|
|
52
|
+
intervention_provider="opaque-reference",
|
|
53
|
+
intervention_provider_version="v1",
|
|
54
|
+
strategy="REPLACE",
|
|
55
|
+
original_evidence_digest="sha256:original",
|
|
56
|
+
counterfactual_reference="runtime://counterfactual",
|
|
57
|
+
counterfactual_digest="sha256:counterfactual",
|
|
58
|
+
intervention_digest="sha256:intervention",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
payload = envelope.to_payload()
|
|
62
|
+
|
|
63
|
+
assert ReplayInterventionEnvelope.from_payload(payload) == envelope
|
|
64
|
+
try:
|
|
65
|
+
ReplayInterventionEnvelope.from_payload({**payload, "raw_evidence": {}})
|
|
66
|
+
except ValueError as error:
|
|
67
|
+
assert "unsupported shape" in str(error)
|
|
68
|
+
else: # pragma: no cover - explicit contract guard
|
|
69
|
+
raise AssertionError("Unknown envelope fields must fail closed.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_external_result_is_immutable_and_has_no_core_execution_type() -> None:
|
|
73
|
+
result = ReplayExecutionResult(
|
|
74
|
+
execution_status="COMPLETED",
|
|
75
|
+
final_state={"outcome": 0.0},
|
|
76
|
+
events=({"type": "EXTERNAL_RUNTIME_REPLAY_COMPLETED"},),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
assert result.final_state["outcome"] == 0.0
|
|
80
|
+
assert result.events[0]["type"] == "EXTERNAL_RUNTIME_REPLAY_COMPLETED"
|