runkite-governance 0.8.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.
@@ -0,0 +1,180 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+ "Legal Entity" shall mean the union of the acting entity and all
15
+ other entities that control, are controlled by, or are under common
16
+ control with that entity. For the purposes of this definition,
17
+ "control" means (i) the power, direct or indirect, to cause the
18
+ direction or management of such entity, whether by contract or
19
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
20
+ outstanding shares, or (iii) beneficial ownership of such entity.
21
+ "You" (or "Your") shall mean an individual or Legal Entity
22
+ exercising permissions granted by this License.
23
+
24
+ "Source" form shall mean the preferred form for making modifications,
25
+ including but not limited to software source code, documentation
26
+ source, and configuration files.
27
+ "Object" form shall mean any form resulting from mechanical
28
+ transformation or translation of a Source form, including but
29
+ not limited to compiled object code, generated documentation,
30
+ and conversions to other media types.
31
+
32
+ "Work" shall mean the work of authorship, whether in Source or
33
+ Object form, made available under the License, as indicated by a
34
+ copyright notice that is included in or attached to the work
35
+ (an example is provided in the Appendix below).
36
+ "Derivative Works" shall mean any work, whether in Source or Object
37
+ form, that is based on (or derived from) the Work and for which the
38
+ editorial revisions, annotations, elaborations, or other modifications
39
+ represent, as a whole, an original work of authorship. For the purposes
40
+ of this License, Derivative Works shall not include works that remain
41
+ separable from, or merely link (or bind by name) to the interfaces of,
42
+ the Work and Derivative Works thereof.
43
+ "Contribution" shall mean any work of authorship, including
44
+ the original version of the Work and any modifications or additions
45
+ to that Work or Derivative Works thereof, that is intentionally
46
+ submitted to Licensor for inclusion in the Work by the copyright owner
47
+ or by an individual or Legal Entity authorized to submit on behalf of
48
+ the copyright owner. For the purposes of this definition, "submitted"
49
+ means any form of electronic, verbal, or written communication sent
50
+ to the Licensor or its representatives, including but not limited to
51
+ communication on electronic mailing lists, source code control systems,
52
+ and issue tracking systems that are managed by, or on behalf of, the
53
+ Licensor for the purpose of discussing and improving the Work, but
54
+ excluding communication that is conspicuously marked or otherwise
55
+ designated in writing by the copyright owner as "Not a Contribution."
56
+ "Contributor" shall mean Licensor and any individual or Legal Entity
57
+ on behalf of whom a Contribution has been received by Licensor and
58
+ subsequently incorporated within the Work.
59
+ 2. Grant of Copyright License. Subject to the terms and conditions of
60
+ this License, each Contributor hereby grants to You a perpetual,
61
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
62
+ copyright license to reproduce, prepare Derivative Works of,
63
+ publicly display, publicly perform, sublicense, and distribute the
64
+ Work and such Derivative Works in Source or Object form.
65
+ 3. Grant of Patent License. Subject to the terms and conditions of
66
+ this License, each Contributor hereby grants to You a perpetual,
67
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
68
+ (except as stated in this section) patent license to make, have made,
69
+ use, offer to sell, sell, import, and otherwise transfer the Work,
70
+ where such license applies only to those patent claims licensable
71
+ by such Contributor that are necessarily infringed by their
72
+ Contribution(s) alone or by combination of their Contribution(s)
73
+ with the Work to which such Contribution(s) was submitted. If You
74
+ institute patent litigation against any entity (including a
75
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
76
+ or a Contribution incorporated within the Work constitutes direct
77
+ or contributory patent infringement, then any patent licenses
78
+ granted to You under this License for that Work shall terminate
79
+ as of the date such litigation is filed.
80
+ 4. Redistribution. You may reproduce and distribute copies of the
81
+ Work or Derivative Works thereof in any medium, with or without
82
+ modifications, and in Source or Object form, provided that You
83
+ meet the following conditions:
84
+
85
+ (a) You must give any other recipients of the Work or
86
+ Derivative Works a copy of this License; and
87
+
88
+ (b) You must cause any modified files to carry prominent notices
89
+ stating that You changed the files; and
90
+ (c) You must retain, in the Source form of any Derivative Works
91
+ that You distribute, all copyright, patent, trademark, and
92
+ attribution notices from the Source form of the Work,
93
+ excluding those notices that do not pertain to any part of
94
+ the Derivative Works; and
95
+ (d) If the Work includes a "NOTICE" text file as part of its
96
+ distribution, then any Derivative Works that You distribute must
97
+ include a readable copy of the attribution notices contained
98
+ within such NOTICE file, excluding those notices that do not
99
+ pertain to any part of the Derivative Works, in at least one
100
+ of the following places: within a NOTICE text file distributed
101
+ as part of the Derivative Works; within the Source form or
102
+ documentation, if provided along with the Derivative Works; or,
103
+ within a display generated by the Derivative Works, if and
104
+ wherever such third-party notices normally appear. The contents
105
+ of the NOTICE file are for informational purposes only and
106
+ do not modify the License. You may add Your own attribution
107
+ notices within Derivative Works that You distribute, alongside
108
+ or as an addendum to the NOTICE text from the Work, provided
109
+ that such additional attribution notices cannot be construed
110
+ as modifying the License.
111
+ You may add Your own copyright statement to Your modifications and
112
+ may provide additional or different license terms and conditions
113
+ for use, reproduction, or distribution of Your modifications, or
114
+ for any such Derivative Works as a whole, provided Your use,
115
+ reproduction, and distribution of the Work otherwise complies with
116
+ the conditions stated in this License.
117
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
118
+ any Contribution intentionally submitted for inclusion in the Work
119
+ by You to the Licensor shall be under the terms and conditions of
120
+ this License, without any additional terms or conditions.
121
+ Notwithstanding the above, nothing herein shall supersede or modify
122
+ the terms of any separate license agreement you may have executed
123
+ with Licensor regarding such Contributions.
124
+ 6. Trademarks. This License does not grant permission to use the trade
125
+ names, trademarks, service marks, or product names of the Licensor,
126
+ except as required for reasonable and customary use in describing the
127
+ origin of the Work and reproducing the content of the NOTICE file.
128
+ 7. Disclaimer of Warranty. Unless required by applicable law or
129
+ agreed to in writing, Licensor provides the Work (and each
130
+ Contributor provides its Contributions) on an "AS IS" BASIS,
131
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
132
+ implied, including, without limitation, any warranties or conditions
133
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
134
+ PARTICULAR PURPOSE. You are solely responsible for determining the
135
+ appropriateness of using or redistributing the Work and assume any
136
+ risks associated with Your exercise of permissions under this License.
137
+ 8. Limitation of Liability. In no event and under no legal theory,
138
+ whether in tort (including negligence), contract, or otherwise,
139
+ unless required by applicable law (such as deliberate and grossly
140
+ negligent acts) or agreed to in writing, shall any Contributor be
141
+ liable to You for damages, including any direct, indirect, special,
142
+ incidental, or consequential damages of any character arising as a
143
+ result of this License or out of the use or inability to use the
144
+ Work (including but not limited to damages for loss of goodwill,
145
+ work stoppage, computer failure or malfunction, or any and all
146
+ other commercial damages or losses), even if such Contributor
147
+ has been advised of the possibility of such damages.
148
+ 9. Accepting Warranty or Additional Liability. While redistributing
149
+ the Work or Derivative Works thereof, You may choose to offer,
150
+ and charge a fee for, acceptance of support, warranty, indemnity,
151
+ or other liability obligations and/or rights consistent with this
152
+ License. However, in accepting such obligations, You may act only
153
+ on Your own behalf and on Your sole responsibility, not on behalf
154
+ of any other Contributor, and only if You agree to indemnify,
155
+ defend, and hold each Contributor harmless for any liability
156
+ incurred by, or claims asserted against, such Contributor by reason
157
+ of your accepting any such warranty or additional liability.
158
+ END OF TERMS AND CONDITIONS
159
+
160
+ APPENDIX: How to apply the Apache License to your work.
161
+ To apply the Apache License to your work, attach the following
162
+ boilerplate notice, with the fields enclosed by brackets "[]"
163
+ replaced with your own identifying information. (Don't include
164
+ the brackets!) The text should be enclosed in the appropriate
165
+ comment syntax for the file format. We also recommend that a
166
+ file or class name and description of purpose be included on the
167
+ same "printed page" as the copyright notice for easier
168
+ identification within third-party archives.
169
+ Copyright [yyyy] [name of copyright owner]
170
+
171
+ Licensed under the Apache License, Version 2.0 (the "License");
172
+ you may not use this file except in compliance with the License.
173
+ You may obtain a copy of the License at
174
+
175
+ http://www.apache.org/licenses/LICENSE-2.0
176
+ Unless required by applicable law or agreed to in writing, software
177
+ distributed under the License is distributed on an "AS IS" BASIS,
178
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
179
+ See the License for the specific language governing permissions and
180
+ limitations under the License.
@@ -0,0 +1,6 @@
1
+ include LICENSE NOTICE README.md pyproject.toml __init__.py agent_framework.py py.typed
2
+ global-exclude test_*.py e2e_*.py *.py[cod]
3
+ prune tests
4
+ prune build
5
+ prune dist
6
+ prune .venv
@@ -0,0 +1,5 @@
1
+ Runkite
2
+ Copyright 2026 Sharan Harsoor
3
+
4
+ Runkite's first-party code is licensed under the Apache License, Version 2.0.
5
+ Third-party components retain their respective copyrights and license terms.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: runkite-governance
3
+ Version: 0.8.0
4
+ Summary: Small decide and pending-action helper for app-owned agent loops.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Repository, https://github.com/getrunkite/runkite
7
+ Project-URL: Documentation, https://github.com/getrunkite/runkite/blob/main/docs/client-sdk.md
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Provides-Extra: agent-framework
13
+ Requires-Dist: agent-framework-core==1.23.0; extra == "agent-framework"
14
+ Dynamic: license-file
15
+
16
+ # Runkite governance helper
17
+
18
+ A standard-library-only Python client for `POST /governance/decide` and `GET /governance/actions/{id}` on your self-hosted Runkite plane. It does not depend on the runner package or an agent framework.
19
+
20
+ This helper has its own distribution, separate from `runkite-runner`. The
21
+ published Runkite 0.7.0 release did not include it. Use the registry command
22
+ only for a version whose publication has been verified:
23
+
24
+ ```sh
25
+ python -m pip install runkite-governance
26
+ ```
27
+
28
+ For source that has not been published, install from a checkout:
29
+
30
+ ```sh
31
+ python -m pip install ./python/runkite_governance
32
+ ```
33
+
34
+ The runtime requires Python 3.11 or newer. Installing a managed runner does not
35
+ install this decision-only helper.
36
+
37
+ ```python
38
+ from runkite_governance import GovernanceClient
39
+
40
+ governance = GovernanceClient("http://localhost:2026", api_key="server-side-key", timeout_ms=10_000)
41
+ decision = governance.decide(
42
+ agent_id="notes-app",
43
+ connector="workspace",
44
+ tool="createNote",
45
+ args={"title": "Release notes", "body": "A draft"},
46
+ run_id="stable-app-turn-and-tool-call-id",
47
+ )
48
+ ```
49
+
50
+ Your application executes after `allow`, refuses after `deny`, and persists the action ID and exact original arguments after `pending`. On a later application request, use `governance.action(action_id)` and execute only after `approved`. Rejected or consumed actions must not execute. Implement idempotency in the application's database: GET does not consume approval.
51
+
52
+ `resolve(...)` performs a bounded wait and returns an allow/deny outcome. Its deadline covers the initial decision and every poll. `decide` and `action` accept `timeout_ms` and a `threading.Event` as `cancelled`. Cancellation stops the caller promptly. Blocking standard-library I/O unwinds separately with a socket timeout and response-chunk checks; a bounded shared worker pool prevents unlimited abandoned resolver/socket threads. Do not hold a serverless request open for long operator approval.
53
+
54
+ Use a server-side key with `governance:decide`. Tenant identity comes from that key. The caller retains its model loop and tool execution. This path creates no Runkite thread or run and does not apply run kill switches, break-glass or spend caps. It is separate from the deliberately deferred full client SDK.
55
+
56
+ ## Optional Microsoft Agent Framework middleware
57
+
58
+ For a verified registry release, install the optional middleware with:
59
+
60
+ ```sh
61
+ python -m pip install 'runkite-governance[agent-framework]'
62
+ ```
63
+
64
+ For unpublished source, install the extra from a checkout:
65
+
66
+ ```sh
67
+ python -m pip install './python/runkite_governance[agent-framework]'
68
+ ```
69
+
70
+ The extra pins the tested framework core; model-provider packages remain
71
+ application choices.
72
+
73
+ Import `GovernanceFunctionMiddleware`, `GovernedFunction`,
74
+ `GovernanceBlockedError` and `PendingInvocation` from
75
+ `runkite_governance.agent_framework`. Register exact local `FunctionTool` objects,
76
+ server-owned policy identities and an application operation-ID callback. Install
77
+ the guard last, after argument repair. It snapshots JSON arguments and Python
78
+ defaults, decides once, and reaches native execution only after allow. Deny and
79
+ pending stop the agent with a native `MiddlewareFailure`; an optional async
80
+ `on_pending` callback persists the exact frozen proposal first.
81
+
82
+ The app owns pending continuation and execution idempotency. Rich Python types,
83
+ coercing validators and provider-executed tools are outside this adapter's scope.
84
+ See the [framework example](https://github.com/getrunkite/runkite/tree/main/examples/governance_agent_framework/README.md)
85
+ for a durable application transaction, offline streaming and non-streaming tests,
86
+ and supported argument constraints. Execution retains native mutable containers;
87
+ the app must not mutate invocation arguments after authorization. Importing the
88
+ base client does not import the optional framework. This extra is not a runner
89
+ adapter and creates no run.
90
+
91
+ License: Apache-2.0. See `LICENSE` and `NOTICE`. Third-party dependencies retain
92
+ their own licenses.
@@ -0,0 +1,77 @@
1
+ # Runkite governance helper
2
+
3
+ A standard-library-only Python client for `POST /governance/decide` and `GET /governance/actions/{id}` on your self-hosted Runkite plane. It does not depend on the runner package or an agent framework.
4
+
5
+ This helper has its own distribution, separate from `runkite-runner`. The
6
+ published Runkite 0.7.0 release did not include it. Use the registry command
7
+ only for a version whose publication has been verified:
8
+
9
+ ```sh
10
+ python -m pip install runkite-governance
11
+ ```
12
+
13
+ For source that has not been published, install from a checkout:
14
+
15
+ ```sh
16
+ python -m pip install ./python/runkite_governance
17
+ ```
18
+
19
+ The runtime requires Python 3.11 or newer. Installing a managed runner does not
20
+ install this decision-only helper.
21
+
22
+ ```python
23
+ from runkite_governance import GovernanceClient
24
+
25
+ governance = GovernanceClient("http://localhost:2026", api_key="server-side-key", timeout_ms=10_000)
26
+ decision = governance.decide(
27
+ agent_id="notes-app",
28
+ connector="workspace",
29
+ tool="createNote",
30
+ args={"title": "Release notes", "body": "A draft"},
31
+ run_id="stable-app-turn-and-tool-call-id",
32
+ )
33
+ ```
34
+
35
+ Your application executes after `allow`, refuses after `deny`, and persists the action ID and exact original arguments after `pending`. On a later application request, use `governance.action(action_id)` and execute only after `approved`. Rejected or consumed actions must not execute. Implement idempotency in the application's database: GET does not consume approval.
36
+
37
+ `resolve(...)` performs a bounded wait and returns an allow/deny outcome. Its deadline covers the initial decision and every poll. `decide` and `action` accept `timeout_ms` and a `threading.Event` as `cancelled`. Cancellation stops the caller promptly. Blocking standard-library I/O unwinds separately with a socket timeout and response-chunk checks; a bounded shared worker pool prevents unlimited abandoned resolver/socket threads. Do not hold a serverless request open for long operator approval.
38
+
39
+ Use a server-side key with `governance:decide`. Tenant identity comes from that key. The caller retains its model loop and tool execution. This path creates no Runkite thread or run and does not apply run kill switches, break-glass or spend caps. It is separate from the deliberately deferred full client SDK.
40
+
41
+ ## Optional Microsoft Agent Framework middleware
42
+
43
+ For a verified registry release, install the optional middleware with:
44
+
45
+ ```sh
46
+ python -m pip install 'runkite-governance[agent-framework]'
47
+ ```
48
+
49
+ For unpublished source, install the extra from a checkout:
50
+
51
+ ```sh
52
+ python -m pip install './python/runkite_governance[agent-framework]'
53
+ ```
54
+
55
+ The extra pins the tested framework core; model-provider packages remain
56
+ application choices.
57
+
58
+ Import `GovernanceFunctionMiddleware`, `GovernedFunction`,
59
+ `GovernanceBlockedError` and `PendingInvocation` from
60
+ `runkite_governance.agent_framework`. Register exact local `FunctionTool` objects,
61
+ server-owned policy identities and an application operation-ID callback. Install
62
+ the guard last, after argument repair. It snapshots JSON arguments and Python
63
+ defaults, decides once, and reaches native execution only after allow. Deny and
64
+ pending stop the agent with a native `MiddlewareFailure`; an optional async
65
+ `on_pending` callback persists the exact frozen proposal first.
66
+
67
+ The app owns pending continuation and execution idempotency. Rich Python types,
68
+ coercing validators and provider-executed tools are outside this adapter's scope.
69
+ See the [framework example](https://github.com/getrunkite/runkite/tree/main/examples/governance_agent_framework/README.md)
70
+ for a durable application transaction, offline streaming and non-streaming tests,
71
+ and supported argument constraints. Execution retains native mutable containers;
72
+ the app must not mutate invocation arguments after authorization. Importing the
73
+ base client does not import the optional framework. This extra is not a runner
74
+ adapter and creates no run.
75
+
76
+ License: Apache-2.0. See `LICENSE` and `NOTICE`. Third-party dependencies retain
77
+ their own licenses.
@@ -0,0 +1,321 @@
1
+ """Calls POST /governance/decide, then polls GET /governance/actions/{id}
2
+ when the effect is pending. The app executes the tool after allow.
3
+ Runkite does not execute the tool and does not consume the approval
4
+ on poll. This is not the deferred client SDK in docs/client-sdk.md.
5
+
6
+ Stdlib only. Until the first registry publication is verified, install from
7
+ the repository with ``python -m pip install ./python/runkite_governance``.
8
+ The v0.8.0 source candidate prepares distribution; it does not publish it.
9
+ The optional Microsoft middleware lives in ``runkite_governance.agent_framework``.
10
+ The base client has the same decision boundary as typescript/runkite-governance.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import math
17
+ import queue
18
+ import threading
19
+ import time
20
+ import urllib.error
21
+ import urllib.parse
22
+ import urllib.request
23
+ from typing import Any
24
+
25
+ DEFAULT_INTERVAL_MS = 500
26
+ DEFAULT_TIMEOUT_MS = 60_000
27
+
28
+ # Bound abandoned resolver/socket operations as well as the caller wait.
29
+ _HTTP_WORKERS = threading.BoundedSemaphore(32)
30
+
31
+ __all__ = ["DEFAULT_INTERVAL_MS", "DEFAULT_TIMEOUT_MS", "GovernanceClient", "GovernanceError"]
32
+
33
+
34
+ class GovernanceError(Exception):
35
+ def __init__(self, message: str, status: int) -> None:
36
+ super().__init__(message)
37
+ self.status = status
38
+
39
+
40
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
41
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
42
+ # A decision must come from the configured plane. Do not forward its
43
+ # bearer credential or accept authorization from a redirected endpoint.
44
+ return None
45
+
46
+
47
+ class GovernanceClient:
48
+ def __init__(
49
+ self,
50
+ base_url: str,
51
+ *,
52
+ api_key: str | None = None,
53
+ interval_ms: int = DEFAULT_INTERVAL_MS,
54
+ timeout_ms: int = DEFAULT_TIMEOUT_MS,
55
+ ) -> None:
56
+ self._base = base_url.rstrip("/")
57
+ self._api_key = api_key
58
+ self._interval_ms = interval_ms
59
+ self._timeout_ms = timeout_ms
60
+ self._opener = urllib.request.build_opener(_NoRedirect())
61
+
62
+ def decide(
63
+ self,
64
+ *,
65
+ agent_id: str,
66
+ connector: str,
67
+ tool: str,
68
+ args: dict[str, Any] | None = None,
69
+ run_id: str = "",
70
+ tenant_id: str | None = None,
71
+ cancelled: threading.Event | None = None,
72
+ timeout_ms: float | None = None,
73
+ ) -> dict[str, Any]:
74
+ body: dict[str, Any] = {
75
+ "agent_id": agent_id,
76
+ "connector": connector,
77
+ "tool": tool,
78
+ "args": {} if args is None else args,
79
+ "run_id": run_id,
80
+ }
81
+ if tenant_id:
82
+ body["tenant_id"] = tenant_id
83
+ return _as_decision(
84
+ self._request("POST", "/governance/decide", body, cancelled=cancelled, timeout_ms=timeout_ms)
85
+ )
86
+
87
+ def action(
88
+ self, action_id: str, *, cancelled: threading.Event | None = None, timeout_ms: float | None = None
89
+ ) -> dict[str, Any]:
90
+ path = "/governance/actions/" + urllib.parse.quote(action_id, safe="")
91
+ return _as_action(self._request("GET", path, cancelled=cancelled, timeout_ms=timeout_ms))
92
+
93
+ def resolve(
94
+ self,
95
+ *,
96
+ agent_id: str,
97
+ connector: str,
98
+ tool: str,
99
+ args: dict[str, Any] | None = None,
100
+ run_id: str = "",
101
+ tenant_id: str | None = None,
102
+ interval_ms: int | None = None,
103
+ timeout_ms: int | None = None,
104
+ cancelled: threading.Event | None = None,
105
+ ) -> dict[str, Any]:
106
+ """Poll until the row settles. Set cancelled from another thread to stop, including during the wait."""
107
+ _raise_if_cancelled(cancelled)
108
+ limit = self._timeout_ms if timeout_ms is None else timeout_ms
109
+ if not math.isfinite(limit) or limit <= 0:
110
+ raise GovernanceError("governance poll timed out", 408)
111
+ deadline = time.monotonic() + limit / 1000
112
+ decision = self.decide(
113
+ agent_id=agent_id,
114
+ connector=connector,
115
+ tool=tool,
116
+ args=args,
117
+ run_id=run_id,
118
+ tenant_id=tenant_id,
119
+ cancelled=cancelled,
120
+ timeout_ms=_remaining(deadline, cancelled) * 1000,
121
+ )
122
+ if decision["effect"] != "pending":
123
+ return _compact(
124
+ {
125
+ "effect": decision["effect"],
126
+ "reason": decision.get("reason"),
127
+ "reason_code": decision.get("reason_code"),
128
+ }
129
+ )
130
+ action_id = decision.get("action_id")
131
+ if not isinstance(action_id, str) or not action_id:
132
+ raise GovernanceError("pending decision missing action_id", 502)
133
+ every = self._interval_ms if interval_ms is None else interval_ms
134
+ if not math.isfinite(every) or every < 0:
135
+ raise GovernanceError("interval_ms must be non-negative", 400)
136
+ while True:
137
+ action = self.action(action_id, cancelled=cancelled, timeout_ms=_remaining(deadline, cancelled) * 1000)
138
+ if action["status"] != "pending":
139
+ return _outcome(action)
140
+ pause = min(every / 1000, _remaining(deadline, cancelled))
141
+ if cancelled is not None and cancelled.wait(pause):
142
+ raise GovernanceError("governance poll aborted", 499)
143
+ if cancelled is None:
144
+ time.sleep(pause)
145
+
146
+ def _request(
147
+ self,
148
+ method: str,
149
+ path: str,
150
+ body: dict[str, Any] | None = None,
151
+ *,
152
+ cancelled: threading.Event | None = None,
153
+ timeout_ms: float | None = None,
154
+ ) -> Any:
155
+ """Bound the caller's entire request, including headers and response body.
156
+
157
+ urllib is blocking. A daemon transport worker lets cancellation stop
158
+ the caller immediately. Its socket deadline and chunk checks bound
159
+ cleanup without replaying a decision that the plane may have received.
160
+ """
161
+ limit = self._timeout_ms if timeout_ms is None else timeout_ms
162
+ if not math.isfinite(limit) or limit <= 0:
163
+ raise GovernanceError("governance poll timed out", 408)
164
+ deadline = time.monotonic() + limit / 1000
165
+ _remaining(deadline, cancelled)
166
+ data = None
167
+ headers = {"Accept": "application/json"}
168
+ if body is not None:
169
+ data = json.dumps(body).encode()
170
+ headers["Content-Type"] = "application/json"
171
+ if self._api_key:
172
+ headers["Authorization"] = "Bearer " + self._api_key
173
+ req = urllib.request.Request(self._base + path, data=data, headers=headers, method=method)
174
+ results: queue.Queue = queue.Queue(maxsize=1)
175
+ workers = _HTTP_WORKERS
176
+
177
+ def read_body(resp) -> bytes:
178
+ chunks = []
179
+ while True:
180
+ _remaining(deadline, cancelled)
181
+ chunk = resp.read1(64 * 1024)
182
+ if not chunk:
183
+ return b"".join(chunks)
184
+ chunks.append(chunk)
185
+
186
+ def transport() -> None:
187
+ try:
188
+ with self._opener.open(req, timeout=_remaining(deadline, cancelled)) as resp:
189
+ if not 200 <= resp.status < 300:
190
+ raise GovernanceError(f"governance request failed: HTTP {resp.status}", resp.status)
191
+ raw = read_body(resp)
192
+ results.put((True, json.loads(raw) if raw else None))
193
+ except urllib.error.HTTPError as exc:
194
+ # Error bodies are subject to the same outer wall-clock limit.
195
+ try:
196
+ message = _message(read_body(exc), exc.reason or "")
197
+ results.put((False, GovernanceError(message, exc.code)))
198
+ except Exception as error:
199
+ results.put((False, error))
200
+ finally:
201
+ exc.close()
202
+ except json.JSONDecodeError:
203
+ results.put((True, None))
204
+ except Exception as exc:
205
+ results.put((False, exc))
206
+ finally:
207
+ workers.release()
208
+
209
+ while not workers.acquire(timeout=min(_remaining(deadline, cancelled), 0.025)):
210
+ pass
211
+ try:
212
+ _remaining(deadline, cancelled)
213
+ threading.Thread(target=transport, daemon=True, name="runkite-governance-http").start()
214
+ except BaseException:
215
+ workers.release()
216
+ raise
217
+ while True:
218
+ remaining = _remaining(deadline, cancelled)
219
+ try:
220
+ ok, value = results.get(timeout=min(remaining, 0.025))
221
+ except queue.Empty:
222
+ continue
223
+ _remaining(deadline, cancelled)
224
+ if ok:
225
+ return value
226
+ if (
227
+ isinstance(value, (TimeoutError,))
228
+ or isinstance(value, urllib.error.URLError)
229
+ and isinstance(value.reason, TimeoutError)
230
+ ):
231
+ raise GovernanceError("governance poll timed out", 408) from value
232
+ raise value
233
+
234
+
235
+ def _as_decision(raw: Any) -> dict[str, Any]:
236
+ row = _object(raw)
237
+ effect = row.get("effect")
238
+ if effect not in ("allow", "deny", "pending"):
239
+ raise GovernanceError("governance decide response missing effect", 502)
240
+ return _compact(
241
+ {
242
+ "effect": effect,
243
+ "reason": _optional_str(row.get("reason")),
244
+ "reason_code": _optional_str(row.get("reason_code")),
245
+ "action_id": _optional_str(row.get("action_id")),
246
+ }
247
+ )
248
+
249
+
250
+ def _as_action(raw: Any) -> dict[str, Any]:
251
+ row = _object(raw)
252
+ status = row.get("status")
253
+ if status not in ("pending", "approved", "rejected", "consumed"):
254
+ raise GovernanceError("governance action response missing status", 502)
255
+ action_id = _optional_str(row.get("action_id"))
256
+ if not action_id:
257
+ raise GovernanceError("governance action response missing action_id", 502)
258
+ return _compact(
259
+ {
260
+ "action_id": action_id,
261
+ "status": status,
262
+ "reason": _optional_str(row.get("reason")),
263
+ "reason_code": _optional_str(row.get("reason_code")),
264
+ }
265
+ )
266
+
267
+
268
+ def _outcome(action: dict[str, Any]) -> dict[str, Any]:
269
+ return _compact(
270
+ {
271
+ "effect": "allow" if action["status"] == "approved" else "deny",
272
+ "reason": action.get("reason"),
273
+ "reason_code": action.get("reason_code"),
274
+ "action_id": action["action_id"],
275
+ "status": action["status"],
276
+ }
277
+ )
278
+
279
+
280
+ def _object(raw: Any) -> dict[str, Any]:
281
+ if not isinstance(raw, dict):
282
+ raise GovernanceError("governance response was not JSON", 502)
283
+ return raw
284
+
285
+
286
+ def _optional_str(value: Any) -> str | None:
287
+ if isinstance(value, str) and value != "":
288
+ return value
289
+ return None
290
+
291
+
292
+ def _compact(row: dict[str, Any]) -> dict[str, Any]:
293
+ return {key: value for key, value in row.items() if value is not None}
294
+
295
+
296
+ def _raise_if_cancelled(cancelled: threading.Event | None) -> None:
297
+ if cancelled is not None and cancelled.is_set():
298
+ raise GovernanceError("governance poll aborted", 499)
299
+
300
+
301
+ def _remaining(deadline: float, cancelled: threading.Event | None) -> float:
302
+ _raise_if_cancelled(cancelled)
303
+ remaining = deadline - time.monotonic()
304
+ if remaining <= 0:
305
+ raise GovernanceError("governance poll timed out", 408)
306
+ return remaining
307
+
308
+
309
+ def _message(raw: bytes, fallback: str) -> str:
310
+ text = raw.decode("utf-8", errors="replace").strip()
311
+ if not text:
312
+ return fallback or "governance request failed"
313
+ try:
314
+ parsed = json.loads(text)
315
+ except json.JSONDecodeError:
316
+ return text
317
+ if isinstance(parsed, dict):
318
+ message = parsed.get("message")
319
+ if isinstance(message, str) and message:
320
+ return message
321
+ return text
@@ -0,0 +1,319 @@
1
+ """Optional Microsoft Agent Framework function middleware.
2
+
3
+ Install this last, after argument-repair middleware. Only JSON-native function
4
+ arguments and JSON defaults stable under native validation are supported. The
5
+ application retains its loop, execution idempotency and approval continuation.
6
+ Importing runkite_governance itself does not import this optional dependency.
7
+
8
+ Execution arguments remain ordinary native dict/list containers. The framework
9
+ may reuse prepared argument aliases, so the application, middleware and tool
10
+ must not mutate invocation data after authorization. This is a pre-execution
11
+ gate, not an immutable execution environment. Pending records are immutable.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import inspect
18
+ import json
19
+ import math
20
+ import threading
21
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
22
+ from dataclasses import dataclass
23
+ from types import MappingProxyType
24
+ from typing import Any
25
+
26
+ from agent_framework import FunctionInvocationContext, FunctionMiddleware, FunctionTool, MiddlewareFailure
27
+
28
+ from . import GovernanceClient
29
+
30
+ __all__ = [
31
+ "GovernedFunction",
32
+ "GovernanceFunctionMiddleware",
33
+ "GovernanceBlockedError",
34
+ "PendingInvocation",
35
+ ]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class GovernedFunction:
40
+ function: FunctionTool
41
+ connector: str
42
+ tool: str | None = None
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class PendingInvocation:
47
+ action_id: str
48
+ agent_id: str
49
+ connector: str
50
+ tool: str
51
+ run_id: str
52
+ arguments: Mapping[str, Any]
53
+
54
+
55
+ class GovernanceBlockedError(MiddlewareFailure):
56
+ """A denied or pending operation. Arguments are never attached to this error."""
57
+
58
+ def __init__(self, decision: Mapping[str, str]) -> None:
59
+ copied = _decision(dict(decision))
60
+ if copied["effect"] == "allow":
61
+ raise MiddlewareFailure("A blocked governance decision cannot allow execution")
62
+ super().__init__(
63
+ "Runkite tool execution is pending approval"
64
+ if copied["effect"] == "pending"
65
+ else "Runkite tool execution was denied"
66
+ )
67
+ self.decision = copied
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class _Binding:
72
+ function: FunctionTool
73
+ connector: str
74
+ tool: str
75
+ name: str
76
+ func: Callable[..., Any]
77
+ model: Any
78
+ signature: inspect.Signature
79
+ properties: tuple[str, ...]
80
+ schema: bytes
81
+
82
+
83
+ def _identifier(value: Any) -> str:
84
+ if type(value) is not str or not value or value != value.strip():
85
+ raise ValueError("Governance identifiers must be canonical nonempty strings")
86
+ encoded = value.encode("utf-8", errors="strict")
87
+ if len(encoded) > 256:
88
+ raise ValueError("Governance identifiers must be at most 256 UTF-8 bytes")
89
+ return value
90
+
91
+
92
+ def _snapshot(value: Any, *, root: bool = True) -> Any:
93
+ """Copy exact JSON types without custom serialization or coercion."""
94
+ ancestors: set[int] = set()
95
+
96
+ def copy(item: Any) -> Any:
97
+ kind = type(item)
98
+ if item is None or kind is bool:
99
+ return item
100
+ if kind is str:
101
+ item.encode("utf-8", errors="strict")
102
+ return item
103
+ if kind is int:
104
+ if abs(item) > 2**53 - 1:
105
+ raise ValueError("Tool integers must be in the safe JSON integer range")
106
+ return item
107
+ if kind is float:
108
+ if not math.isfinite(item) or item == 0 and math.copysign(1, item) < 0:
109
+ raise ValueError("Tool numbers must be lossless finite JSON values")
110
+ return item
111
+ if kind not in (dict, list) or id(item) in ancestors:
112
+ raise ValueError("Tool arguments must contain exact JSON types without cycles")
113
+ ancestors.add(id(item))
114
+ try:
115
+ if kind is list:
116
+ return [copy(part) for part in item]
117
+ result = {}
118
+ for key, part in item.items():
119
+ if type(key) is not str:
120
+ raise ValueError("Tool argument keys must be strings")
121
+ key.encode("utf-8", errors="strict")
122
+ result[key] = copy(part)
123
+ return result
124
+ finally:
125
+ ancestors.remove(id(item))
126
+
127
+ if root and type(value) is not dict:
128
+ raise ValueError("Tool arguments must be a JSON object")
129
+ return copy(value)
130
+
131
+
132
+ def _encoded(value: Any) -> bytes:
133
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
134
+
135
+
136
+ def _freeze(value: Any) -> Any:
137
+ if type(value) is dict:
138
+ return MappingProxyType({key: _freeze(part) for key, part in value.items()})
139
+ if type(value) is list:
140
+ return tuple(_freeze(part) for part in value)
141
+ return value
142
+
143
+
144
+ def _decision(raw: Any) -> Mapping[str, str]:
145
+ if type(raw) is not dict:
146
+ raise ValueError("Governance decision must be an object")
147
+ copied = raw.copy()
148
+ effect = copied.get("effect")
149
+ if type(effect) is not str or effect not in ("allow", "deny", "pending"):
150
+ raise ValueError("Governance decision is missing a valid effect")
151
+ result = {"effect": effect}
152
+ for key in ("reason", "reason_code", "action_id"):
153
+ if key in copied:
154
+ if type(copied[key]) is not str:
155
+ raise ValueError("Governance decision metadata must be strings")
156
+ result[key] = copied[key]
157
+ if effect == "pending":
158
+ _identifier(result.get("action_id"))
159
+ return MappingProxyType(result)
160
+
161
+
162
+ def _binding(config: GovernedFunction) -> _Binding:
163
+ function = config.function
164
+ if not isinstance(function, FunctionTool) or not (
165
+ inspect.isfunction(function.func) or inspect.ismethod(function.func)
166
+ ):
167
+ raise ValueError("Governed functions must wrap ordinary Python callables")
168
+ signature = inspect.signature(function.func)
169
+ if any(
170
+ parameter.kind
171
+ in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY)
172
+ for parameter in signature.parameters.values()
173
+ ):
174
+ raise ValueError("Variadic and positional-only governed functions are unsupported")
175
+ schema = _snapshot(function.parameters())
176
+ properties = schema.get("properties", {})
177
+ if type(properties) is not dict or any(name not in signature.parameters for name in properties):
178
+ raise ValueError("Governed function schemas must name declared Python parameters")
179
+ return _Binding(
180
+ function,
181
+ _identifier(config.connector),
182
+ _identifier(function.name if config.tool is None else config.tool),
183
+ _identifier(function.name),
184
+ function.func,
185
+ function.input_model,
186
+ signature,
187
+ tuple(properties),
188
+ _encoded(schema),
189
+ )
190
+
191
+
192
+ def _projection(binding: _Binding, source: dict[str, Any]) -> dict[str, Any]:
193
+ if any(name not in binding.properties for name in source):
194
+ raise ValueError("Tool arguments must name declared tool parameters")
195
+ bound = binding.signature.bind_partial(**source)
196
+ bound.apply_defaults()
197
+ projected = _snapshot({name: bound.arguments[name] for name in binding.properties if name in bound.arguments})
198
+ if any(
199
+ name not in projected
200
+ for name in binding.properties
201
+ if binding.signature.parameters[name].default is inspect.Parameter.empty
202
+ ):
203
+ raise ValueError("Tool arguments are missing a required parameter")
204
+ if binding.model is not None:
205
+ validated = binding.model.model_validate(_snapshot(projected)).model_dump(mode="python", exclude_unset=True)
206
+ if _encoded(_snapshot(validated)) != _encoded(projected):
207
+ raise ValueError("Tool normalization must preserve the governed JSON arguments")
208
+ return projected
209
+
210
+
211
+ def _unchanged(binding: _Binding, context: FunctionInvocationContext, original: Any, before: bytes) -> None:
212
+ if (
213
+ context.function is not binding.function
214
+ or binding.function.name != binding.name
215
+ or binding.function.func is not binding.func
216
+ or binding.function.input_model is not binding.model
217
+ or context.arguments is not original
218
+ or _encoded(_snapshot(original)) != before
219
+ or _encoded(_snapshot(context.arguments)) != before
220
+ or _encoded(_snapshot(binding.function.parameters())) != binding.schema
221
+ ):
222
+ raise ValueError("The governed function or its arguments changed during the decision")
223
+
224
+
225
+ def _check_cancellation() -> None:
226
+ task = asyncio.current_task()
227
+ if task is not None and task.cancelling():
228
+ raise asyncio.CancelledError
229
+
230
+
231
+ class GovernanceFunctionMiddleware(FunctionMiddleware):
232
+ """Decide once before native execution. Install last, after argument repair.
233
+
234
+ No polling, approval replay or model-loop ownership is provided. Pending
235
+ persistence and exactly-once application side effects remain app concerns.
236
+ """
237
+
238
+ def __init__(
239
+ self,
240
+ client: GovernanceClient,
241
+ *,
242
+ agent_id: str,
243
+ functions: Sequence[GovernedFunction],
244
+ operation_id: Callable[[FunctionInvocationContext], str],
245
+ on_pending: Callable[[PendingInvocation], Awaitable[None]] | None = None,
246
+ ) -> None:
247
+ try:
248
+ self._agent_id = _identifier(agent_id)
249
+ self._decide = client.decide
250
+ if not callable(self._decide) or not callable(operation_id):
251
+ raise ValueError("Governance decision and operation ID callbacks must be callable")
252
+ if on_pending is not None and not callable(on_pending):
253
+ raise ValueError("The pending callback must be callable")
254
+ self._bindings = tuple(_binding(config) for config in functions)
255
+ if len({id(binding.function) for binding in self._bindings}) != len(self._bindings):
256
+ raise ValueError("Governed function objects must be unique")
257
+ self._operation_id = operation_id
258
+ self._on_pending = on_pending
259
+ self._bridges = asyncio.Semaphore(32)
260
+ except Exception as exc:
261
+ raise MiddlewareFailure("Invalid Runkite governance middleware configuration") from exc
262
+
263
+ async def process(self, context: FunctionInvocationContext, call_next: Callable[[], Awaitable[None]]) -> None:
264
+ cancelled = threading.Event()
265
+ try:
266
+ _check_cancellation()
267
+ binding = next((item for item in self._bindings if context.function is item.function), None)
268
+ if binding is None:
269
+ raise ValueError("This function object is not registered for governance")
270
+ original = context.arguments
271
+ source = _snapshot(original)
272
+ before = _encoded(source)
273
+ projected = _projection(binding, source)
274
+ run_id = _identifier(self._operation_id(context))
275
+ _unchanged(binding, context, original, before)
276
+ request_args = _snapshot(projected)
277
+ async with self._bridges:
278
+ raw = await asyncio.to_thread(
279
+ self._decide,
280
+ agent_id=self._agent_id,
281
+ connector=binding.connector,
282
+ tool=binding.tool,
283
+ args=request_args,
284
+ run_id=run_id,
285
+ cancelled=cancelled,
286
+ )
287
+ _check_cancellation()
288
+ decision = _decision(raw)
289
+ _unchanged(binding, context, original, before)
290
+ if _encoded(_snapshot(request_args)) != _encoded(projected):
291
+ raise ValueError("Governance request arguments changed during the decision")
292
+ if decision["effect"] == "pending" and self._on_pending is not None:
293
+ await self._on_pending(
294
+ PendingInvocation(
295
+ decision["action_id"],
296
+ self._agent_id,
297
+ binding.connector,
298
+ binding.tool,
299
+ run_id,
300
+ _freeze(projected),
301
+ )
302
+ )
303
+ if decision["effect"] != "allow":
304
+ raise GovernanceBlockedError(decision)
305
+ _check_cancellation()
306
+ # Preserve ordinary dict/list semantics, including nested Any values.
307
+ # Native prepared-argument caches may reuse checked original aliases.
308
+ context.arguments = _snapshot(projected)
309
+ except asyncio.CancelledError:
310
+ cancelled.set()
311
+ raise
312
+ except MiddlewareFailure:
313
+ raise
314
+ except Exception as exc:
315
+ raise MiddlewareFailure("Runkite governance check failed") from exc
316
+ finally:
317
+ cancelled.set()
318
+ # Native tool failures retain the framework's handling. Never retry them.
319
+ await call_next()
File without changes
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "runkite-governance"
7
+ version = "0.8.0"
8
+ description = "Small decide and pending-action helper for app-owned agent loops."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ license-files = ["LICENSE", "NOTICE"]
12
+ requires-python = ">=3.11"
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ agent-framework = ["agent-framework-core==1.23.0"]
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/getrunkite/runkite"
20
+ Documentation = "https://github.com/getrunkite/runkite/blob/main/docs/client-sdk.md"
21
+
22
+ [tool.setuptools]
23
+ packages = ["runkite_governance"]
24
+ package-dir = { runkite_governance = "." }
25
+
26
+ [tool.setuptools.package-data]
27
+ runkite_governance = ["py.typed"]
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: runkite-governance
3
+ Version: 0.8.0
4
+ Summary: Small decide and pending-action helper for app-owned agent loops.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Repository, https://github.com/getrunkite/runkite
7
+ Project-URL: Documentation, https://github.com/getrunkite/runkite/blob/main/docs/client-sdk.md
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Provides-Extra: agent-framework
13
+ Requires-Dist: agent-framework-core==1.23.0; extra == "agent-framework"
14
+ Dynamic: license-file
15
+
16
+ # Runkite governance helper
17
+
18
+ A standard-library-only Python client for `POST /governance/decide` and `GET /governance/actions/{id}` on your self-hosted Runkite plane. It does not depend on the runner package or an agent framework.
19
+
20
+ This helper has its own distribution, separate from `runkite-runner`. The
21
+ published Runkite 0.7.0 release did not include it. Use the registry command
22
+ only for a version whose publication has been verified:
23
+
24
+ ```sh
25
+ python -m pip install runkite-governance
26
+ ```
27
+
28
+ For source that has not been published, install from a checkout:
29
+
30
+ ```sh
31
+ python -m pip install ./python/runkite_governance
32
+ ```
33
+
34
+ The runtime requires Python 3.11 or newer. Installing a managed runner does not
35
+ install this decision-only helper.
36
+
37
+ ```python
38
+ from runkite_governance import GovernanceClient
39
+
40
+ governance = GovernanceClient("http://localhost:2026", api_key="server-side-key", timeout_ms=10_000)
41
+ decision = governance.decide(
42
+ agent_id="notes-app",
43
+ connector="workspace",
44
+ tool="createNote",
45
+ args={"title": "Release notes", "body": "A draft"},
46
+ run_id="stable-app-turn-and-tool-call-id",
47
+ )
48
+ ```
49
+
50
+ Your application executes after `allow`, refuses after `deny`, and persists the action ID and exact original arguments after `pending`. On a later application request, use `governance.action(action_id)` and execute only after `approved`. Rejected or consumed actions must not execute. Implement idempotency in the application's database: GET does not consume approval.
51
+
52
+ `resolve(...)` performs a bounded wait and returns an allow/deny outcome. Its deadline covers the initial decision and every poll. `decide` and `action` accept `timeout_ms` and a `threading.Event` as `cancelled`. Cancellation stops the caller promptly. Blocking standard-library I/O unwinds separately with a socket timeout and response-chunk checks; a bounded shared worker pool prevents unlimited abandoned resolver/socket threads. Do not hold a serverless request open for long operator approval.
53
+
54
+ Use a server-side key with `governance:decide`. Tenant identity comes from that key. The caller retains its model loop and tool execution. This path creates no Runkite thread or run and does not apply run kill switches, break-glass or spend caps. It is separate from the deliberately deferred full client SDK.
55
+
56
+ ## Optional Microsoft Agent Framework middleware
57
+
58
+ For a verified registry release, install the optional middleware with:
59
+
60
+ ```sh
61
+ python -m pip install 'runkite-governance[agent-framework]'
62
+ ```
63
+
64
+ For unpublished source, install the extra from a checkout:
65
+
66
+ ```sh
67
+ python -m pip install './python/runkite_governance[agent-framework]'
68
+ ```
69
+
70
+ The extra pins the tested framework core; model-provider packages remain
71
+ application choices.
72
+
73
+ Import `GovernanceFunctionMiddleware`, `GovernedFunction`,
74
+ `GovernanceBlockedError` and `PendingInvocation` from
75
+ `runkite_governance.agent_framework`. Register exact local `FunctionTool` objects,
76
+ server-owned policy identities and an application operation-ID callback. Install
77
+ the guard last, after argument repair. It snapshots JSON arguments and Python
78
+ defaults, decides once, and reaches native execution only after allow. Deny and
79
+ pending stop the agent with a native `MiddlewareFailure`; an optional async
80
+ `on_pending` callback persists the exact frozen proposal first.
81
+
82
+ The app owns pending continuation and execution idempotency. Rich Python types,
83
+ coercing validators and provider-executed tools are outside this adapter's scope.
84
+ See the [framework example](https://github.com/getrunkite/runkite/tree/main/examples/governance_agent_framework/README.md)
85
+ for a durable application transaction, offline streaming and non-streaming tests,
86
+ and supported argument constraints. Execution retains native mutable containers;
87
+ the app must not mutate invocation arguments after authorization. Importing the
88
+ base client does not import the optional framework. This extra is not a runner
89
+ adapter and creates no run.
90
+
91
+ License: Apache-2.0. See `LICENSE` and `NOTICE`. Third-party dependencies retain
92
+ their own licenses.
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ NOTICE
4
+ README.md
5
+ __init__.py
6
+ agent_framework.py
7
+ py.typed
8
+ pyproject.toml
9
+ ./__init__.py
10
+ ./agent_framework.py
11
+ ./py.typed
12
+ runkite_governance.egg-info/PKG-INFO
13
+ runkite_governance.egg-info/SOURCES.txt
14
+ runkite_governance.egg-info/dependency_links.txt
15
+ runkite_governance.egg-info/requires.txt
16
+ runkite_governance.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [agent-framework]
3
+ agent-framework-core==1.23.0
@@ -0,0 +1 @@
1
+ runkite_governance
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+