parmana-connector-sdk 0.1.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.
Files changed (26) hide show
  1. parmana_connector_sdk-0.1.0/PKG-INFO +165 -0
  2. parmana_connector_sdk-0.1.0/README.md +133 -0
  3. parmana_connector_sdk-0.1.0/parmana_connector_sdk/__init__.py +90 -0
  4. parmana_connector_sdk-0.1.0/parmana_connector_sdk/connectors/__init__.py +5 -0
  5. parmana_connector_sdk-0.1.0/parmana_connector_sdk/connectors/oracle.py +29 -0
  6. parmana_connector_sdk-0.1.0/parmana_connector_sdk/connectors/salesforce.py +29 -0
  7. parmana_connector_sdk-0.1.0/parmana_connector_sdk/connectors/sap.py +29 -0
  8. parmana_connector_sdk-0.1.0/parmana_connector_sdk/connectors/workday.py +29 -0
  9. parmana_connector_sdk-0.1.0/parmana_connector_sdk/credentials.py +137 -0
  10. parmana_connector_sdk-0.1.0/parmana_connector_sdk/factory.py +24 -0
  11. parmana_connector_sdk-0.1.0/parmana_connector_sdk/metadata.py +66 -0
  12. parmana_connector_sdk-0.1.0/parmana_connector_sdk/mock_connector.py +72 -0
  13. parmana_connector_sdk-0.1.0/parmana_connector_sdk/py.typed +0 -0
  14. parmana_connector_sdk-0.1.0/parmana_connector_sdk/types.py +125 -0
  15. parmana_connector_sdk-0.1.0/parmana_connector_sdk/version.py +1 -0
  16. parmana_connector_sdk-0.1.0/parmana_connector_sdk.egg-info/PKG-INFO +165 -0
  17. parmana_connector_sdk-0.1.0/parmana_connector_sdk.egg-info/SOURCES.txt +24 -0
  18. parmana_connector_sdk-0.1.0/parmana_connector_sdk.egg-info/dependency_links.txt +1 -0
  19. parmana_connector_sdk-0.1.0/parmana_connector_sdk.egg-info/requires.txt +6 -0
  20. parmana_connector_sdk-0.1.0/parmana_connector_sdk.egg-info/top_level.txt +1 -0
  21. parmana_connector_sdk-0.1.0/pyproject.toml +94 -0
  22. parmana_connector_sdk-0.1.0/setup.cfg +4 -0
  23. parmana_connector_sdk-0.1.0/tests/test_credentials.py +102 -0
  24. parmana_connector_sdk-0.1.0/tests/test_enterprise_mock_connectors.py +103 -0
  25. parmana_connector_sdk-0.1.0/tests/test_mock_connector.py +109 -0
  26. parmana_connector_sdk-0.1.0/tests/test_types.py +39 -0
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: parmana-connector-sdk
3
+ Version: 0.1.0
4
+ Summary: Connector authoring contracts and reference implementations for Parmana, the authorization layer for AI execution.
5
+ Author: Parmana
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/pavancharak/parmana
8
+ Project-URL: Website, https://parmanasystems.com/
9
+ Project-URL: Documentation, https://docs.parmanasystems.com
10
+ Project-URL: Repository, https://github.com/pavancharak/parmana
11
+ Project-URL: Issues, https://github.com/pavancharak/parmana/issues
12
+ Keywords: ai,agentic-ai,governance,authorization,connector,integration,sdk
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
30
+ Requires-Dist: black>=24.0.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.10.0; extra == "dev"
32
+
33
+ # parmana-connector-sdk
34
+
35
+ Contracts and reference implementations for building a Connector for [Parmana](https://github.com/pavancharak/parmana), the authorization layer for AI execution.
36
+
37
+ A Connector is the last hop in Parmana's pipeline: it receives an already-authorized, already-verified request and carries it out against a real system (an API, a database, anything). A Connector never evaluates policy, never authorizes execution, and never resolves its own credentials — it only executes, using a credential Parmana has already resolved for it.
38
+
39
+ This is the Python counterpart to the TypeScript [`@parmana/connector-sdk`](https://www.npmjs.com/package/@parmana/connector-sdk) package — same contract, same design, idiomatic Python (synchronous, dataclasses and `Protocol` instead of interfaces).
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install parmana-connector-sdk
45
+ ```
46
+
47
+ ## The contract
48
+
49
+ Every Connector implements one protocol:
50
+
51
+ ```python
52
+ from parmana_connector_sdk import (
53
+ Connector,
54
+ ConnectorRequest,
55
+ ConnectorExecutionContext,
56
+ ConnectorResponse,
57
+ connector_capabilities,
58
+ )
59
+
60
+
61
+ class MyConnector:
62
+ connector_id = "my-service"
63
+ capabilities = connector_capabilities(["my-service:do-thing"])
64
+
65
+ def execute(
66
+ self, request: ConnectorRequest, context: ConnectorExecutionContext
67
+ ) -> ConnectorResponse:
68
+ # request.parameters is exactly what was authorized -- never re-derive it.
69
+ # context.credential is an opaque, already-resolved CredentialHandle.
70
+ # context.timeout_ms is how long you have.
71
+ return ConnectorResponse(success=True, metadata={})
72
+ ```
73
+
74
+ `Connector` is a `typing.Protocol` — `MyConnector` doesn't need to subclass anything, it just
75
+ needs to match the shape (`connector_id`, `capabilities`, `execute`).
76
+
77
+ `capabilities` must be namespaced verbs (`"namespace:verb"`, e.g. `"crm:update-contact"`),
78
+ validated eagerly by `connector_capabilities()` so a malformed capability fails at
79
+ construction, not mid-execution.
80
+
81
+ ## One reference implementation, for testing
82
+
83
+ `MockConnector` gives you scripted responses or failure injection, and records every request
84
+ it receives, so you can test your own Connector's callers without standing up a real endpoint:
85
+
86
+ ```python
87
+ from parmana_connector_sdk import MockConnector, MockConnectorOptions, MockConnectorScript, connector_capabilities
88
+
89
+ connector = MockConnector(
90
+ MockConnectorOptions(
91
+ connector_id="billing",
92
+ capabilities=connector_capabilities(["billing:issue-refund"]),
93
+ script=MockConnectorScript(
94
+ respond=lambda request, context: ConnectorResponse(
95
+ success=True, metadata={"refund_id": "r_1"}
96
+ )
97
+ ),
98
+ )
99
+ )
100
+
101
+ connector.execute(request, context)
102
+ connector.invocations # every request it received, for assertions
103
+ ```
104
+
105
+ This package intentionally ships no HTTP-calling reference implementation — only the contract
106
+ and the hermetic mock above. A REST-shaped connector is usually this simple to write directly
107
+ against the contract:
108
+
109
+ ```python
110
+ import urllib.request
111
+ import json
112
+
113
+ from parmana_connector_sdk import (
114
+ ConnectorRequest,
115
+ ConnectorExecutionContext,
116
+ ConnectorResponse,
117
+ connector_capabilities,
118
+ )
119
+
120
+
121
+ class BillingConnector:
122
+ connector_id = "billing"
123
+ capabilities = connector_capabilities(["billing:issue-refund"])
124
+
125
+ def __init__(self, base_url: str) -> None:
126
+ self.base_url = base_url
127
+
128
+ def execute(
129
+ self, request: ConnectorRequest, context: ConnectorExecutionContext
130
+ ) -> ConnectorResponse:
131
+ token = context.credential.value # shape is whatever your CredentialProvider resolved
132
+ body = json.dumps(request.parameters).encode()
133
+ req = urllib.request.Request(
134
+ f"{self.base_url}/{request.action}",
135
+ data=body,
136
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
137
+ method="POST",
138
+ )
139
+ with urllib.request.urlopen(req, timeout=context.timeout_ms / 1000) as response:
140
+ if response.status >= 300:
141
+ raise RuntimeError(f"HTTP {response.status}")
142
+ return ConnectorResponse(success=True, metadata=json.load(response))
143
+ ```
144
+
145
+ ## Four worked examples
146
+
147
+ `parmana_connector_sdk.connectors.{oracle,salesforce,sap,workday}` are deterministic
148
+ `MockConnector`s shaped like a real enterprise integration — each declares one realistic
149
+ capability and nothing else. They are explicit mocks, not real integrations; read them as a
150
+ template for structuring your own connector's capability and metadata, not as something to
151
+ call in production.
152
+
153
+ ## Credentials
154
+
155
+ A Connector never resolves its own credentials. `CredentialProvider` is the seam Parmana's
156
+ Execution Gateway uses upstream of a Connector — `EnvironmentCredentialProvider` (reads a
157
+ mapped env var) and `StaticCredentialProvider` (in-memory, for tests) are the two provided
158
+ implementations.
159
+
160
+ ## Full reference
161
+
162
+ See the [Connector SDK reference](https://docs.parmanasystems.com/reference/connector-sdk) and
163
+ the [Connector Development Guide](https://docs.parmanasystems.com/integrations/connector-development-guide)
164
+ — written against the TypeScript package, but the contract this package implements is
165
+ identical.
@@ -0,0 +1,133 @@
1
+ # parmana-connector-sdk
2
+
3
+ Contracts and reference implementations for building a Connector for [Parmana](https://github.com/pavancharak/parmana), the authorization layer for AI execution.
4
+
5
+ A Connector is the last hop in Parmana's pipeline: it receives an already-authorized, already-verified request and carries it out against a real system (an API, a database, anything). A Connector never evaluates policy, never authorizes execution, and never resolves its own credentials — it only executes, using a credential Parmana has already resolved for it.
6
+
7
+ This is the Python counterpart to the TypeScript [`@parmana/connector-sdk`](https://www.npmjs.com/package/@parmana/connector-sdk) package — same contract, same design, idiomatic Python (synchronous, dataclasses and `Protocol` instead of interfaces).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install parmana-connector-sdk
13
+ ```
14
+
15
+ ## The contract
16
+
17
+ Every Connector implements one protocol:
18
+
19
+ ```python
20
+ from parmana_connector_sdk import (
21
+ Connector,
22
+ ConnectorRequest,
23
+ ConnectorExecutionContext,
24
+ ConnectorResponse,
25
+ connector_capabilities,
26
+ )
27
+
28
+
29
+ class MyConnector:
30
+ connector_id = "my-service"
31
+ capabilities = connector_capabilities(["my-service:do-thing"])
32
+
33
+ def execute(
34
+ self, request: ConnectorRequest, context: ConnectorExecutionContext
35
+ ) -> ConnectorResponse:
36
+ # request.parameters is exactly what was authorized -- never re-derive it.
37
+ # context.credential is an opaque, already-resolved CredentialHandle.
38
+ # context.timeout_ms is how long you have.
39
+ return ConnectorResponse(success=True, metadata={})
40
+ ```
41
+
42
+ `Connector` is a `typing.Protocol` — `MyConnector` doesn't need to subclass anything, it just
43
+ needs to match the shape (`connector_id`, `capabilities`, `execute`).
44
+
45
+ `capabilities` must be namespaced verbs (`"namespace:verb"`, e.g. `"crm:update-contact"`),
46
+ validated eagerly by `connector_capabilities()` so a malformed capability fails at
47
+ construction, not mid-execution.
48
+
49
+ ## One reference implementation, for testing
50
+
51
+ `MockConnector` gives you scripted responses or failure injection, and records every request
52
+ it receives, so you can test your own Connector's callers without standing up a real endpoint:
53
+
54
+ ```python
55
+ from parmana_connector_sdk import MockConnector, MockConnectorOptions, MockConnectorScript, connector_capabilities
56
+
57
+ connector = MockConnector(
58
+ MockConnectorOptions(
59
+ connector_id="billing",
60
+ capabilities=connector_capabilities(["billing:issue-refund"]),
61
+ script=MockConnectorScript(
62
+ respond=lambda request, context: ConnectorResponse(
63
+ success=True, metadata={"refund_id": "r_1"}
64
+ )
65
+ ),
66
+ )
67
+ )
68
+
69
+ connector.execute(request, context)
70
+ connector.invocations # every request it received, for assertions
71
+ ```
72
+
73
+ This package intentionally ships no HTTP-calling reference implementation — only the contract
74
+ and the hermetic mock above. A REST-shaped connector is usually this simple to write directly
75
+ against the contract:
76
+
77
+ ```python
78
+ import urllib.request
79
+ import json
80
+
81
+ from parmana_connector_sdk import (
82
+ ConnectorRequest,
83
+ ConnectorExecutionContext,
84
+ ConnectorResponse,
85
+ connector_capabilities,
86
+ )
87
+
88
+
89
+ class BillingConnector:
90
+ connector_id = "billing"
91
+ capabilities = connector_capabilities(["billing:issue-refund"])
92
+
93
+ def __init__(self, base_url: str) -> None:
94
+ self.base_url = base_url
95
+
96
+ def execute(
97
+ self, request: ConnectorRequest, context: ConnectorExecutionContext
98
+ ) -> ConnectorResponse:
99
+ token = context.credential.value # shape is whatever your CredentialProvider resolved
100
+ body = json.dumps(request.parameters).encode()
101
+ req = urllib.request.Request(
102
+ f"{self.base_url}/{request.action}",
103
+ data=body,
104
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
105
+ method="POST",
106
+ )
107
+ with urllib.request.urlopen(req, timeout=context.timeout_ms / 1000) as response:
108
+ if response.status >= 300:
109
+ raise RuntimeError(f"HTTP {response.status}")
110
+ return ConnectorResponse(success=True, metadata=json.load(response))
111
+ ```
112
+
113
+ ## Four worked examples
114
+
115
+ `parmana_connector_sdk.connectors.{oracle,salesforce,sap,workday}` are deterministic
116
+ `MockConnector`s shaped like a real enterprise integration — each declares one realistic
117
+ capability and nothing else. They are explicit mocks, not real integrations; read them as a
118
+ template for structuring your own connector's capability and metadata, not as something to
119
+ call in production.
120
+
121
+ ## Credentials
122
+
123
+ A Connector never resolves its own credentials. `CredentialProvider` is the seam Parmana's
124
+ Execution Gateway uses upstream of a Connector — `EnvironmentCredentialProvider` (reads a
125
+ mapped env var) and `StaticCredentialProvider` (in-memory, for tests) are the two provided
126
+ implementations.
127
+
128
+ ## Full reference
129
+
130
+ See the [Connector SDK reference](https://docs.parmanasystems.com/reference/connector-sdk) and
131
+ the [Connector Development Guide](https://docs.parmanasystems.com/integrations/connector-development-guide)
132
+ — written against the TypeScript package, but the contract this package implements is
133
+ identical.
@@ -0,0 +1,90 @@
1
+ """
2
+ Parmana Connector SDK (Python).
3
+
4
+ Connector authoring contracts: capability definitions, DTOs, metadata, and
5
+ the Connector/CredentialProvider protocols. Passive by design -- this
6
+ package describes capabilities, it does not execute them for real. No
7
+ HTTP-calling reference implementation ships from this package; see the
8
+ README for why, and for how short a real connector is to write directly
9
+ against the Connector protocol.
10
+
11
+ Mirrors the TypeScript @parmana/connector-sdk package's shape; see that
12
+ package for the canonical design rationale behind each contract.
13
+ """
14
+
15
+ from .connectors.oracle import OracleMetadata, create_oracle_connector
16
+ from .connectors.salesforce import SalesforceMetadata, create_salesforce_connector
17
+ from .connectors.sap import SapMetadata, create_sap_connector
18
+ from .connectors.workday import WorkdayMetadata, create_workday_connector
19
+ from .credentials import (
20
+ CredentialHandle,
21
+ CredentialProvider,
22
+ EnvironmentCredentialProvider,
23
+ StaticCredentialProvider,
24
+ brand_credential_handle,
25
+ is_credential_handle,
26
+ )
27
+ from .factory import ConnectorFactory
28
+ from .metadata import (
29
+ ConnectorHealth,
30
+ ConnectorHealthStatus,
31
+ ConnectorMetadata,
32
+ ConnectorVersion,
33
+ connector_versions_equal,
34
+ format_connector_version,
35
+ healthy_now,
36
+ )
37
+ from .mock_connector import MockConnector, MockConnectorOptions, MockConnectorScript
38
+ from .types import (
39
+ Connector,
40
+ ConnectorCapabilities,
41
+ ConnectorCapability,
42
+ ConnectorExecutionContext,
43
+ ConnectorRequest,
44
+ ConnectorResponse,
45
+ connector_capabilities,
46
+ is_namespaced_capability,
47
+ )
48
+ from .version import __version__
49
+
50
+ __all__ = [
51
+ "__version__",
52
+ # Contract
53
+ "Connector",
54
+ "ConnectorCapability",
55
+ "ConnectorCapabilities",
56
+ "ConnectorExecutionContext",
57
+ "ConnectorRequest",
58
+ "ConnectorResponse",
59
+ "connector_capabilities",
60
+ "is_namespaced_capability",
61
+ "ConnectorFactory",
62
+ # Metadata
63
+ "ConnectorHealth",
64
+ "ConnectorHealthStatus",
65
+ "ConnectorMetadata",
66
+ "ConnectorVersion",
67
+ "connector_versions_equal",
68
+ "format_connector_version",
69
+ "healthy_now",
70
+ # Credentials
71
+ "CredentialHandle",
72
+ "CredentialProvider",
73
+ "EnvironmentCredentialProvider",
74
+ "StaticCredentialProvider",
75
+ "brand_credential_handle",
76
+ "is_credential_handle",
77
+ # Reference implementation
78
+ "MockConnector",
79
+ "MockConnectorOptions",
80
+ "MockConnectorScript",
81
+ # Four enterprise-named reference mocks
82
+ "OracleMetadata",
83
+ "create_oracle_connector",
84
+ "SalesforceMetadata",
85
+ "create_salesforce_connector",
86
+ "SapMetadata",
87
+ "create_sap_connector",
88
+ "WorkdayMetadata",
89
+ "create_workday_connector",
90
+ ]
@@ -0,0 +1,5 @@
1
+ """
2
+ Four enterprise-named reference mocks. Not re-exported implicitly from
3
+ here -- import each connector's own module, or use the top-level package
4
+ barrel (``parmana_connector_sdk``).
5
+ """
@@ -0,0 +1,29 @@
1
+ """
2
+ Mock Oracle connector.
3
+
4
+ Deterministic, in-memory connector used until a real enterprise connector
5
+ is implemented.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ..metadata import ConnectorMetadata, ConnectorVersion, healthy_now
11
+ from ..mock_connector import MockConnector, MockConnectorOptions
12
+ from ..types import connector_capabilities
13
+
14
+ OracleMetadata = ConnectorMetadata(
15
+ connector_id="oracle",
16
+ display_name="Oracle",
17
+ version=ConnectorVersion(major=1, minor=0, patch=0),
18
+ health=healthy_now(),
19
+ description="Mock connector for Oracle purchase order execution.",
20
+ )
21
+
22
+
23
+ def create_oracle_connector() -> MockConnector:
24
+ return MockConnector(
25
+ MockConnectorOptions(
26
+ connector_id="oracle",
27
+ capabilities=connector_capabilities(["oracle:create-purchase-order"]),
28
+ )
29
+ )
@@ -0,0 +1,29 @@
1
+ """
2
+ Mock Salesforce connector.
3
+
4
+ Deterministic, in-memory connector used until a real enterprise connector
5
+ is implemented.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ..metadata import ConnectorMetadata, ConnectorVersion, healthy_now
11
+ from ..mock_connector import MockConnector, MockConnectorOptions
12
+ from ..types import connector_capabilities
13
+
14
+ SalesforceMetadata = ConnectorMetadata(
15
+ connector_id="salesforce",
16
+ display_name="Salesforce",
17
+ version=ConnectorVersion(major=1, minor=0, patch=0),
18
+ health=healthy_now(),
19
+ description="Mock connector for Salesforce opportunity execution.",
20
+ )
21
+
22
+
23
+ def create_salesforce_connector() -> MockConnector:
24
+ return MockConnector(
25
+ MockConnectorOptions(
26
+ connector_id="salesforce",
27
+ capabilities=connector_capabilities(["salesforce:update-opportunity"]),
28
+ )
29
+ )
@@ -0,0 +1,29 @@
1
+ """
2
+ Mock SAP connector.
3
+
4
+ Deterministic, in-memory connector used until a real enterprise connector
5
+ is implemented.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ..metadata import ConnectorMetadata, ConnectorVersion, healthy_now
11
+ from ..mock_connector import MockConnector, MockConnectorOptions
12
+ from ..types import connector_capabilities
13
+
14
+ SapMetadata = ConnectorMetadata(
15
+ connector_id="sap",
16
+ display_name="SAP",
17
+ version=ConnectorVersion(major=1, minor=0, patch=0),
18
+ health=healthy_now(),
19
+ description="Mock connector for SAP invoice execution.",
20
+ )
21
+
22
+
23
+ def create_sap_connector() -> MockConnector:
24
+ return MockConnector(
25
+ MockConnectorOptions(
26
+ connector_id="sap",
27
+ capabilities=connector_capabilities(["sap:post-invoice"]),
28
+ )
29
+ )
@@ -0,0 +1,29 @@
1
+ """
2
+ Mock Workday connector.
3
+
4
+ Deterministic, in-memory connector used until a real enterprise connector
5
+ is implemented.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ..metadata import ConnectorMetadata, ConnectorVersion, healthy_now
11
+ from ..mock_connector import MockConnector, MockConnectorOptions
12
+ from ..types import connector_capabilities
13
+
14
+ WorkdayMetadata = ConnectorMetadata(
15
+ connector_id="workday",
16
+ display_name="Workday",
17
+ version=ConnectorVersion(major=1, minor=0, patch=0),
18
+ health=healthy_now(),
19
+ description="Mock connector for Workday expense report execution.",
20
+ )
21
+
22
+
23
+ def create_workday_connector() -> MockConnector:
24
+ return MockConnector(
25
+ MockConnectorOptions(
26
+ connector_id="workday",
27
+ capabilities=connector_capabilities(["workday:submit-expense-report"]),
28
+ )
29
+ )
@@ -0,0 +1,137 @@
1
+ """
2
+ Credential resolution seam.
3
+
4
+ Credential resolution happens exclusively inside the Execution Gateway (via a
5
+ CredentialVault backed by one of these providers). Connectors never resolve,
6
+ retrieve, discover, or store credentials themselves -- they only ever receive
7
+ an opaque, already-resolved CredentialHandle.
8
+
9
+ This module defines the interface seam for future providers (HashiCorp
10
+ Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager). Only the
11
+ seam is defined here; no cloud SDK integration is implemented.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from collections.abc import Mapping
18
+ from dataclasses import dataclass, field, replace
19
+ from typing import Any, Protocol, runtime_checkable
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CredentialHandle:
24
+ """
25
+ Opaque credential material resolved by a CredentialProvider.
26
+
27
+ ``value`` is never logged, never included in connector evidence, and
28
+ never serialized into an Execution Trust Record. It exists only to be
29
+ read, ephemerally, by a Connector's own execution call (e.g. an HTTP
30
+ header).
31
+ """
32
+
33
+ provider_id: str
34
+ credential_id: str
35
+ value: Any
36
+ _branded: bool = field(default=False, repr=False, compare=False)
37
+
38
+
39
+ def brand_credential_handle(handle: CredentialHandle) -> CredentialHandle:
40
+ """
41
+ Marks a CredentialHandle as having been produced by a CredentialProvider,
42
+ as opposed to a raw value supplied directly by a caller. Production
43
+ connector executors reject any credential that is not branded this way,
44
+ closing the "raw credential supplied" gap execution must fail closed on.
45
+ """
46
+ return replace(handle, _branded=True)
47
+
48
+
49
+ def is_credential_handle(value: object) -> bool:
50
+ return isinstance(value, CredentialHandle) and value._branded
51
+
52
+
53
+ @runtime_checkable
54
+ class CredentialProvider(Protocol):
55
+ """
56
+ Resolves opaque credential material for a connector.
57
+
58
+ Implementations MUST NOT raise errors that embed the resolved secret
59
+ value -- only identifiers (connector_id, provider_id, credential_id) may
60
+ appear in error messages.
61
+ """
62
+
63
+ provider_id: str
64
+
65
+ def resolve(self, connector_id: str) -> CredentialHandle: ...
66
+
67
+
68
+ class EnvironmentCredentialProvider:
69
+ """
70
+ Resolves credentials from process environment variables.
71
+
72
+ Future providers (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault,
73
+ Google Secret Manager) implement the same CredentialProvider protocol.
74
+ None is implemented in this package.
75
+ """
76
+
77
+ provider_id = "environment"
78
+
79
+ def __init__(
80
+ self,
81
+ connector_environment_variable: Mapping[str, str],
82
+ environment: Mapping[str, str | None] | None = None,
83
+ ) -> None:
84
+ self._connector_environment_variable = dict(connector_environment_variable)
85
+ self._environment: Mapping[str, str | None] = (
86
+ environment if environment is not None else os.environ
87
+ )
88
+
89
+ def resolve(self, connector_id: str) -> CredentialHandle:
90
+ variable_name = self._connector_environment_variable.get(connector_id)
91
+ if variable_name is None:
92
+ raise ValueError(
93
+ "No environment credential mapping configured for connector: "
94
+ f"{connector_id}."
95
+ )
96
+ value = self._environment.get(variable_name)
97
+ if value is None:
98
+ raise ValueError(
99
+ f'Environment variable "{variable_name}" for connector '
100
+ f'"{connector_id}" is not set.'
101
+ )
102
+ return brand_credential_handle(
103
+ CredentialHandle(
104
+ provider_id=self.provider_id,
105
+ credential_id=variable_name,
106
+ value={"token": value},
107
+ )
108
+ )
109
+
110
+
111
+ class StaticCredentialProvider:
112
+ """
113
+ Resolves credentials from an in-memory map.
114
+
115
+ Intended for hermetic tests and local development only.
116
+ """
117
+
118
+ provider_id = "static"
119
+
120
+ def __init__(self, entries: Mapping[str, Any] | None = None) -> None:
121
+ self._credentials: dict[str, Any] = dict(entries or {})
122
+
123
+ def set(self, connector_id: str, value: Any) -> None:
124
+ self._credentials[connector_id] = value
125
+
126
+ def resolve(self, connector_id: str) -> CredentialHandle:
127
+ if connector_id not in self._credentials:
128
+ raise ValueError(
129
+ f"No static credential configured for connector: {connector_id}."
130
+ )
131
+ return brand_credential_handle(
132
+ CredentialHandle(
133
+ provider_id=self.provider_id,
134
+ credential_id=connector_id,
135
+ value=self._credentials[connector_id],
136
+ )
137
+ )
@@ -0,0 +1,24 @@
1
+ """
2
+ Construction contract a connector factory can implement.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Generic, Protocol, TypeVar
8
+
9
+ from .types import Connector
10
+
11
+ TConfig = TypeVar("TConfig", contravariant=True)
12
+
13
+
14
+ class ConnectorFactory(Protocol, Generic[TConfig]):
15
+ """
16
+ Constructs a Connector from configuration.
17
+
18
+ No concrete factory ships in this package; only the seam is defined so
19
+ connectors have a single, consistent construction contract to target.
20
+ """
21
+
22
+ connector_type: str
23
+
24
+ def create(self, config: TConfig) -> Connector: ...