unitares-sdk 0.1.0__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.
- unitares_sdk/__init__.py +78 -0
- unitares_sdk/_substrate.py +223 -0
- unitares_sdk/agent.py +651 -0
- unitares_sdk/client.py +811 -0
- unitares_sdk/errors.py +168 -0
- unitares_sdk/lease_plane/__init__.py +80 -0
- unitares_sdk/lease_plane/advisory.py +479 -0
- unitares_sdk/lease_plane/canonical.py +121 -0
- unitares_sdk/lease_plane/canonicalize.py +220 -0
- unitares_sdk/lease_plane/client.py +806 -0
- unitares_sdk/lease_plane/models.py +369 -0
- unitares_sdk/lease_plane/reclaim.py +147 -0
- unitares_sdk/models.py +185 -0
- unitares_sdk/py.typed +0 -0
- unitares_sdk/sync_client.py +542 -0
- unitares_sdk/utils.py +321 -0
- unitares_sdk-0.1.0.dist-info/METADATA +196 -0
- unitares_sdk-0.1.0.dist-info/RECORD +21 -0
- unitares_sdk-0.1.0.dist-info/WHEEL +5 -0
- unitares_sdk-0.1.0.dist-info/licenses/LICENSE +190 -0
- unitares_sdk-0.1.0.dist-info/top_level.txt +1 -0
unitares_sdk/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""UNITARES Agent SDK — typed client and lifecycle base class for governance agents."""
|
|
2
|
+
|
|
3
|
+
from unitares_sdk.errors import (
|
|
4
|
+
GovernanceConnectionError,
|
|
5
|
+
GovernanceError,
|
|
6
|
+
GovernanceTimeoutError,
|
|
7
|
+
IdentityDriftError,
|
|
8
|
+
VerdictError,
|
|
9
|
+
)
|
|
10
|
+
from unitares_sdk.models import (
|
|
11
|
+
ArchiveResult,
|
|
12
|
+
AuditResult,
|
|
13
|
+
CheckinResult,
|
|
14
|
+
CleanupResult,
|
|
15
|
+
IdentityResult,
|
|
16
|
+
InferenceHost,
|
|
17
|
+
InferenceHostResult,
|
|
18
|
+
InferenceHostsResult,
|
|
19
|
+
InferenceProvenance,
|
|
20
|
+
MetricsResult,
|
|
21
|
+
ModelResult,
|
|
22
|
+
NoteResult,
|
|
23
|
+
OnboardResult,
|
|
24
|
+
RecoveryResult,
|
|
25
|
+
SearchResult,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Clients (imported lazily by consumers)
|
|
30
|
+
"GovernanceClient",
|
|
31
|
+
"SyncGovernanceClient",
|
|
32
|
+
# Agent base class
|
|
33
|
+
"GovernanceAgent",
|
|
34
|
+
"CycleResult",
|
|
35
|
+
# Models
|
|
36
|
+
"ArchiveResult",
|
|
37
|
+
"AuditResult",
|
|
38
|
+
"CheckinResult",
|
|
39
|
+
"CleanupResult",
|
|
40
|
+
"IdentityResult",
|
|
41
|
+
"InferenceHost",
|
|
42
|
+
"InferenceHostResult",
|
|
43
|
+
"InferenceHostsResult",
|
|
44
|
+
"InferenceProvenance",
|
|
45
|
+
"MetricsResult",
|
|
46
|
+
"ModelResult",
|
|
47
|
+
"NoteResult",
|
|
48
|
+
"OnboardResult",
|
|
49
|
+
"RecoveryResult",
|
|
50
|
+
"SearchResult",
|
|
51
|
+
# Errors
|
|
52
|
+
"GovernanceError",
|
|
53
|
+
"GovernanceConnectionError",
|
|
54
|
+
"GovernanceTimeoutError",
|
|
55
|
+
"IdentityDriftError",
|
|
56
|
+
"VerdictError",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __getattr__(name: str):
|
|
61
|
+
"""Lazy imports for heavier modules to keep initial import fast."""
|
|
62
|
+
if name == "GovernanceClient":
|
|
63
|
+
from unitares_sdk.client import GovernanceClient
|
|
64
|
+
|
|
65
|
+
return GovernanceClient
|
|
66
|
+
if name == "SyncGovernanceClient":
|
|
67
|
+
from unitares_sdk.sync_client import SyncGovernanceClient
|
|
68
|
+
|
|
69
|
+
return SyncGovernanceClient
|
|
70
|
+
if name == "GovernanceAgent":
|
|
71
|
+
from unitares_sdk.agent import GovernanceAgent
|
|
72
|
+
|
|
73
|
+
return GovernanceAgent
|
|
74
|
+
if name == "CycleResult":
|
|
75
|
+
from unitares_sdk.agent import CycleResult
|
|
76
|
+
|
|
77
|
+
return CycleResult
|
|
78
|
+
raise AttributeError(f"module 'unitares_sdk' has no attribute {name!r}")
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Lease-plane substrate-state emission for resident agents (RFC §7.13).
|
|
2
|
+
|
|
3
|
+
Used by `unitares_sdk.client.UnitaresClient.checkin()` to emit each resident's
|
|
4
|
+
post-checkin EISV onto `lease_plane.surface_leases.substrate_state`. Per RFC
|
|
5
|
+
§7.13.4 dual-run authority, the emission is observational-only until each
|
|
6
|
+
resident's individual canary completes — `audit.events` (gated by PR 8's
|
|
7
|
+
`AUDIT_EISV_SYNC_ENABLED_RESIDENTS` env var) remains authoritative until the
|
|
8
|
+
operator removes that resident's name from the env var.
|
|
9
|
+
|
|
10
|
+
Net-new write path; does NOT replace `process_agent_update`. Failures are
|
|
11
|
+
swallowed to keep the checkin contract — RFC §7.13.4 contract: lease-plane
|
|
12
|
+
failures MUST NOT fail the resident's checkin loop.
|
|
13
|
+
|
|
14
|
+
Design:
|
|
15
|
+
- `KNOWN_RESIDENT_NAMES` is the deployment's resident roster, read from the
|
|
16
|
+
`UNITARES_RESIDENTS` env var (comma-separated labels). It mirrors
|
|
17
|
+
`src/grounding/class_indicator.py::KNOWN_RESIDENT_LABELS`, which reads the
|
|
18
|
+
same env var. The env var NAME is the cross-package contract — the SDK is
|
|
19
|
+
a standalone package that must not import from `src/`. Unset/empty means
|
|
20
|
+
this install has no named residents (the user-agnostic default).
|
|
21
|
+
- Emission is GATED on `name in KNOWN_RESIDENT_NAMES` — non-resident agents
|
|
22
|
+
using the SDK don't emit (the migration-034 `substrate_state_only_on_resident_kind`
|
|
23
|
+
CHECK would reject them at the DB anyway; gating client-side is friendlier).
|
|
24
|
+
- Lease handle (`_LeaseCache`) is per-client-instance (one per resident loop).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
from datetime import UTC, datetime
|
|
32
|
+
from typing import Any
|
|
33
|
+
from uuid import UUID
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Deployment resident roster, read from UNITARES_RESIDENTS (comma-separated
|
|
38
|
+
# labels). Mirrors src/grounding/class_indicator.py::KNOWN_RESIDENT_LABELS,
|
|
39
|
+
# which reads the same env var — the env var name is the cross-package
|
|
40
|
+
# contract. Empty by default (user-agnostic install with no named residents).
|
|
41
|
+
RESIDENT_ROSTER_ENV = "UNITARES_RESIDENTS"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_resident_roster(raw: str | None) -> frozenset[str]:
|
|
45
|
+
"""Parse a comma-separated resident roster string into a label set."""
|
|
46
|
+
if not raw:
|
|
47
|
+
return frozenset()
|
|
48
|
+
return frozenset(part.strip() for part in raw.split(",") if part.strip())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
KNOWN_RESIDENT_NAMES: frozenset[str] = parse_resident_roster(
|
|
52
|
+
os.environ.get(RESIDENT_ROSTER_ENV)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _LeaseCache:
|
|
57
|
+
"""Per-client cached lease handle for substrate emission.
|
|
58
|
+
|
|
59
|
+
First emission triggers acquire (idempotent on (surface_id, holder_uuid)
|
|
60
|
+
per Repo.acquire — restart re-uses the existing lease row). Subsequent
|
|
61
|
+
emissions trigger renew. Renew failure clears cache so next emission
|
|
62
|
+
re-acquires.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
self.lease_id: UUID | None = None
|
|
67
|
+
|
|
68
|
+
def reset(self) -> None:
|
|
69
|
+
self.lease_id = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resolve_resident_name(raw_name: str) -> str | None:
|
|
73
|
+
"""Match `raw_name` (case-sensitive) against KNOWN_RESIDENT_NAMES.
|
|
74
|
+
|
|
75
|
+
Residents are constructed with the SDK's `onboard(name=...)` and the
|
|
76
|
+
name string lands in `core.identities`. The casing convention is
|
|
77
|
+
capitalized labels (Vigil, Sentinel, etc.) per CLAUDE.md identity
|
|
78
|
+
rules. Returns None for non-resident or unrecognized names.
|
|
79
|
+
"""
|
|
80
|
+
if not raw_name:
|
|
81
|
+
return None
|
|
82
|
+
if raw_name in KNOWN_RESIDENT_NAMES:
|
|
83
|
+
return raw_name
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_substrate_state(metrics: dict[str, Any]) -> dict[str, Any]:
|
|
88
|
+
"""Project a checkin response's metrics into the §7.13.1.2 shape.
|
|
89
|
+
|
|
90
|
+
Pulls E/I/S/V from the metrics object (set by process_agent_update on
|
|
91
|
+
the server side). Sensor status defaults to 'healthy' — we only reach
|
|
92
|
+
this code path when checkin succeeded, so by the SDK's reckoning the
|
|
93
|
+
resident's substrate observation is intact. Future iterations may
|
|
94
|
+
inspect metrics for NaN / out-of-range / staleness and emit 'degraded'
|
|
95
|
+
or 'failed' with a `reason` field per §7.13.1.2.
|
|
96
|
+
"""
|
|
97
|
+
return {
|
|
98
|
+
"E": float(metrics.get("E", 0.0)),
|
|
99
|
+
"I": float(metrics.get("I", 0.0)),
|
|
100
|
+
"S": float(metrics.get("S", 0.0)),
|
|
101
|
+
"V": float(metrics.get("V", 0.0)),
|
|
102
|
+
"sensor": {"status": "healthy"},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _make_client():
|
|
107
|
+
"""Build a lease-plane client lazily.
|
|
108
|
+
|
|
109
|
+
Imported lazily to keep module load light. The lease-plane client is
|
|
110
|
+
part of this SDK (``unitares_sdk.lease_plane``); emission is skipped
|
|
111
|
+
silently when no bearer token is configured.
|
|
112
|
+
"""
|
|
113
|
+
from unitares_sdk.lease_plane.client import (
|
|
114
|
+
LeasePlaneClient,
|
|
115
|
+
LeasePlaneClientConfig,
|
|
116
|
+
LeasePlaneDisabledClient,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
token = os.environ.get("LEASE_PLANE_BEARER_TOKEN", "").strip()
|
|
120
|
+
base_url = os.environ.get(
|
|
121
|
+
"LEASE_PLANE_BASE_URL", "http://127.0.0.1:8788"
|
|
122
|
+
).strip()
|
|
123
|
+
|
|
124
|
+
if not token:
|
|
125
|
+
return LeasePlaneDisabledClient()
|
|
126
|
+
|
|
127
|
+
return LeasePlaneClient(
|
|
128
|
+
LeasePlaneClientConfig(
|
|
129
|
+
base_url=base_url,
|
|
130
|
+
bearer_token=token,
|
|
131
|
+
timeout_s=2.0,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def emit_substrate_observation(
|
|
137
|
+
*,
|
|
138
|
+
resident_name: str,
|
|
139
|
+
holder_uuid: str,
|
|
140
|
+
metrics: dict[str, Any],
|
|
141
|
+
cache: _LeaseCache,
|
|
142
|
+
client=None,
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Emit one substrate observation for a resident agent.
|
|
145
|
+
|
|
146
|
+
Returns True iff the lease-plane call succeeded. Caller MUST ignore the
|
|
147
|
+
return value for checkin contract — this is observational-only per
|
|
148
|
+
RFC §7.13.4. Skips silently if `resident_name` isn't a known resident
|
|
149
|
+
(non-resident agents shouldn't write `resident:/` surfaces).
|
|
150
|
+
"""
|
|
151
|
+
matched = _resolve_resident_name(resident_name)
|
|
152
|
+
if matched is None:
|
|
153
|
+
return False
|
|
154
|
+
if not holder_uuid:
|
|
155
|
+
return False
|
|
156
|
+
if not metrics:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
if client is None:
|
|
160
|
+
client = _make_client()
|
|
161
|
+
if client is None:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
surface_id = f"resident:/{matched.lower()}"
|
|
165
|
+
substrate_state = _build_substrate_state(metrics)
|
|
166
|
+
observed_at = datetime.now(UTC)
|
|
167
|
+
|
|
168
|
+
if cache.lease_id is None:
|
|
169
|
+
from unitares_sdk.lease_plane.models import AcquireOk, AcquireRequest
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
request = AcquireRequest(
|
|
173
|
+
surface_id=surface_id,
|
|
174
|
+
holder_agent_uuid=UUID(holder_uuid),
|
|
175
|
+
holder_class="substrate_earned",
|
|
176
|
+
holder_kind="remote_heartbeat",
|
|
177
|
+
ttl_s=1000, # §7.5 v0.9 measurement: p99 × 1.5 rounded
|
|
178
|
+
intent=f"{matched} resident heartbeat (RFC §7.13)",
|
|
179
|
+
substrate_state=substrate_state,
|
|
180
|
+
substrate_state_observed_at=observed_at,
|
|
181
|
+
)
|
|
182
|
+
result = client.acquire(request)
|
|
183
|
+
except Exception as exc: # noqa: BLE001 — observational-only by contract
|
|
184
|
+
logger.debug("[substrate] %s acquire raised: %r", matched, exc)
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
if isinstance(result, AcquireOk):
|
|
188
|
+
cache.lease_id = result.lease.lease_id
|
|
189
|
+
logger.info(
|
|
190
|
+
"[substrate] %s acquired lease_id=%s (idempotent=%s)",
|
|
191
|
+
matched,
|
|
192
|
+
cache.lease_id,
|
|
193
|
+
result.idempotent,
|
|
194
|
+
)
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
logger.debug("[substrate] %s acquire non-OK: %r", matched, type(result).__name__)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
from unitares_sdk.lease_plane.models import RenewRequest, SimpleOk
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
renew_request = RenewRequest(
|
|
204
|
+
lease_id=cache.lease_id,
|
|
205
|
+
substrate_state=substrate_state,
|
|
206
|
+
substrate_state_observed_at=observed_at,
|
|
207
|
+
)
|
|
208
|
+
result = client.renew(renew_request)
|
|
209
|
+
except Exception as exc: # noqa: BLE001
|
|
210
|
+
logger.debug("[substrate] %s renew raised: %r", matched, exc)
|
|
211
|
+
cache.reset()
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
if isinstance(result, SimpleOk):
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
logger.debug(
|
|
218
|
+
"[substrate] %s renew non-OK (%r); resetting for re-acquire",
|
|
219
|
+
matched,
|
|
220
|
+
type(result).__name__,
|
|
221
|
+
)
|
|
222
|
+
cache.reset()
|
|
223
|
+
return False
|