mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-27 01:22:09 +00:00
345 lines
12 KiB
Python
345 lines
12 KiB
Python
"""Capability-authenticated client for the Electron local secret broker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
from collections.abc import MutableMapping, Sequence
|
|
from dataclasses import dataclass, field
|
|
from http.client import HTTPConnection, HTTPException
|
|
from typing import Literal
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
def _capture_broker_environment(
|
|
environment: MutableMapping[str, str],
|
|
) -> tuple[str, str]:
|
|
"""Take broker authority out of the environment inherited by agents."""
|
|
|
|
endpoint = environment.pop("EIGENT_WORKSPACE_SECRET_BROKER_ENDPOINT", "")
|
|
capability = environment.pop(
|
|
"EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY", ""
|
|
)
|
|
# Strip any obsolete or vendor-specific broker authority as well. Only the
|
|
# Workspace keys above are accepted as the active authority, but no broker
|
|
# credential may leak into an agent child process.
|
|
for key in tuple(environment):
|
|
if re.fullmatch(
|
|
r"EIGENT_[A-Z0-9_]+_SECRET_BROKER_(?:ENDPOINT|CAPABILITY)",
|
|
key.strip().upper(),
|
|
):
|
|
environment.pop(key, None)
|
|
return endpoint, capability
|
|
|
|
|
|
_BROKER_ENDPOINT, _BROKER_CAPABILITY = _capture_broker_environment(os.environ)
|
|
|
|
|
|
class WorkspaceSecretBrokerError(RuntimeError):
|
|
"""Raised when a local-only Bundle binding cannot be verified."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorkspaceSecretIdentity:
|
|
secret_ref: str
|
|
account_scope_digest: str
|
|
space_id: str
|
|
revision_id: str
|
|
slot_id: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorkspaceSecretVerification:
|
|
identity: WorkspaceSecretIdentity
|
|
state: Literal["available", "missing", "needs_rebind"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorkspaceSecretResolution:
|
|
"""One in-memory secret value resolved for its exact binding identity."""
|
|
|
|
identity: WorkspaceSecretIdentity
|
|
value: str = field(repr=False)
|
|
|
|
|
|
class WorkspaceSecretBroker:
|
|
"""Verify or resolve exact bindings through Electron's private channel."""
|
|
|
|
MAX_BATCH_BINDINGS = 100
|
|
MAX_REQUEST_BYTES = 16 * 1024
|
|
MAX_RESPONSE_BYTES = 64 * 1024
|
|
MAX_RESOLVE_RESPONSE_BYTES = 8 * 1024 * 1024
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
endpoint: str,
|
|
capability: str,
|
|
timeout_seconds: float = 3.0,
|
|
) -> None:
|
|
try:
|
|
parsed = urlsplit(endpoint)
|
|
except ValueError as exc:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker endpoint is invalid"
|
|
) from exc
|
|
if (
|
|
parsed.scheme != "http"
|
|
or parsed.hostname != "127.0.0.1"
|
|
or parsed.username is not None
|
|
or parsed.password is not None
|
|
or parsed.path not in {"", "/"}
|
|
or parsed.query
|
|
or parsed.fragment
|
|
):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker must use a loopback endpoint"
|
|
)
|
|
try:
|
|
port = parsed.port
|
|
except ValueError as exc:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker endpoint is invalid"
|
|
) from exc
|
|
if (
|
|
port is None
|
|
or not 1 <= port <= 65535
|
|
or re.fullmatch(r"[A-Za-z0-9_-]{32,256}", capability) is None
|
|
or not 0 < timeout_seconds <= 30
|
|
):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker configuration is invalid"
|
|
)
|
|
self._port = port
|
|
self._capability = capability
|
|
self._timeout_seconds = timeout_seconds
|
|
|
|
@classmethod
|
|
def from_environment(cls) -> WorkspaceSecretBroker:
|
|
if not _BROKER_ENDPOINT or not _BROKER_CAPABILITY:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker is unavailable"
|
|
)
|
|
return cls(
|
|
endpoint=_BROKER_ENDPOINT,
|
|
capability=_BROKER_CAPABILITY,
|
|
)
|
|
|
|
def verify(self, identity: WorkspaceSecretIdentity) -> None:
|
|
verification = self.verify_many((identity,))[0]
|
|
if verification.state != "available":
|
|
raise WorkspaceSecretBrokerError("Workspace secret is unavailable")
|
|
|
|
def verify_many(
|
|
self,
|
|
identities: Sequence[WorkspaceSecretIdentity],
|
|
) -> tuple[WorkspaceSecretVerification, ...]:
|
|
requested = tuple(identities)
|
|
if not requested:
|
|
return ()
|
|
if len(requested) > self.MAX_BATCH_BINDINGS:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret verification batch is too large"
|
|
)
|
|
response = self._request(
|
|
"/v1/workspace-secrets/verify-batch",
|
|
{
|
|
"bindings": [
|
|
self._identity_payload(identity) for identity in requested
|
|
]
|
|
},
|
|
)
|
|
if set(response) != {"statuses"}:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid verification"
|
|
)
|
|
statuses = response.get("statuses")
|
|
if not isinstance(statuses, list) or len(statuses) != len(requested):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid verification"
|
|
)
|
|
allowed_status_keys = {
|
|
"secret_ref",
|
|
"account_scope_digest",
|
|
"space_id",
|
|
"revision_id",
|
|
"slot_id",
|
|
"state",
|
|
"created_at",
|
|
"updated_at",
|
|
}
|
|
result: list[WorkspaceSecretVerification] = []
|
|
for identity, status in zip(requested, statuses, strict=True):
|
|
expected = self._identity_payload(identity)
|
|
if (
|
|
not isinstance(status, dict)
|
|
or not set(status).issubset(allowed_status_keys)
|
|
or any(
|
|
status.get(key) != value for key, value in expected.items()
|
|
)
|
|
or status.get("state")
|
|
not in {"available", "missing", "needs_rebind"}
|
|
):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid verification"
|
|
)
|
|
result.append(
|
|
WorkspaceSecretVerification(
|
|
identity=identity,
|
|
state=status["state"],
|
|
)
|
|
)
|
|
return tuple(result)
|
|
|
|
def resolve_many(
|
|
self,
|
|
identities: Sequence[WorkspaceSecretIdentity],
|
|
) -> tuple[WorkspaceSecretResolution, ...]:
|
|
"""Resolve values in memory without persisting or logging them.
|
|
|
|
Electron binds every value to the complete secret reference and
|
|
account/Space/revision/slot tuple. Any missing, stale, or mismatched
|
|
binding fails the entire batch closed.
|
|
"""
|
|
|
|
requested = tuple(identities)
|
|
if not requested:
|
|
return ()
|
|
if len(requested) > self.MAX_BATCH_BINDINGS:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret resolution batch is too large"
|
|
)
|
|
response = self._request(
|
|
"/v1/workspace-secrets/resolve-batch",
|
|
{
|
|
"bindings": [
|
|
self._identity_payload(identity) for identity in requested
|
|
]
|
|
},
|
|
max_response_bytes=self.MAX_RESOLVE_RESPONSE_BYTES,
|
|
)
|
|
if set(response) != {"resolutions"}:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid resolution"
|
|
)
|
|
resolutions = response.get("resolutions")
|
|
if not isinstance(resolutions, list) or len(resolutions) != len(
|
|
requested
|
|
):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid resolution"
|
|
)
|
|
allowed_keys = {
|
|
"secret_ref",
|
|
"account_scope_digest",
|
|
"space_id",
|
|
"revision_id",
|
|
"slot_id",
|
|
"value",
|
|
}
|
|
result: list[WorkspaceSecretResolution] = []
|
|
for identity, resolution in zip(requested, resolutions, strict=True):
|
|
expected = self._identity_payload(identity)
|
|
if (
|
|
not isinstance(resolution, dict)
|
|
or set(resolution) != allowed_keys
|
|
or any(
|
|
resolution.get(key) != value
|
|
for key, value in expected.items()
|
|
)
|
|
or not isinstance(resolution.get("value"), str)
|
|
or not resolution["value"]
|
|
):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid resolution"
|
|
)
|
|
result.append(
|
|
WorkspaceSecretResolution(
|
|
identity=identity,
|
|
value=resolution["value"],
|
|
)
|
|
)
|
|
return tuple(result)
|
|
|
|
@staticmethod
|
|
def _identity_payload(identity: WorkspaceSecretIdentity) -> dict[str, str]:
|
|
return {
|
|
"secret_ref": identity.secret_ref,
|
|
"account_scope_digest": identity.account_scope_digest,
|
|
"space_id": identity.space_id,
|
|
"revision_id": identity.revision_id,
|
|
"slot_id": identity.slot_id,
|
|
}
|
|
|
|
def _request(
|
|
self,
|
|
path: str,
|
|
request: dict,
|
|
*,
|
|
max_response_bytes: int | None = None,
|
|
) -> dict:
|
|
response_limit = max_response_bytes or self.MAX_RESPONSE_BYTES
|
|
encoded = json.dumps(
|
|
request, separators=(",", ":"), sort_keys=True
|
|
).encode("utf-8")
|
|
if len(encoded) > self.MAX_REQUEST_BYTES:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker request is too large"
|
|
)
|
|
connection = HTTPConnection(
|
|
"127.0.0.1",
|
|
self._port,
|
|
timeout=self._timeout_seconds,
|
|
)
|
|
try:
|
|
connection.request(
|
|
"POST",
|
|
path,
|
|
body=encoded,
|
|
headers={
|
|
"Authorization": f"Bearer {self._capability}",
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
},
|
|
)
|
|
http_response = connection.getresponse()
|
|
content_length = http_response.getheader("Content-Length")
|
|
if content_length is not None:
|
|
try:
|
|
declared_length = int(content_length)
|
|
except ValueError as exc:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned invalid metadata"
|
|
) from exc
|
|
if declared_length > response_limit:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker response is too large"
|
|
)
|
|
raw = http_response.read(response_limit + 1)
|
|
if len(raw) > response_limit:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker response is too large"
|
|
)
|
|
if http_response.status != 200:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret is unavailable "
|
|
f"(broker_status_{http_response.status})"
|
|
)
|
|
except (OSError, TimeoutError, HTTPException) as exc:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker is unavailable"
|
|
) from exc
|
|
finally:
|
|
connection.close()
|
|
try:
|
|
response = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid response"
|
|
) from exc
|
|
if not isinstance(response, dict):
|
|
raise WorkspaceSecretBrokerError(
|
|
"Workspace secret broker returned an invalid response"
|
|
)
|
|
return response
|