monoid-agent-kernel 0.16.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. monoid_agent_kernel/__init__.py +11 -0
  2. monoid_agent_kernel/_policy_util.py +40 -0
  3. monoid_agent_kernel/_proc.py +86 -0
  4. monoid_agent_kernel/builder.py +351 -0
  5. monoid_agent_kernel/cli.py +1244 -0
  6. monoid_agent_kernel/conformance/__init__.py +32 -0
  7. monoid_agent_kernel/conformance/harness.py +209 -0
  8. monoid_agent_kernel/conformance/profiles/__init__.py +55 -0
  9. monoid_agent_kernel/conformance/profiles/_metadata.py +16 -0
  10. monoid_agent_kernel/conformance/profiles/capability_security.py +216 -0
  11. monoid_agent_kernel/conformance/profiles/control_plane.py +41 -0
  12. monoid_agent_kernel/conformance/profiles/durable_runner.py +42 -0
  13. monoid_agent_kernel/conformance/profiles/message_fabric.py +42 -0
  14. monoid_agent_kernel/conformance/profiles/minimal_agent.py +13 -0
  15. monoid_agent_kernel/conformance/profiles/multi_agent.py +112 -0
  16. monoid_agent_kernel/conformance/profiles/provider_gateway.py +107 -0
  17. monoid_agent_kernel/conformance/profiles/reference_full.py +135 -0
  18. monoid_agent_kernel/conformance/profiles/side_effect_tool_agent.py +36 -0
  19. monoid_agent_kernel/conformance/profiles/tool_agent.py +39 -0
  20. monoid_agent_kernel/contracts.py +329 -0
  21. monoid_agent_kernel/core/__init__.py +1 -0
  22. monoid_agent_kernel/core/_util.py +145 -0
  23. monoid_agent_kernel/core/agents.py +883 -0
  24. monoid_agent_kernel/core/cancellation.py +16 -0
  25. monoid_agent_kernel/core/capability.py +376 -0
  26. monoid_agent_kernel/core/capability_revocation.py +92 -0
  27. monoid_agent_kernel/core/checkpoint.py +350 -0
  28. monoid_agent_kernel/core/content.py +101 -0
  29. monoid_agent_kernel/core/context.py +67 -0
  30. monoid_agent_kernel/core/control.py +124 -0
  31. monoid_agent_kernel/core/control_audit.py +99 -0
  32. monoid_agent_kernel/core/durable_metadata.py +116 -0
  33. monoid_agent_kernel/core/event_sequencing.py +164 -0
  34. monoid_agent_kernel/core/events.py +204 -0
  35. monoid_agent_kernel/core/external_agent_envelope.py +338 -0
  36. monoid_agent_kernel/core/frontmatter.py +140 -0
  37. monoid_agent_kernel/core/inbox.py +110 -0
  38. monoid_agent_kernel/core/lease_admission.py +57 -0
  39. monoid_agent_kernel/core/lifecycle.py +440 -0
  40. monoid_agent_kernel/core/manifest.py +88 -0
  41. monoid_agent_kernel/core/media.py +393 -0
  42. monoid_agent_kernel/core/outbox.py +212 -0
  43. monoid_agent_kernel/core/output_validator.py +103 -0
  44. monoid_agent_kernel/core/packages.py +742 -0
  45. monoid_agent_kernel/core/projections.py +213 -0
  46. monoid_agent_kernel/core/prompt.py +37 -0
  47. monoid_agent_kernel/core/proposal_file.py +84 -0
  48. monoid_agent_kernel/core/result.py +131 -0
  49. monoid_agent_kernel/core/schemas.py +1146 -0
  50. monoid_agent_kernel/core/scope.py +185 -0
  51. monoid_agent_kernel/core/side_effect_policy.py +153 -0
  52. monoid_agent_kernel/core/spec.py +325 -0
  53. monoid_agent_kernel/core/streaming.py +208 -0
  54. monoid_agent_kernel/core/subagent_runtime.py +187 -0
  55. monoid_agent_kernel/core/tool_approval.py +175 -0
  56. monoid_agent_kernel/core/tool_surface.py +505 -0
  57. monoid_agent_kernel/core/trace_context.py +76 -0
  58. monoid_agent_kernel/core/wire_validation.py +156 -0
  59. monoid_agent_kernel/core/workspace.py +161 -0
  60. monoid_agent_kernel/core/workspace_index.py +127 -0
  61. monoid_agent_kernel/env.py +32 -0
  62. monoid_agent_kernel/errors.py +105 -0
  63. monoid_agent_kernel/event_loader.py +47 -0
  64. monoid_agent_kernel/identifiers.py +50 -0
  65. monoid_agent_kernel/loop.py +4140 -0
  66. monoid_agent_kernel/loop_phases.py +561 -0
  67. monoid_agent_kernel/mcp/__init__.py +10 -0
  68. monoid_agent_kernel/mcp/client.py +227 -0
  69. monoid_agent_kernel/mcp/provider.py +407 -0
  70. monoid_agent_kernel/narration.py +92 -0
  71. monoid_agent_kernel/observability/__init__.py +9 -0
  72. monoid_agent_kernel/observability/otel.py +264 -0
  73. monoid_agent_kernel/permissions.py +74 -0
  74. monoid_agent_kernel/providers/__init__.py +7 -0
  75. monoid_agent_kernel/providers/_common.py +122 -0
  76. monoid_agent_kernel/providers/base.py +312 -0
  77. monoid_agent_kernel/providers/fake.py +76 -0
  78. monoid_agent_kernel/providers/gateway.py +474 -0
  79. monoid_agent_kernel/providers/openai.py +487 -0
  80. monoid_agent_kernel/public_view.py +173 -0
  81. monoid_agent_kernel/py.typed +0 -0
  82. monoid_agent_kernel/recorder.py +421 -0
  83. monoid_agent_kernel/reference/__init__.py +15 -0
  84. monoid_agent_kernel/reference/_shared/__init__.py +4 -0
  85. monoid_agent_kernel/reference/_shared/http_util.py +111 -0
  86. monoid_agent_kernel/reference/_shared/tokens.py +314 -0
  87. monoid_agent_kernel/reference/backend/__init__.py +21 -0
  88. monoid_agent_kernel/reference/backend/commands.py +375 -0
  89. monoid_agent_kernel/reference/backend/http.py +439 -0
  90. monoid_agent_kernel/reference/backend/jobs.py +68 -0
  91. monoid_agent_kernel/reference/backend/loop_factory.py +253 -0
  92. monoid_agent_kernel/reference/backend/outbox_dispatch.py +130 -0
  93. monoid_agent_kernel/reference/backend/ports.py +188 -0
  94. monoid_agent_kernel/reference/backend/projection.py +286 -0
  95. monoid_agent_kernel/reference/backend/proposal.py +220 -0
  96. monoid_agent_kernel/reference/backend/proposal_reader.py +16 -0
  97. monoid_agent_kernel/reference/backend/recovery.py +253 -0
  98. monoid_agent_kernel/reference/backend/run_execution.py +152 -0
  99. monoid_agent_kernel/reference/backend/run_preparation.py +197 -0
  100. monoid_agent_kernel/reference/backend/run_state.py +243 -0
  101. monoid_agent_kernel/reference/backend/run_types.py +128 -0
  102. monoid_agent_kernel/reference/backend/runtime_config.py +147 -0
  103. monoid_agent_kernel/reference/backend/service.py +1664 -0
  104. monoid_agent_kernel/reference/backend/session.py +280 -0
  105. monoid_agent_kernel/reference/backend/session_drive.py +231 -0
  106. monoid_agent_kernel/reference/capability.py +101 -0
  107. monoid_agent_kernel/reference/conformance.py +1777 -0
  108. monoid_agent_kernel/reference/llm_gateway/__init__.py +14 -0
  109. monoid_agent_kernel/reference/llm_gateway/http.py +225 -0
  110. monoid_agent_kernel/reference/llm_gateway/providers.py +96 -0
  111. monoid_agent_kernel/reference/llm_gateway/service.py +404 -0
  112. monoid_agent_kernel/reference/mcp_gateway/__init__.py +26 -0
  113. monoid_agent_kernel/reference/mcp_gateway/http.py +187 -0
  114. monoid_agent_kernel/reference/mcp_gateway/service.py +206 -0
  115. monoid_agent_kernel/reference/outbox.py +135 -0
  116. monoid_agent_kernel/reference/stores/__init__.py +20 -0
  117. monoid_agent_kernel/reference/stores/lease.py +116 -0
  118. monoid_agent_kernel/reference/stores/sqlite.py +295 -0
  119. monoid_agent_kernel/reference/studio/README.md +97 -0
  120. monoid_agent_kernel/reference/studio/__init__.py +21 -0
  121. monoid_agent_kernel/reference/studio/activity.py +53 -0
  122. monoid_agent_kernel/reference/studio/cli.py +481 -0
  123. monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/SKILL.md +25 -0
  124. monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/references/review-checklist.md +8 -0
  125. monoid_agent_kernel/reference/studio/sample-skills/commit-message/SKILL.md +32 -0
  126. monoid_agent_kernel/reference/studio/sample-skills/incident-summary/SKILL.md +24 -0
  127. monoid_agent_kernel/reference/studio/sample-skills/incident-summary/references/incident-template.md +32 -0
  128. monoid_agent_kernel/reference/studio/sample-skills/release-notes/SKILL.md +24 -0
  129. monoid_agent_kernel/reference/studio/sample-skills/release-notes/references/release-note-template.md +33 -0
  130. monoid_agent_kernel/reference/studio/sample-skills/release-notes/scripts/collect_changes.py +51 -0
  131. monoid_agent_kernel/reference/studio/server.py +1922 -0
  132. monoid_agent_kernel/reference/studio/web/index.html +1877 -0
  133. monoid_agent_kernel/reference/studio/web/settings.html +108 -0
  134. monoid_agent_kernel/reference/studio/web/vendor/katex/auto-render.min.js +1 -0
  135. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  136. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  137. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  138. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  139. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  140. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  141. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  142. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  143. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  144. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  145. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  146. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  147. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  148. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  149. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  150. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  151. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  152. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  153. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  154. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  155. monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.css +1 -0
  156. monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.js +1 -0
  157. monoid_agent_kernel/reference/studio/window.py +72 -0
  158. monoid_agent_kernel/reference/web_gateway/__init__.py +6 -0
  159. monoid_agent_kernel/reference/web_gateway/http.py +155 -0
  160. monoid_agent_kernel/reference/web_gateway/providers.py +807 -0
  161. monoid_agent_kernel/reference/web_gateway/service.py +559 -0
  162. monoid_agent_kernel/shell.py +722 -0
  163. monoid_agent_kernel/skills/__init__.py +20 -0
  164. monoid_agent_kernel/skills/definition.py +82 -0
  165. monoid_agent_kernel/skills/loader.py +55 -0
  166. monoid_agent_kernel/skills/provider.py +378 -0
  167. monoid_agent_kernel/subagent_loader.py +56 -0
  168. monoid_agent_kernel/tasks.py +1326 -0
  169. monoid_agent_kernel/tool_loader.py +51 -0
  170. monoid_agent_kernel/tool_services/__init__.py +8 -0
  171. monoid_agent_kernel/tool_services/base.py +25 -0
  172. monoid_agent_kernel/tool_services/jobs.py +47 -0
  173. monoid_agent_kernel/tool_services/shell.py +255 -0
  174. monoid_agent_kernel/tool_services/web.py +356 -0
  175. monoid_agent_kernel/tools/__init__.py +2 -0
  176. monoid_agent_kernel/tools/base.py +205 -0
  177. monoid_agent_kernel/tools/builtin.py +958 -0
  178. monoid_agent_kernel/tools/decorator.py +132 -0
  179. monoid_agent_kernel/tools/tool_ids.py +62 -0
  180. monoid_agent_kernel/web.py +171 -0
  181. monoid_agent_kernel/workspace/__init__.py +2 -0
  182. monoid_agent_kernel/workspace/local.py +1004 -0
  183. monoid_agent_kernel/workspace/paths.py +30 -0
  184. monoid_agent_kernel-0.16.1.dist-info/METADATA +709 -0
  185. monoid_agent_kernel-0.16.1.dist-info/RECORD +191 -0
  186. monoid_agent_kernel-0.16.1.dist-info/WHEEL +4 -0
  187. monoid_agent_kernel-0.16.1.dist-info/entry_points.txt +3 -0
  188. monoid_agent_kernel-0.16.1.dist-info/licenses/LICENSE +202 -0
  189. monoid_agent_kernel-0.16.1.dist-info/licenses/NOTICE +8 -0
  190. native_agent_runner/__init__.py +103 -0
  191. native_agent_runner/py.typed +0 -0
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass
8
+ class CancellationToken:
9
+ _event: threading.Event = field(default_factory=threading.Event, init=False, repr=False)
10
+
11
+ def cancel(self) -> None:
12
+ self._event.set()
13
+
14
+ @property
15
+ def requested(self) -> bool:
16
+ return self._event.is_set()
@@ -0,0 +1,376 @@
1
+ """Capability request/lease: the agent asks for scoped, short-lived access; secrets stay out.
2
+
3
+ The runtime never holds a raw credential. When a tool needs external access (web, email, a
4
+ cloud API), it carries a *capability* requirement (declared on its binding). At call time the
5
+ loop asks a :class:`CapabilityBroker` for a lease — a scoped, expiring handle (``token_ref``,
6
+ never the secret) — and only then runs the tool. This generalizes the gateway-token pattern
7
+ (LLM/web access already keep the provider key behind a gateway) into one contract any
8
+ capability can use, and makes acquisition on-demand and brokered (auto-grant, policy, or
9
+ human escalation) rather than only statically provisioned at run start.
10
+
11
+ Protocols:
12
+ ``monoid.capability-request.v1`` / ``...capability-lease.v1``
13
+
14
+ Security invariants the core enforces (see ``CapabilityVault.admit``):
15
+ - the secret never enters the core (a lease carries ``token_ref``, a handle);
16
+ - a grant may only NARROW the requested scope, never widen it (fail-closed);
17
+ - a lease is checked for expiry before every use; an expired lease is re-requested;
18
+ - leases are NOT checkpointed — on restart they are re-brokered (no stale secret on disk).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import time
24
+ import uuid
25
+ from dataclasses import dataclass, field
26
+ from typing import Any, Protocol, runtime_checkable
27
+
28
+ from monoid_agent_kernel.core.capability_revocation import (
29
+ CapabilityRevocationState,
30
+ apply_capability_revocation,
31
+ export_revocation_state,
32
+ import_revocation_state,
33
+ is_capability_revoked,
34
+ is_lease_revoked,
35
+ )
36
+ from monoid_agent_kernel.core.lease_admission import validate_lease_admission
37
+ from monoid_agent_kernel.core.scope import scope_within
38
+ from monoid_agent_kernel.core.wire_validation import (
39
+ parse_bool,
40
+ parse_float,
41
+ parse_str,
42
+ require_object,
43
+ )
44
+ from monoid_agent_kernel.identifiers import accepted_namespaced_ids, namespaced_id
45
+
46
+ CAPABILITY_REQUEST_VERSION = namespaced_id("capability-request.v1")
47
+ CAPABILITY_LEASE_VERSION = namespaced_id("capability-lease.v1")
48
+ ACCEPTED_CAPABILITY_LEASE_VERSIONS = accepted_namespaced_ids("capability-lease.v1")
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class CapabilityRequest:
53
+ """A scoped, time-boxed request for a capability, issued by the core when a tool needs
54
+ access it does not yet hold a lease for."""
55
+
56
+ capability: str
57
+ scope: dict[str, Any] = field(default_factory=dict)
58
+ run_id: str = ""
59
+ binding_id: str = ""
60
+ ttl_seconds: int = 600
61
+ reason: str = ""
62
+ request_id: str = field(default_factory=lambda: f"cap_req_{uuid.uuid4().hex[:12]}")
63
+
64
+ def to_json(self) -> dict[str, Any]:
65
+ return {
66
+ "protocol": CAPABILITY_REQUEST_VERSION,
67
+ "request_id": self.request_id,
68
+ "run_id": self.run_id,
69
+ "binding_id": self.binding_id,
70
+ "capability": self.capability,
71
+ "scope": dict(self.scope),
72
+ "ttl_seconds": self.ttl_seconds,
73
+ "reason": self.reason,
74
+ }
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class CapabilityLease:
79
+ """A granted lease: a scoped, expiring handle to a secret the broker manages. ``token_ref``
80
+ is a reference (e.g. ``secret-ref://…`` or a gateway token), never the raw secret —
81
+ resolution happens at the edge (the gateway/tool), not in the core."""
82
+
83
+ capability: str
84
+ token_ref: str
85
+ expires_at: float # epoch seconds; checked before every use
86
+ scope: dict[str, Any] = field(default_factory=dict)
87
+ lease_id: str = field(default_factory=lambda: f"lease_{uuid.uuid4().hex[:12]}")
88
+ # Whether this lease should survive a restart (checkpointed). Sync auto-grants stay ephemeral
89
+ # (False) — re-brokering is cheap and no handle touches disk. A human/policy-approved lease is
90
+ # marked durable so a restart does not re-prompt the approver. The handle (token_ref), never a
91
+ # secret, is what persists.
92
+ durable: bool = False
93
+ # When the lease was minted (epoch seconds). Backs the per-run "revoke everything issued before
94
+ # T" watermark (a bulk cohort kill, à la AWS STS ``aws:TokenIssueTime``). Old checkpoint payloads
95
+ # without it decode to ``0.0`` — safely *before* any watermark, so they fail closed.
96
+ issued_at: float = field(default_factory=time.time)
97
+ # Absolute lifetime ceiling (epoch seconds). Rotation may refresh the lease repeatedly, but never
98
+ # past this — so a one-time human approval cannot be silently auto-extended forever. ``None`` =
99
+ # no ceiling (the default for ephemeral sync grants); a policy/approval broker sets it.
100
+ max_expires_at: float | None = None
101
+
102
+ def is_valid(self, now: float) -> bool:
103
+ return now < self.expires_at
104
+
105
+ def can_rotate(self, now: float, skew: float) -> bool:
106
+ """True if this lease should be refreshed now: still valid, within ``skew`` seconds of
107
+ expiry, and not yet at its absolute ceiling. Past the ceiling it is left to expire (then the
108
+ normal re-broker / re-escalation path applies) rather than auto-extended."""
109
+ if not self.is_valid(now) or now < self.expires_at - skew:
110
+ return False
111
+ return self.max_expires_at is None or now < self.max_expires_at
112
+
113
+ def to_json(self) -> dict[str, Any]:
114
+ return {
115
+ "protocol": CAPABILITY_LEASE_VERSION,
116
+ "lease_id": self.lease_id,
117
+ "capability": self.capability,
118
+ "scope": dict(self.scope),
119
+ "expires_at": self.expires_at,
120
+ "token_ref": self.token_ref,
121
+ "durable": self.durable,
122
+ "issued_at": self.issued_at,
123
+ "max_expires_at": self.max_expires_at,
124
+ }
125
+
126
+ @classmethod
127
+ def from_json(cls, payload: dict[str, Any]) -> CapabilityLease:
128
+ payload = require_object(payload, "capability lease")
129
+ protocol = parse_str(payload, "protocol")
130
+ if protocol and protocol not in ACCEPTED_CAPABILITY_LEASE_VERSIONS:
131
+ raise ValueError("unsupported capability lease protocol")
132
+ max_expires_at = parse_float(
133
+ payload,
134
+ "max_expires_at",
135
+ default=0.0,
136
+ allow_none=True,
137
+ ) if "max_expires_at" in payload else None
138
+ scope = require_object(payload["scope"], "scope") if "scope" in payload else {}
139
+ kwargs: dict[str, Any] = {
140
+ "capability": parse_str(payload, "capability"),
141
+ "token_ref": parse_str(payload, "token_ref"),
142
+ "expires_at": parse_float(payload, "expires_at", default=0.0) or 0.0,
143
+ "scope": dict(scope),
144
+ "durable": parse_bool(payload, "durable", default=False),
145
+ "issued_at": parse_float(payload, "issued_at", default=0.0) or 0.0,
146
+ "max_expires_at": max_expires_at,
147
+ }
148
+ lease_id = parse_str(payload, "lease_id")
149
+ if lease_id:
150
+ kwargs["lease_id"] = lease_id
151
+ return cls(**kwargs)
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class CapabilityDenial:
156
+ """A broker's refusal to grant. ``retryable`` hints whether a later attempt might succeed
157
+ (e.g. a transient policy backend) versus a hard no."""
158
+
159
+ capability: str
160
+ reason: str = ""
161
+ retryable: bool = False
162
+
163
+ def to_json(self) -> dict[str, Any]:
164
+ return {
165
+ "capability": self.capability,
166
+ "reason": self.reason,
167
+ "retryable": self.retryable,
168
+ }
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class CapabilityPending:
173
+ """The broker cannot grant synchronously — the request must be escalated (e.g. human/Daemon
174
+ approval). The loop parks the run on a ``capability`` hosted-task carrying ``request``; when the
175
+ grant is reported (``report_task_result``) the lease is admitted to the vault and the model
176
+ retries the gated tool. ``prompt`` is a human-facing description for the approval UI."""
177
+
178
+ request: CapabilityRequest
179
+ prompt: str = ""
180
+
181
+ def to_json(self) -> dict[str, Any]:
182
+ return {
183
+ "capability": self.request.capability,
184
+ "request_id": self.request.request_id,
185
+ "prompt": self.prompt,
186
+ }
187
+
188
+
189
+ CapabilityGrant = CapabilityLease | CapabilityDenial | CapabilityPending
190
+
191
+
192
+ @runtime_checkable
193
+ class CapabilityBroker(Protocol):
194
+ """The seam an integrator (an Agent Daemon / Cell) implements to decide capability access.
195
+ The core only ever *requests*; the broker grants a scoped lease or denies. Transport-neutral:
196
+ an in-process policy object, a gateway-token minter, or a human-escalation broker all fit."""
197
+
198
+ def request(self, req: CapabilityRequest) -> CapabilityGrant: ...
199
+
200
+
201
+ @dataclass
202
+ class CapabilityVault:
203
+ """Per-run, in-memory cache of granted leases. Holds only handles (``token_ref``), never
204
+ secrets. Durable (human/policy-approved) leases are checkpointed; ephemeral sync grants are
205
+ not, so they re-broker on restart and no handle for them survives on disk. ``admit`` is the
206
+ core's fail-closed gate: a grant that widens the requested scope is rejected.
207
+
208
+ Revocation is an *object-capability caretaker* move: because a tool only ever holds a handle
209
+ that it re-fetches per call (via :meth:`token_for`), revoking is simply the vault refusing to
210
+ hand the handle back. The read path (:meth:`get_valid` / :meth:`token_for`) is **fail-closed**
211
+ against three revocation records — a per-lease set, a per-capability set, and a per-run
212
+ ``issued_before`` watermark (a bulk cohort kill). The gate additionally refuses to *re-broker*
213
+ a revoked capability (see ``AgentLoop._ensure_capability_lease``) so revocation survives even a
214
+ permissive broker."""
215
+
216
+ _leases: dict[str, CapabilityLease] = field(default_factory=dict)
217
+ _revocations: CapabilityRevocationState = field(default_factory=CapabilityRevocationState)
218
+
219
+ @property
220
+ def _revoked_lease_ids(self) -> set[str]:
221
+ return self._revocations.lease_ids
222
+
223
+ @_revoked_lease_ids.setter
224
+ def _revoked_lease_ids(self, value: set[str]) -> None:
225
+ self._revocations.lease_ids = value
226
+
227
+ @property
228
+ def _revoked_capabilities(self) -> set[str]:
229
+ return self._revocations.capabilities
230
+
231
+ @_revoked_capabilities.setter
232
+ def _revoked_capabilities(self, value: set[str]) -> None:
233
+ self._revocations.capabilities = value
234
+
235
+ @property
236
+ def _revoked_before(self) -> float:
237
+ return self._revocations.before
238
+
239
+ @_revoked_before.setter
240
+ def _revoked_before(self, value: float) -> None:
241
+ self._revocations.before = value
242
+
243
+ @property
244
+ def _revoked_all(self) -> bool:
245
+ return self._revocations.all_revoked
246
+
247
+ @_revoked_all.setter
248
+ def _revoked_all(self, value: bool) -> None:
249
+ self._revocations.all_revoked = value
250
+
251
+ def _is_revoked(self, lease: CapabilityLease) -> bool:
252
+ return is_lease_revoked(self._revocations, lease)
253
+
254
+ def get_valid(self, capability: str, scope: dict[str, Any], *, now: float) -> CapabilityLease | None:
255
+ """Return a cached, non-expired, non-revoked lease that COVERS ``scope`` (the requested
256
+ constraints are within the lease's scope), else ``None``."""
257
+ lease = self._leases.get(capability)
258
+ if lease is None or not lease.is_valid(now) or self._is_revoked(lease):
259
+ return None
260
+ # The cached lease must be at least as broad as what this call needs.
261
+ if not scope_within(scope, lease.scope):
262
+ return None
263
+ return lease
264
+
265
+ def token_for(self, capability: str, *, now: float) -> str | None:
266
+ """The ``token_ref`` (access handle) of a currently-valid, non-revoked lease for
267
+ ``capability``, or ``None``. A tool handler reads this (via ``ToolContext.capability_token``)
268
+ to obtain the handle the gate acquired — the handle, never the secret; the edge resolves it.
269
+ Returns ``None`` once revoked: the caretaker has cleared its slot."""
270
+ lease = self._leases.get(capability)
271
+ if lease is None or not lease.is_valid(now) or self._is_revoked(lease):
272
+ return None
273
+ return lease.token_ref
274
+
275
+ def admit(self, request: CapabilityRequest, lease: CapabilityLease) -> CapabilityLease:
276
+ """Store a granted lease after enforcing least-privilege (grant scope ⊆ request scope).
277
+ Raises ``ValueError`` if the broker tried to widen scope or grant another capability."""
278
+ validate_lease_admission(request.capability, request.scope, lease.capability, lease.scope)
279
+ self._leases[lease.capability] = lease
280
+ return lease
281
+
282
+ def revoke(
283
+ self,
284
+ *,
285
+ capability: str | None = None,
286
+ lease_id: str | None = None,
287
+ before: float | None = None,
288
+ ) -> dict[str, Any]:
289
+ """Record a revocation and return a summary of what was revoked. Three granularities,
290
+ composable in one call:
291
+ - ``capability`` — block this capability for the run, authoritatively (the gate will not
292
+ re-broker it). The primary operator kill switch.
293
+ - ``lease_id`` — invalidate one specific grant (a compromised lease).
294
+ - ``before`` — a watermark: every lease issued before this epoch time is rejected in O(1)
295
+ (a bulk cohort kill).
296
+ Revocation is monotonic and additive — there is no un-revoke (start a fresh lease cohort)."""
297
+ return apply_capability_revocation(
298
+ self._revocations,
299
+ capability=capability,
300
+ lease_id=lease_id,
301
+ before=before,
302
+ )
303
+
304
+ def is_capability_revoked(self, capability: str) -> bool:
305
+ """True if this capability is under a per-capability revocation — the gate's hard stop that
306
+ refuses to even re-broker (so revocation cannot be undone by a permissive broker)."""
307
+ return is_capability_revoked(self._revocations, capability)
308
+
309
+ def export_durable(self) -> list[dict[str, Any]]:
310
+ """Serialize the leases marked ``durable`` (e.g. human/policy-approved) for the checkpoint.
311
+ Ephemeral sync grants are intentionally excluded — they re-broker on restart, so no handle
312
+ for them ever lands on disk. Expiry is re-checked on use, so an expired lease here is
313
+ harmless (it is filtered by ``get_valid`` after restore)."""
314
+ return [lease.to_json() for lease in self._leases.values() if lease.durable]
315
+
316
+ def export_revocations(self) -> dict[str, Any]:
317
+ """Serialize the revocation records for the checkpoint, so a revoked durable lease stays
318
+ dead across a restart (the kill switch must not be forgotten when the run resumes)."""
319
+ return export_revocation_state(self._revocations)
320
+
321
+ def fork_for_child(self) -> CapabilityVault:
322
+ """Create a child-run vault with isolated live lease slots and shared revocations.
323
+
324
+ Durable grants are copied into the child so approved access survives delegation, while
325
+ ephemeral live leases stay local to each run. Revocations share one state object so an
326
+ operator kill switch in the parent is immediately visible to already-running children.
327
+ """
328
+ child = CapabilityVault(_revocations=self._revocations)
329
+ for lease in self._leases.values():
330
+ if lease.durable:
331
+ child.install(lease)
332
+ return child
333
+
334
+ def import_revocations(
335
+ self,
336
+ *,
337
+ lease_ids: list[str] | None = None,
338
+ capabilities: list[str] | None = None,
339
+ before: float = 0.0,
340
+ all_revoked: bool = False,
341
+ ) -> None:
342
+ """Rehydrate revocation records on restore (paired with :meth:`export_revocations`)."""
343
+ import_revocation_state(
344
+ self._revocations,
345
+ lease_ids=lease_ids,
346
+ capabilities=capabilities,
347
+ before=before,
348
+ all_revoked=all_revoked,
349
+ )
350
+
351
+ def install(self, lease: CapabilityLease) -> None:
352
+ """Directly install a lease (no scope re-check) — used on restore to rehydrate durable
353
+ leases from a trusted checkpoint. The lease was already scope-checked at grant time."""
354
+ self._leases[lease.capability] = lease
355
+
356
+
357
+ @dataclass
358
+ class AutoGrantBroker:
359
+ """The zero-config default broker: grants any request, scoped exactly to what was asked,
360
+ with a fixed TTL. Intended for local development and tests — NOT for production (it applies
361
+ no policy). ``token_ref`` is a non-secret placeholder."""
362
+
363
+ ttl_seconds: int = 600
364
+ now: Any = None # optional injectable clock for tests: a callable() -> float
365
+
366
+ def request(self, req: CapabilityRequest) -> CapabilityGrant:
367
+ import time
368
+
369
+ clock = self.now if callable(self.now) else time.time
370
+ ttl = req.ttl_seconds or self.ttl_seconds
371
+ return CapabilityLease(
372
+ capability=req.capability,
373
+ token_ref=f"auto:{req.capability}",
374
+ expires_at=clock() + ttl,
375
+ scope=dict(req.scope),
376
+ )
@@ -0,0 +1,92 @@
1
+ """Capability revocation helpers shared by vaults and conformance tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Protocol
7
+
8
+
9
+ @dataclass
10
+ class CapabilityRevocationState:
11
+ lease_ids: set[str] = field(default_factory=set)
12
+ capabilities: set[str] = field(default_factory=set)
13
+ before: float = 0.0
14
+ all_revoked: bool = False
15
+
16
+
17
+ class RevocableLease(Protocol):
18
+ lease_id: str
19
+ capability: str
20
+ issued_at: float
21
+
22
+
23
+ def apply_capability_revocation(
24
+ state: CapabilityRevocationState,
25
+ *,
26
+ capability: str | None = None,
27
+ lease_id: str | None = None,
28
+ before: float | None = None,
29
+ ) -> dict[str, object]:
30
+ """Record a monotonic revocation and return the public summary."""
31
+ revoked_caps: list[str] = ["*"] if capability == "*" else []
32
+ if capability and capability != "*":
33
+ state.capabilities.add(capability)
34
+ revoked_caps = [capability]
35
+ elif capability == "*":
36
+ state.all_revoked = True
37
+ if lease_id:
38
+ state.lease_ids.add(lease_id)
39
+ if before is not None:
40
+ state.before = max(state.before, before)
41
+ return {
42
+ "capabilities": revoked_caps,
43
+ "lease_id": lease_id or "",
44
+ "revoked_before": state.before,
45
+ }
46
+
47
+
48
+ def is_lease_revoked(state: CapabilityRevocationState, lease: RevocableLease) -> bool:
49
+ """Return true when a concrete lease is covered by revocation state."""
50
+ return (
51
+ state.all_revoked
52
+ or lease.lease_id in state.lease_ids
53
+ or lease.capability in state.capabilities
54
+ or lease.issued_at < state.before
55
+ )
56
+
57
+
58
+ def is_capability_revoked(state: CapabilityRevocationState, capability: str) -> bool:
59
+ """Return true when a capability is under an authoritative re-broker stop."""
60
+ return state.all_revoked or capability in state.capabilities
61
+
62
+
63
+ def export_revocation_state(state: CapabilityRevocationState) -> dict[str, object]:
64
+ """Serialize revocation records for checkpoint/conformance transfer."""
65
+ capabilities = sorted(state.capabilities)
66
+ if state.all_revoked and "*" not in capabilities:
67
+ capabilities = ["*", *capabilities]
68
+ return {
69
+ "revoked_lease_ids": sorted(state.lease_ids),
70
+ "revoked_capabilities": capabilities,
71
+ "revoked_before": state.before,
72
+ "revoked_all": state.all_revoked,
73
+ }
74
+
75
+
76
+ def import_revocation_state(
77
+ state: CapabilityRevocationState,
78
+ *,
79
+ lease_ids: list[str] | None = None,
80
+ capabilities: list[str] | None = None,
81
+ before: float = 0.0,
82
+ all_revoked: bool = False,
83
+ ) -> None:
84
+ """Merge serialized revocation records into an existing state."""
85
+ state.lease_ids.update(lease_ids or ())
86
+ imported_capabilities = set(capabilities or ())
87
+ if "*" in imported_capabilities:
88
+ all_revoked = True
89
+ imported_capabilities.discard("*")
90
+ state.capabilities.update(imported_capabilities)
91
+ state.before = max(state.before, before)
92
+ state.all_revoked = state.all_revoked or all_revoked