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.
- monoid_agent_kernel/__init__.py +11 -0
- monoid_agent_kernel/_policy_util.py +40 -0
- monoid_agent_kernel/_proc.py +86 -0
- monoid_agent_kernel/builder.py +351 -0
- monoid_agent_kernel/cli.py +1244 -0
- monoid_agent_kernel/conformance/__init__.py +32 -0
- monoid_agent_kernel/conformance/harness.py +209 -0
- monoid_agent_kernel/conformance/profiles/__init__.py +55 -0
- monoid_agent_kernel/conformance/profiles/_metadata.py +16 -0
- monoid_agent_kernel/conformance/profiles/capability_security.py +216 -0
- monoid_agent_kernel/conformance/profiles/control_plane.py +41 -0
- monoid_agent_kernel/conformance/profiles/durable_runner.py +42 -0
- monoid_agent_kernel/conformance/profiles/message_fabric.py +42 -0
- monoid_agent_kernel/conformance/profiles/minimal_agent.py +13 -0
- monoid_agent_kernel/conformance/profiles/multi_agent.py +112 -0
- monoid_agent_kernel/conformance/profiles/provider_gateway.py +107 -0
- monoid_agent_kernel/conformance/profiles/reference_full.py +135 -0
- monoid_agent_kernel/conformance/profiles/side_effect_tool_agent.py +36 -0
- monoid_agent_kernel/conformance/profiles/tool_agent.py +39 -0
- monoid_agent_kernel/contracts.py +329 -0
- monoid_agent_kernel/core/__init__.py +1 -0
- monoid_agent_kernel/core/_util.py +145 -0
- monoid_agent_kernel/core/agents.py +883 -0
- monoid_agent_kernel/core/cancellation.py +16 -0
- monoid_agent_kernel/core/capability.py +376 -0
- monoid_agent_kernel/core/capability_revocation.py +92 -0
- monoid_agent_kernel/core/checkpoint.py +350 -0
- monoid_agent_kernel/core/content.py +101 -0
- monoid_agent_kernel/core/context.py +67 -0
- monoid_agent_kernel/core/control.py +124 -0
- monoid_agent_kernel/core/control_audit.py +99 -0
- monoid_agent_kernel/core/durable_metadata.py +116 -0
- monoid_agent_kernel/core/event_sequencing.py +164 -0
- monoid_agent_kernel/core/events.py +204 -0
- monoid_agent_kernel/core/external_agent_envelope.py +338 -0
- monoid_agent_kernel/core/frontmatter.py +140 -0
- monoid_agent_kernel/core/inbox.py +110 -0
- monoid_agent_kernel/core/lease_admission.py +57 -0
- monoid_agent_kernel/core/lifecycle.py +440 -0
- monoid_agent_kernel/core/manifest.py +88 -0
- monoid_agent_kernel/core/media.py +393 -0
- monoid_agent_kernel/core/outbox.py +212 -0
- monoid_agent_kernel/core/output_validator.py +103 -0
- monoid_agent_kernel/core/packages.py +742 -0
- monoid_agent_kernel/core/projections.py +213 -0
- monoid_agent_kernel/core/prompt.py +37 -0
- monoid_agent_kernel/core/proposal_file.py +84 -0
- monoid_agent_kernel/core/result.py +131 -0
- monoid_agent_kernel/core/schemas.py +1146 -0
- monoid_agent_kernel/core/scope.py +185 -0
- monoid_agent_kernel/core/side_effect_policy.py +153 -0
- monoid_agent_kernel/core/spec.py +325 -0
- monoid_agent_kernel/core/streaming.py +208 -0
- monoid_agent_kernel/core/subagent_runtime.py +187 -0
- monoid_agent_kernel/core/tool_approval.py +175 -0
- monoid_agent_kernel/core/tool_surface.py +505 -0
- monoid_agent_kernel/core/trace_context.py +76 -0
- monoid_agent_kernel/core/wire_validation.py +156 -0
- monoid_agent_kernel/core/workspace.py +161 -0
- monoid_agent_kernel/core/workspace_index.py +127 -0
- monoid_agent_kernel/env.py +32 -0
- monoid_agent_kernel/errors.py +105 -0
- monoid_agent_kernel/event_loader.py +47 -0
- monoid_agent_kernel/identifiers.py +50 -0
- monoid_agent_kernel/loop.py +4140 -0
- monoid_agent_kernel/loop_phases.py +561 -0
- monoid_agent_kernel/mcp/__init__.py +10 -0
- monoid_agent_kernel/mcp/client.py +227 -0
- monoid_agent_kernel/mcp/provider.py +407 -0
- monoid_agent_kernel/narration.py +92 -0
- monoid_agent_kernel/observability/__init__.py +9 -0
- monoid_agent_kernel/observability/otel.py +264 -0
- monoid_agent_kernel/permissions.py +74 -0
- monoid_agent_kernel/providers/__init__.py +7 -0
- monoid_agent_kernel/providers/_common.py +122 -0
- monoid_agent_kernel/providers/base.py +312 -0
- monoid_agent_kernel/providers/fake.py +76 -0
- monoid_agent_kernel/providers/gateway.py +474 -0
- monoid_agent_kernel/providers/openai.py +487 -0
- monoid_agent_kernel/public_view.py +173 -0
- monoid_agent_kernel/py.typed +0 -0
- monoid_agent_kernel/recorder.py +421 -0
- monoid_agent_kernel/reference/__init__.py +15 -0
- monoid_agent_kernel/reference/_shared/__init__.py +4 -0
- monoid_agent_kernel/reference/_shared/http_util.py +111 -0
- monoid_agent_kernel/reference/_shared/tokens.py +314 -0
- monoid_agent_kernel/reference/backend/__init__.py +21 -0
- monoid_agent_kernel/reference/backend/commands.py +375 -0
- monoid_agent_kernel/reference/backend/http.py +439 -0
- monoid_agent_kernel/reference/backend/jobs.py +68 -0
- monoid_agent_kernel/reference/backend/loop_factory.py +253 -0
- monoid_agent_kernel/reference/backend/outbox_dispatch.py +130 -0
- monoid_agent_kernel/reference/backend/ports.py +188 -0
- monoid_agent_kernel/reference/backend/projection.py +286 -0
- monoid_agent_kernel/reference/backend/proposal.py +220 -0
- monoid_agent_kernel/reference/backend/proposal_reader.py +16 -0
- monoid_agent_kernel/reference/backend/recovery.py +253 -0
- monoid_agent_kernel/reference/backend/run_execution.py +152 -0
- monoid_agent_kernel/reference/backend/run_preparation.py +197 -0
- monoid_agent_kernel/reference/backend/run_state.py +243 -0
- monoid_agent_kernel/reference/backend/run_types.py +128 -0
- monoid_agent_kernel/reference/backend/runtime_config.py +147 -0
- monoid_agent_kernel/reference/backend/service.py +1664 -0
- monoid_agent_kernel/reference/backend/session.py +280 -0
- monoid_agent_kernel/reference/backend/session_drive.py +231 -0
- monoid_agent_kernel/reference/capability.py +101 -0
- monoid_agent_kernel/reference/conformance.py +1777 -0
- monoid_agent_kernel/reference/llm_gateway/__init__.py +14 -0
- monoid_agent_kernel/reference/llm_gateway/http.py +225 -0
- monoid_agent_kernel/reference/llm_gateway/providers.py +96 -0
- monoid_agent_kernel/reference/llm_gateway/service.py +404 -0
- monoid_agent_kernel/reference/mcp_gateway/__init__.py +26 -0
- monoid_agent_kernel/reference/mcp_gateway/http.py +187 -0
- monoid_agent_kernel/reference/mcp_gateway/service.py +206 -0
- monoid_agent_kernel/reference/outbox.py +135 -0
- monoid_agent_kernel/reference/stores/__init__.py +20 -0
- monoid_agent_kernel/reference/stores/lease.py +116 -0
- monoid_agent_kernel/reference/stores/sqlite.py +295 -0
- monoid_agent_kernel/reference/studio/README.md +97 -0
- monoid_agent_kernel/reference/studio/__init__.py +21 -0
- monoid_agent_kernel/reference/studio/activity.py +53 -0
- monoid_agent_kernel/reference/studio/cli.py +481 -0
- monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/SKILL.md +25 -0
- monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/references/review-checklist.md +8 -0
- monoid_agent_kernel/reference/studio/sample-skills/commit-message/SKILL.md +32 -0
- monoid_agent_kernel/reference/studio/sample-skills/incident-summary/SKILL.md +24 -0
- monoid_agent_kernel/reference/studio/sample-skills/incident-summary/references/incident-template.md +32 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/SKILL.md +24 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/references/release-note-template.md +33 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/scripts/collect_changes.py +51 -0
- monoid_agent_kernel/reference/studio/server.py +1922 -0
- monoid_agent_kernel/reference/studio/web/index.html +1877 -0
- monoid_agent_kernel/reference/studio/web/settings.html +108 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/auto-render.min.js +1 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.css +1 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.js +1 -0
- monoid_agent_kernel/reference/studio/window.py +72 -0
- monoid_agent_kernel/reference/web_gateway/__init__.py +6 -0
- monoid_agent_kernel/reference/web_gateway/http.py +155 -0
- monoid_agent_kernel/reference/web_gateway/providers.py +807 -0
- monoid_agent_kernel/reference/web_gateway/service.py +559 -0
- monoid_agent_kernel/shell.py +722 -0
- monoid_agent_kernel/skills/__init__.py +20 -0
- monoid_agent_kernel/skills/definition.py +82 -0
- monoid_agent_kernel/skills/loader.py +55 -0
- monoid_agent_kernel/skills/provider.py +378 -0
- monoid_agent_kernel/subagent_loader.py +56 -0
- monoid_agent_kernel/tasks.py +1326 -0
- monoid_agent_kernel/tool_loader.py +51 -0
- monoid_agent_kernel/tool_services/__init__.py +8 -0
- monoid_agent_kernel/tool_services/base.py +25 -0
- monoid_agent_kernel/tool_services/jobs.py +47 -0
- monoid_agent_kernel/tool_services/shell.py +255 -0
- monoid_agent_kernel/tool_services/web.py +356 -0
- monoid_agent_kernel/tools/__init__.py +2 -0
- monoid_agent_kernel/tools/base.py +205 -0
- monoid_agent_kernel/tools/builtin.py +958 -0
- monoid_agent_kernel/tools/decorator.py +132 -0
- monoid_agent_kernel/tools/tool_ids.py +62 -0
- monoid_agent_kernel/web.py +171 -0
- monoid_agent_kernel/workspace/__init__.py +2 -0
- monoid_agent_kernel/workspace/local.py +1004 -0
- monoid_agent_kernel/workspace/paths.py +30 -0
- monoid_agent_kernel-0.16.1.dist-info/METADATA +709 -0
- monoid_agent_kernel-0.16.1.dist-info/RECORD +191 -0
- monoid_agent_kernel-0.16.1.dist-info/WHEEL +4 -0
- monoid_agent_kernel-0.16.1.dist-info/entry_points.txt +3 -0
- monoid_agent_kernel-0.16.1.dist-info/licenses/LICENSE +202 -0
- monoid_agent_kernel-0.16.1.dist-info/licenses/NOTICE +8 -0
- native_agent_runner/__init__.py +103 -0
- 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
|