chore(oss): audit the server package and resync the SDK mirrors

Split the 657-line operation service into eor, submit, and verify
modules, add the AGENT_REVOKED and KEY_RETIRED codes the Backend already
emits, and condense every multi-line comment. Mirror Node, Python, and
Go at 2.1.0 with the deep-health 503, non-idempotent retry, rotation
grace, and path-encoding fixes. Document which routes and response
fields the self-hosted server does not provide.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
雷电芽衣 committed 2026-09-03 20:48:20 -04:00
1 parent 2010175a82
commit bc6983a62e
364 files changed
+10180 -23349

No files matched your search

+2 -11
View File
@@ -6,6 +6,7 @@ import os
import stat
from typing import Any, Optional, Tuple
from ._managed_files import same_file_metadata
from ._pinned_directory import PinnedDirectory
@@ -27,16 +28,6 @@ def _regular_identity(
raise OSError(f"{label} changed while copying: {path}")
def _stable_metadata(metadata: os.stat_result) -> tuple[int, int, int]:
# Timestamps are excluded: Windows exposes file times with delayed
# cross-handle visibility, so they are unsound identity evidence.
return (
metadata.st_dev,
metadata.st_ino,
metadata.st_size,
)
def copy_into_reserved(
directory: PinnedDirectory,
source_path: str,
@@ -88,7 +79,7 @@ def copy_into_reserved(
directory.chmod_descriptor(destination_descriptor, mode)
os.fsync(destination_descriptor)
after = os.fstat(source_descriptor)
if _stable_metadata(after) != _stable_metadata(before):
if not same_file_metadata(after, before):
raise OSError(f"{label} changed while copying: {source_path}")
finally:
for descriptor in (destination_descriptor, source_descriptor):
+22 -27
View File
@@ -12,7 +12,7 @@ from typing import Any, Dict, Optional
JsonObject = Dict[str, Any]
def _cleanup_failed_write(path: str, label: str, cause: Exception) -> None:
def _cleanup_failed_write(path: str, label: str, cause: BaseException) -> None:
if not path:
return
try:
@@ -48,7 +48,7 @@ def write_text_atomic(path: str, content: str, mode: int, label: str) -> None:
os.fsync(file.fileno())
os.chmod(temporary_path, mode)
os.replace(temporary_path, path)
except Exception as error:
except BaseException as error:
if descriptor >= 0:
try:
os.close(descriptor)
@@ -59,13 +59,32 @@ def write_text_atomic(path: str, content: str, mode: int, label: str) -> None:
f"close failed: {close_error}"
) from error
_cleanup_failed_write(temporary_path, label, error)
raise OSError(f"Write {label} at {path}: {error}") from error
if isinstance(error, Exception):
raise OSError(f"Write {label} at {path}: {error}") from error
raise
def write_json_atomic(path: str, value: JsonObject, mode: int, label: str) -> None:
write_text_atomic(path, json.dumps(value, indent=2) + "\n", mode, label)
def read_json(path: str, label: str) -> Optional[JsonObject]:
try:
with open(path, "r", encoding="utf-8") as file:
raw = file.read()
except FileNotFoundError:
return None
except OSError as error:
raise OSError(f"Read {label} at {path}: {error}") from error
try:
value = json.loads(raw)
except json.JSONDecodeError as error:
raise ValueError(f"Failed to parse {label} at {path}: {error}") from error
if not isinstance(value, dict):
raise ValueError(f"{label} at {path} must contain a JSON object")
return value
def remove_file(path: str, label: str) -> None:
try:
os.remove(path)
@@ -83,27 +102,3 @@ def regular_file_exists(path: str, label: str) -> bool:
except OSError as error:
raise OSError(f"Read {label} at {path}: {error}") from error
return stat.S_ISREG(metadata.st_mode)
def require_runtime(path: str, label: str) -> None:
if not path:
raise ValueError(f"{label} path is required")
if not regular_file_exists(path, label):
raise FileNotFoundError(f"{label} is missing: {path}")
def read_json(path: str, label: str) -> Optional[JsonObject]:
try:
with open(path, "r", encoding="utf-8") as file:
raw = file.read()
except FileNotFoundError:
return None
except OSError as error:
raise OSError(f"Read {label} at {path}: {error}") from error
try:
value = json.loads(raw)
except json.JSONDecodeError as error:
raise ValueError(f"Failed to parse {label} at {path}: {error}") from error
if not isinstance(value, dict):
raise ValueError(f"{label} at {path} must contain a JSON object")
return value
+29 -46
View File
@@ -28,14 +28,7 @@ class DirectorySnapshot:
def identity_mode_bits(mode: int) -> int:
"""Collapse permission bits that are not identity evidence on Windows.
Windows synthesizes POSIX modes and derives execute bits from the file
extension for path stats but not for handle stats, so the synthetic value
cannot serve as identity there. Windows permissions are enforced through
DACLs in _windows_security instead. Captured snapshots still store the
raw bits because restoration re-applies them.
"""
"""Windows synthesizes mode bits, so they are not identity there."""
return mode if os.name != "nt" else 0
@@ -44,12 +37,8 @@ def portable_file_mode(metadata: os.stat_result) -> int:
return identity_mode_bits(stat.S_IMODE(metadata.st_mode))
def _stable_file_metadata(
metadata: os.stat_result,
) -> tuple[int, int, int, int]:
# Timestamps are excluded deliberately: Windows exposes file times with
# delayed cross-handle visibility, so they are unsound identity evidence.
# Content changes are caught by the verification re-read instead.
def stable_file_metadata(metadata: os.stat_result) -> tuple[int, int, int, int]:
"""Identity without timestamps; Windows file times lag across handles."""
return (
metadata.st_dev,
metadata.st_ino,
@@ -58,11 +47,8 @@ def _stable_file_metadata(
)
def _same_file_metadata(
first: os.stat_result,
second: os.stat_result,
) -> bool:
return _stable_file_metadata(first) == _stable_file_metadata(second)
def same_file_metadata(first: os.stat_result, second: os.stat_result) -> bool:
return stable_file_metadata(first) == stable_file_metadata(second)
def assert_content_stable(
@@ -73,12 +59,7 @@ def assert_content_stable(
raw: bytes,
maximum_bytes: int,
) -> None:
"""Re-read a just-read file and require identical identity and bytes.
This is the deterministic replacement for timestamp comparisons: a
concurrent in-place rewrite is proven by content divergence between the
two reads, independent of filesystem timer visibility.
"""
"""Re-read the file and require identical identity and bytes."""
descriptor = opener()
try:
current = os.fstat(descriptor)
@@ -110,26 +91,25 @@ def _inspect_regular_file(file_path: str, label: str) -> Optional[os.stat_result
return metadata
def read_physical_file(
def read_file_snapshot(
before: os.stat_result,
opener: Callable[[int], int],
file_path: str,
label: str,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileSnapshot]:
before = _inspect_regular_file(file_path, label)
if before is None:
return None
maximum_bytes: int,
) -> FileSnapshot:
"""Read one regular file through opener and prove it stayed stable."""
if before.st_size > maximum_bytes:
raise ValueError(f"{label} exceeds {maximum_bytes} bytes: {file_path}")
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_CLOEXEC", 0)
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = -1
try:
descriptor = os.open(file_path, flags)
descriptor = opener(flags)
after = os.fstat(descriptor)
if not stat.S_ISREG(after.st_mode):
raise OSError(f"{label} path is not a physical file: {file_path}")
if not _same_file_metadata(before, after):
if not same_file_metadata(before, after):
raise OSError(f"{label} changed while opening: {file_path}")
if after.st_size > maximum_bytes:
raise ValueError(f"{label} exceeds {maximum_bytes} bytes: {file_path}")
@@ -137,15 +117,10 @@ def read_physical_file(
descriptor = -1
raw = file.read(maximum_bytes + 1)
finished = os.fstat(file.fileno())
if not _same_file_metadata(finished, after):
if not same_file_metadata(finished, after):
raise OSError(f"{label} changed while reading: {file_path}")
assert_content_stable(
lambda: os.open(file_path, flags),
file_path,
label,
after,
raw,
maximum_bytes,
lambda: opener(flags), file_path, label, after, raw, maximum_bytes
)
except OSError as error:
raise OSError(f"Read {label} at {file_path}: {error}") from error
@@ -159,11 +134,19 @@ def read_physical_file(
contents = raw.decode("utf-8")
except UnicodeDecodeError as error:
raise ValueError(f"{label} at {file_path} must contain UTF-8 text") from error
return FileSnapshot(
contents=contents,
device=after.st_dev,
inode=after.st_ino,
mode=stat.S_IMODE(after.st_mode),
return FileSnapshot(contents, after.st_dev, after.st_ino, stat.S_IMODE(after.st_mode))
def read_physical_file(
file_path: str,
label: str,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileSnapshot]:
before = _inspect_regular_file(file_path, label)
if before is None:
return None
return read_file_snapshot(
before, lambda flags: os.open(file_path, flags), file_path, label, maximum_bytes
)
@@ -8,7 +8,7 @@ import os
import stat
from typing import Optional
from ._managed_files import portable_file_mode
from ._managed_files import same_file_metadata
from ._pinned_directory import PinnedDirectory
@@ -17,12 +17,7 @@ _HASH_CHUNK_BYTES = 1024 * 1024
@dataclass(frozen=True)
class OpaqueFileSnapshot:
"""Content and physical identity without retaining file bytes in memory.
Identity is content-based (device, inode, mode, size, SHA-256). File
timestamps are excluded because Windows exposes them with delayed
visibility across handles, which makes them unsound identity evidence.
"""
"""Device, inode, mode, size, and SHA-256 without retaining the bytes."""
device: int
inode: int
@@ -31,19 +26,6 @@ class OpaqueFileSnapshot:
sha256: str
def _metadata(metadata: os.stat_result) -> tuple[int, int, int, int]:
return (
metadata.st_dev,
metadata.st_ino,
portable_file_mode(metadata),
metadata.st_size,
)
def _same_metadata(first: os.stat_result, second: os.stat_result) -> bool:
return _metadata(first) == _metadata(second)
def _require_regular(
metadata: os.stat_result,
file_path: str,
@@ -82,7 +64,7 @@ def stream_opaque_snapshot(
descriptor = directory.open_file(name, flags)
opened = os.fstat(descriptor)
_require_regular(opened, file_path, label, require_owner_only)
if not _same_metadata(opened, before):
if not same_file_metadata(opened, before):
raise OSError(f"{label} changed while opening: {file_path}")
digest = hashlib.sha256()
while True:
@@ -92,7 +74,7 @@ def stream_opaque_snapshot(
digest.update(chunk)
finished = os.fstat(descriptor)
_require_regular(finished, file_path, label, require_owner_only)
if not _same_metadata(finished, opened):
if not same_file_metadata(finished, opened):
raise OSError(f"{label} changed while hashing: {file_path}")
except OSError as error:
raise OSError(f"Read {label} at {file_path}: {error}") from error
@@ -105,7 +87,7 @@ def stream_opaque_snapshot(
except FileNotFoundError as error:
raise OSError(f"{label} changed while hashing: {file_path}") from error
_require_regular(visible, file_path, label, require_owner_only)
if not _same_metadata(visible, finished):
if not same_file_metadata(visible, finished):
raise OSError(f"{label} changed while hashing: {file_path}")
return OpaqueFileSnapshot(
finished.st_dev,
@@ -10,8 +10,7 @@ from typing import Any, Optional, Tuple
from ._managed_files import (
DirectorySnapshot,
FileSnapshot,
_same_file_metadata,
assert_content_stable,
read_file_snapshot,
read_physical_directory,
)
@@ -381,8 +380,7 @@ class PinnedDirectory:
"""Persist directory entries where the host exposes directory fsync."""
self._require_open()
if os.name == "nt":
# NTFS journals atomic rename metadata. Windows directory handles do
# not support FlushFileBuffers, while every staged file is flushed.
# Windows directory handles do not support FlushFileBuffers.
return
if os.name != "posix":
raise OSError(f"Unsupported transaction platform: {os.name}")
@@ -410,71 +408,21 @@ class PinnedDirectory:
label: str,
maximum_bytes: int,
) -> Optional[FileSnapshot]:
file_path = self.path_for(name)
try:
before = self.stat_file(name)
except FileNotFoundError:
return None
except OSError as error:
raise OSError(f"Inspect {label} at {self.path_for(name)}: {error}") from error
raise OSError(f"Inspect {label} at {file_path}: {error}") from error
if not stat.S_ISREG(before.st_mode) or stat.S_ISLNK(before.st_mode):
raise OSError(f"{label} path is not a physical file: {self.path_for(name)}")
if before.st_size > maximum_bytes:
raise ValueError(
f"{label} exceeds {maximum_bytes} bytes: {self.path_for(name)}"
)
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_CLOEXEC", 0)
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = -1
try:
descriptor = self.open_file(name, flags)
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or not _same_file_metadata(
before,
opened,
):
raise OSError(f"{label} changed while opening: {self.path_for(name)}")
if opened.st_size > maximum_bytes:
raise ValueError(
f"{label} exceeds {maximum_bytes} bytes: {self.path_for(name)}"
)
with os.fdopen(descriptor, "rb") as file:
descriptor = -1
raw = file.read(maximum_bytes + 1)
finished = os.fstat(file.fileno())
if not _same_file_metadata(finished, opened):
raise OSError(
f"{label} changed while reading: {self.path_for(name)}"
)
assert_content_stable(
lambda: self.open_file(name, flags),
self.path_for(name),
label,
opened,
raw,
maximum_bytes,
)
except OSError as error:
raise OSError(f"Read {label} at {self.path_for(name)}: {error}") from error
finally:
if descriptor >= 0:
os.close(descriptor)
if len(raw) > maximum_bytes:
raise ValueError(
f"{label} exceeds {maximum_bytes} bytes: {self.path_for(name)}"
)
try:
contents = raw.decode("utf-8")
except UnicodeDecodeError as error:
raise ValueError(
f"{label} at {self.path_for(name)} must contain UTF-8 text"
) from error
return FileSnapshot(
contents,
opened.st_dev,
opened.st_ino,
stat.S_IMODE(opened.st_mode),
raise OSError(f"{label} path is not a physical file: {file_path}")
return read_file_snapshot(
before,
lambda flags: self.open_file(name, flags),
file_path,
label,
maximum_bytes,
)
def chmod_file(
@@ -12,9 +12,12 @@ from ._managed_files import DirectorySnapshot, FileSnapshot
from ._pinned_directory import PinnedDirectory
_RMDIR_SUPPORTS_DIR_FD = os.rmdir in os.supports_dir_fd
def _remove_posix_private_directory(parent_descriptor: int, name: str) -> None:
"""Remove one empty child through the platform's native *at semantics."""
if os.rmdir not in os.supports_dir_fd:
if not _RMDIR_SUPPORTS_DIR_FD:
raise OSError("Host does not support identity-bound directory removal")
os.rmdir(name, dir_fd=parent_descriptor)
+391
View File
@@ -0,0 +1,391 @@
"""Runtime directory identity, config validation, and status checks shared by providers."""
from __future__ import annotations
import base64
from dataclasses import dataclass
import json
import os
from typing import Any, Callable, Dict, List, Optional, Protocol, Sequence, Tuple
import urllib.parse
from elydora._runtime_paths import resolve_agent_directory, runtime_root
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
MAX_SOURCE_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from ._transaction import FileChange, FilePrecondition, file_change, source_change
from .base import InstallConfig
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
GUARD_SCRIPT = "guard.py"
AUDIT_SCRIPT = "hook.py"
DEFAULT_BASE_URL = "https://api.elydora.com"
RUNTIME_CONFIG_FIELDS = frozenset(
{"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
)
RUNTIME_ARTIFACTS = (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
@dataclass(frozen=True)
class RuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
class RuntimeContractLike(Protocol):
@property
def agent_id(self) -> str: ...
@property
def guard_path(self) -> str: ...
@property
def audit_path(self) -> str: ...
def same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_agent_id(value: Any, expected: str) -> bool:
return isinstance(value, str) and os.path.normcase(value) == os.path.normcase(
expected
)
def managed_script_reference(
script_path: str, script_name: str
) -> Optional[Tuple[str, str]]:
"""Return (agent_id, script_path) for an absolute ~/.elydora/<id>/<script_name>."""
if not os.path.isabs(script_path) or os.path.basename(script_path) != script_name:
return None
agent_directory = os.path.dirname(script_path)
if not same_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in ("", ".", ".."):
return None
return agent_id, script_path
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def require_non_empty_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"Elydora runtime config {field} is invalid: {config_path}")
return value
def read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(file_path, "Elydora runtime config", MAX_CONFIG_BYTES)
if snapshot is None:
return None
return parse_json_object(snapshot.contents, f"Elydora runtime config at {file_path}")
def validate_runtime_config(
config: JsonObject,
expected_agent_id: str,
config_path: str,
agent_key: str,
product_label: str,
) -> None:
extra = next((key for key in config if key not in RUNTIME_CONFIG_FIELDS), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
require_non_empty_string(config.get("org_id"), "org_id", config_path)
require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = require_non_empty_string(config.get("agent_id"), "agent_id", config_path)
if not same_agent_id(agent_id, expected_agent_id) or config.get("agent_name") != agent_key:
raise ValueError(
f"Elydora runtime identity does not match {product_label} hooks: {config_path}"
)
if "token" in config:
require_non_empty_string(config.get("token"), "token", config_path)
base_url = require_non_empty_string(config.get("base_url"), "base_url", config_path)
validate_api_origin(base_url, "Elydora runtime config base_url")
def runtime_config_source(
config: InstallConfig, agent_id: str, agent_key: str, **extra: str
) -> str:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", DEFAULT_BASE_URL),
"agent_name": agent_key,
**extra,
}
token = config.get("token")
if token:
value["token"] = token
return json.dumps(value, indent=2) + "\n"
def resolve_runtime_paths(config: InstallConfig) -> RuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
if not same_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
f"Elydora guard runtime must use the managed agent directory: {guard_path}"
)
return RuntimePaths(
agent_id, agent_directory, guard_path, os.path.join(agent_directory, AUDIT_SCRIPT)
)
def validate_install_config(
config: InstallConfig, agent_key: str, product_label: str
) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != agent_key:
raise ValueError(f"{product_label} installation requires agent_name {agent_key}")
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
resolve_runtime_paths(config)
def validate_runtime_tree(
agent_directory: str,
agent_id: str,
agent_key: str,
product_label: str,
extra_artifacts: Tuple[Tuple[str, str], ...] = (),
) -> None:
"""Reject an existing agent directory that belongs to another identity."""
if not physical_directory_exists(runtime_root(), "Elydora runtime directory"):
return
if not physical_directory_exists(agent_directory, "Elydora agent runtime directory"):
return
config_path = os.path.join(agent_directory, "config.json")
config = read_runtime_config(config_path)
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in (*RUNTIME_ARTIFACTS, *extra_artifacts)
]
artifact_exists = any(artifact_states)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
if (
not same_agent_id(config.get("agent_id"), agent_id)
or config.get("agent_name") != agent_key
):
raise ValueError(
f"Elydora runtime config identity does not match {product_label} agent "
f"{agent_id}: {config_path}"
)
ExpectedScripts = Callable[[str, JsonObject], Tuple[str, str]]
def expected_runtime_scripts(
agent_key: str,
agent_id: str,
config: Optional[JsonObject] = None,
**hook_options: Any,
) -> Tuple[str, str]:
"""Guard and audit sources; identity fields come from config when given."""
org_id, kid, base_url = (
("", "", "")
if config is None
else (str(config["org_id"]), str(config["kid"]), str(config["base_url"]))
)
return (
generate_guard_script(agent_key, agent_id),
generate_hook_script(
org_id=org_id,
agent_id=agent_id,
kid=kid,
base_url=base_url,
native_payload=True,
agent_name=agent_key,
**hook_options,
),
)
def runtime_contract_exists(
contract: RuntimeContractLike,
agent_key: str,
product_label: str,
expected_scripts: Optional[ExpectedScripts] = None,
) -> bool:
"""Strict status check: valid config and key plus runtime scripts."""
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if not same_path(os.path.dirname(agent_directory), root) or not same_path(
contract.audit_path, os.path.join(agent_directory, AUDIT_SCRIPT)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(agent_directory, "Elydora agent runtime directory"):
return False
config_path = os.path.join(agent_directory, "config.json")
config = read_runtime_config(config_path)
key = read_physical_file(
os.path.join(agent_directory, "private.key"), "Elydora private key", MAX_SECRET_BYTES
)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
validate_runtime_config(config, contract.agent_id, config_path, agent_key, product_label)
validate_private_key(key.contents, "Elydora private key")
if expected_scripts is None:
return bool(guard.contents) and bool(audit.contents)
expected_guard, expected_audit = expected_scripts(contract.agent_id, config)
return guard.contents == expected_guard and audit.contents == expected_audit
def runtime_present(contract: RuntimeContractLike, agent_key: str) -> bool:
"""Presence-only status check; identity mismatch reports False."""
agent_directory = os.path.dirname(contract.guard_path)
if not physical_directory_exists(agent_directory, "Elydora agent runtime directory"):
return False
config = read_runtime_config(os.path.join(agent_directory, "config.json"))
if (
config is None
or config.get("agent_name") != agent_key
or not same_agent_id(config.get("agent_id"), contract.agent_id)
):
return False
return all(
(
physical_file_exists(contract.guard_path, "Elydora guard runtime"),
physical_file_exists(contract.audit_path, "Elydora audit runtime"),
physical_file_exists(
os.path.join(agent_directory, "private.key"), "Elydora private key"
),
)
)
def present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def runtime_file_changes(
config: InstallConfig,
paths: RuntimePaths,
agent_key: str,
guard_script: str,
audit_script: str,
) -> List[FileChange]:
"""Changes for the guard, config, key, and audit runtime files."""
return present([
file_change(paths.guard_path, "Elydora guard runtime", guard_script, 0o700),
file_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
runtime_config_source(config, paths.agent_id, agent_key),
0o600,
MAX_CONFIG_BYTES,
),
file_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
file_change(paths.audit_path, "Elydora audit runtime", audit_script, 0o700),
])
def rendered_source_change(
file_path: str,
label: str,
original: Optional[str],
next_source: Optional[str],
) -> Optional[FileChange]:
return source_change(file_path, label, original, next_source, 0o600, MAX_SOURCE_BYTES)
def unique_preconditions(
values: Sequence[FilePrecondition],
) -> Tuple[FilePrecondition, ...]:
result: Dict[str, FilePrecondition] = {}
for value in values:
result.setdefault(os.path.normcase(os.path.abspath(value.file_path)), value)
return tuple(result.values())
@@ -0,0 +1,160 @@
"""Shell quoting and parsing for generated hook commands."""
from __future__ import annotations
import base64
import ntpath
import os
import re
import sys
from typing import Optional, Tuple
CommandParts = Tuple[str, str]
POWERSHELL_EXIT_SUFFIX = "; exit $LASTEXITCODE"
_POSIX_APOSTROPHE = "'\"'\"'"
_ENCODED_COMMAND = re.compile(
r'"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive '
r"-EncodedCommand ([A-Za-z0-9+/]+={0,2})"
)
_PYTHON_EXECUTABLE = re.compile(
r"(?:python(?:[0-9]+(?:\.[0-9]+)*)?[td]?|pypy(?:[0-9]+(?:\.[0-9]+)*)?)(?:\.exe)?",
re.IGNORECASE,
)
def quote_posix(value: str) -> str:
return "'" + value.replace("'", _POSIX_APOSTROPHE) + "'"
def quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def posix_source(script_path: str) -> str:
return f"{quote_posix(sys.executable)} {quote_posix(script_path)}"
def powershell_source(script_path: str) -> str:
return (
f"& {quote_powershell(sys.executable)} {quote_powershell(script_path)}"
f"{POWERSHELL_EXIT_SUFFIX}"
)
def windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot") if os.name == "nt" else None
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(system_root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
def encode_powershell_source(source: str) -> str:
return base64.b64encode(source.encode("utf-16le")).decode("ascii")
def encoded_windows_command(script_path: str, preamble: str = "") -> str:
encoded = encode_powershell_source(preamble + powershell_source(script_path))
return (
f'"{windows_powershell_path()}" -NoLogo -NoProfile -NonInteractive '
f"-EncodedCommand {encoded}"
)
def is_python_executable(file_path: str) -> bool:
return _PYTHON_EXECUTABLE.fullmatch(os.path.basename(file_path)) is not None
def is_powershell_executable(file_path: str) -> bool:
return ntpath.isabs(file_path) and ntpath.basename(file_path).lower() == "powershell.exe"
def read_posix_argument(command: str, start: int) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command.startswith(_POSIX_APOSTROPHE, index):
value += "'"
index += len(_POSIX_APOSTROPHE)
continue
if command[index] == "'":
return value, index + 1
value += command[index]
index += 1
return None
def parse_posix_command(command: str) -> Optional[CommandParts]:
executable = read_posix_argument(command, 0)
if executable is None or command[executable[1] : executable[1] + 1] != " ":
return None
script = read_posix_argument(command, executable[1] + 1)
if script is None or script[1] != len(command):
return None
return executable[0], script[0]
def read_powershell_argument(command: str, start: int) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return value, index + 1
return None
def parse_powershell_source(source: str, prefix: str = "& ") -> Optional[CommandParts]:
if not source.startswith(prefix):
return None
executable = read_powershell_argument(source, len(prefix))
if executable is None or source[executable[1] : executable[1] + 1] != " ":
return None
script = read_powershell_argument(source, executable[1] + 1)
if script is None or source[script[1] :] != POWERSHELL_EXIT_SUFFIX:
return None
return executable[0], script[0]
def decode_powershell_source(encoded: str) -> Optional[str]:
if re.fullmatch(r"[A-Za-z0-9+/]+={0,2}", encoded) is None:
return None
try:
payload = base64.b64decode(encoded, validate=True)
if base64.b64encode(payload).decode("ascii") != encoded or len(payload) % 2:
return None
return payload.decode("utf-16le")
except (ValueError, UnicodeDecodeError):
return None
def same_windows_path(left: str, right: str) -> bool:
return ntpath.normcase(ntpath.abspath(left)) == ntpath.normcase(ntpath.abspath(right))
def parse_encoded_windows_command(
command: str,
prefix: str = "& ",
powershell_path: Optional[str] = None,
) -> Optional[CommandParts]:
match = _ENCODED_COMMAND.fullmatch(command)
if match is None or not is_powershell_executable(match.group(1)):
return None
if powershell_path is not None and not same_windows_path(match.group(1), powershell_path):
return None
source = decode_powershell_source(match.group(2))
return None if source is None else parse_powershell_source(source, prefix)
+1 -26
View File
@@ -6,12 +6,7 @@ import os
from typing import List, Optional, Sequence, cast
from uuid import uuid4
from ._managed_files import (
FileSnapshot,
MAX_SOURCE_BYTES,
physical_file_exists,
read_physical_file,
)
from ._managed_files import FileSnapshot, MAX_SOURCE_BYTES, read_physical_file
from ._opaque_cleanup import cleanup_opaque_removal
from ._opaque_removal import (
OpaqueRemovalChange,
@@ -65,15 +60,6 @@ def recover_pending_transactions() -> None:
_recover_pending_transactions()
def read_optional(
file_path: str,
label: str,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[str]:
snapshot = read_physical_file(file_path, label, maximum_bytes)
return None if snapshot is None else snapshot.contents
def source_change(
file_path: str,
label: str,
@@ -479,14 +465,3 @@ def write_changes(
directory_preconditions,
opaque_removals,
)
def regular_file_exists(file_path: str, label: str) -> bool:
return physical_file_exists(file_path, label)
def require_runtime(file_path: str, label: str) -> None:
if not file_path:
raise ValueError(f"{label} path is required")
if not regular_file_exists(file_path, label):
raise FileNotFoundError(f"{label} is missing: {file_path}")
@@ -13,6 +13,8 @@ from ._transaction_recovery import remove_committed_creation
from ._transaction_staging import (
assert_transaction_preconditions,
assert_unchanged,
read_target,
same_identity_and_contents,
same_snapshot,
)
from ._transaction_types import FileChange, StagedChange
@@ -38,14 +40,6 @@ def _original_snapshot(change: FileChange) -> Optional[FileSnapshot]:
)
def _read_target(staged: StagedChange) -> Optional[FileSnapshot]:
return staged.directory.read_file(
staged.directory.name_for(staged.change.file_path),
staged.change.label,
staged.change.maximum_bytes,
)
def _read_temporary(staged: StagedChange) -> Optional[FileSnapshot]:
if staged.temporary_path is None:
return None
@@ -66,18 +60,6 @@ def _read_rollback(staged: StagedChange) -> Optional[FileSnapshot]:
)
def _same_identity_and_contents(
current: Optional[FileSnapshot], expected: Optional[FileSnapshot]
) -> bool:
if current is None or expected is None:
return current is expected
return (
current.contents == expected.contents
and current.device == expected.device
and current.inode == expected.inode
)
def _preserve_rollback(staged: StagedChange, error: Exception) -> OSError:
if staged.rollback_path is None:
return error if isinstance(error, OSError) else OSError(str(error))
@@ -97,7 +79,7 @@ def prepare_change(staged: StagedChange, capture_file: ReplaceFile) -> None:
raise OSError(f"Missing rollback reservation for {staged.change.label}")
capture_file(staged.change.file_path, rollback_path, staged.directory)
assert_transaction_preconditions(*staged.preconditions, staged.directories)
current = _read_target(staged)
current = read_target(staged)
if not same_snapshot(current, original):
raise OSError(
f"{staged.change.label} changed at commit boundary: "
@@ -119,7 +101,7 @@ def prepare_change(staged: StagedChange, capture_file: ReplaceFile) -> None:
staged.change.label,
staged.change.maximum_bytes,
)
current = _read_target(staged)
current = read_target(staged)
if not same_snapshot(current, original):
raise OSError(
f"{staged.change.label} changed while preparing rollback: "
@@ -151,7 +133,7 @@ def _replace_next(
raise OSError(f"Missing staged file for {staged.change.label}")
replace_file(temporary_path, staged.change.file_path, staged.directory)
assert_transaction_preconditions(*staged.preconditions, staged.directories)
current = _read_target(staged)
current = read_target(staged)
expected = _original_snapshot(staged.change)
if not same_snapshot(current, expected):
error = OSError(
@@ -227,7 +209,7 @@ def commit_change(
else:
_replace_next(staged, replace_file, after_replace)
current = _read_target(staged)
current = read_target(staged)
if not same_snapshot(current, staged.committed_snapshot):
raise OSError(
f"{staged.change.label} changed immediately after commit: "
@@ -251,7 +233,7 @@ def _restore_rollback_file(
rollback = staged.rollback_snapshot
if rollback_path is None or rollback is None:
raise OSError(f"Missing rollback path for {staged.change.label}")
if not same_snapshot(_read_target(staged), expected_current):
if not same_snapshot(read_target(staged), expected_current):
raise _preserve_rollback(
staged,
OSError(
@@ -263,7 +245,7 @@ def _restore_rollback_file(
restore_file(rollback_path, staged.change.file_path, staged.directory)
except Exception as error:
raise _preserve_rollback(staged, error) from error
if not same_snapshot(_read_target(staged), expected_current):
if not same_snapshot(read_target(staged), expected_current):
raise _preserve_rollback(
staged,
OSError(
@@ -272,7 +254,7 @@ def _restore_rollback_file(
),
)
current_rollback = _read_rollback(staged)
if not _same_identity_and_contents(current_rollback, rollback):
if not same_identity_and_contents(current_rollback, rollback):
raise _preserve_rollback(
staged,
OSError(
@@ -288,7 +270,7 @@ def _restore_rollback_file(
)
atomic_replace(staged.directory, rollback_path, staged.change.file_path)
staged.rollback_path = None
restored = _read_target(staged)
restored = read_target(staged)
if (
restored is None
or restored.contents != expected.contents
@@ -307,7 +289,7 @@ def rollback_change(
) -> None:
if not staged.committed:
return
current = _read_target(staged)
current = read_target(staged)
if not same_snapshot(current, staged.committed_snapshot):
raise _preserve_rollback(
staged,
@@ -13,7 +13,7 @@ from uuid import uuid4
from ._managed_files import (
DirectorySnapshot,
FileSnapshot,
_same_file_metadata,
same_file_metadata,
identity_mode_bits,
)
from ._opaque_snapshot import OpaqueFileSnapshot
@@ -300,7 +300,7 @@ def inspect_fingerprint(
descriptor = directory.open_file(name, flags)
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or not _same_file_metadata(before, opened):
if not stat.S_ISREG(opened.st_mode) or not same_file_metadata(before, opened):
raise OSError(f"{label} changed while opening: {file_path}")
digest = hashlib.sha256()
size = 0
@@ -311,12 +311,12 @@ def inspect_fingerprint(
digest.update(chunk)
size += len(chunk)
finished = os.fstat(descriptor)
if not _same_file_metadata(finished, opened):
if not same_file_metadata(finished, opened):
raise OSError(f"{label} changed while hashing: {file_path}")
finally:
os.close(descriptor)
visible = directory.stat_file(name)
if not _same_file_metadata(visible, finished):
if not same_file_metadata(visible, finished):
raise OSError(f"{label} changed while hashing: {file_path}")
return FileFingerprint(
digest.hexdigest(),
@@ -419,7 +419,7 @@ def load_journal() -> Optional[TransactionJournal]:
descriptor = os.open(path, flags)
try:
opened = os.fstat(descriptor)
if not _same_file_metadata(opened, metadata):
if not same_file_metadata(opened, metadata):
raise OSError(f"Transaction journal changed while opening: {path}")
chunks = []
remaining = MAX_JOURNAL_BYTES + 1
@@ -431,7 +431,7 @@ def load_journal() -> Optional[TransactionJournal]:
remaining -= len(chunk)
raw = b"".join(chunks)
finished = os.fstat(descriptor)
if not _same_file_metadata(finished, opened):
if not same_file_metadata(finished, opened):
raise OSError(f"Transaction journal changed while reading: {path}")
finally:
os.close(descriptor)
@@ -439,7 +439,7 @@ def load_journal() -> Optional[TransactionJournal]:
visible = os.lstat(path)
except FileNotFoundError as error:
raise OSError(f"Transaction journal changed while reading: {path}") from error
if not _same_file_metadata(visible, finished):
if not same_file_metadata(visible, finished):
raise OSError(f"Transaction journal changed while reading: {path}")
if len(raw) > MAX_JOURNAL_BYTES:
raise ValueError("Transaction journal exceeds its size limit")
@@ -9,21 +9,13 @@ from uuid import uuid4
from ._managed_files import FileSnapshot
from ._pinned_directory import PinnedDirectory
from ._private_artifact import QuarantinedFile, reserve_private_path
from ._transaction_staging import same_snapshot
from ._transaction_staging import read_target, same_snapshot
from ._transaction_types import StagedChange
ReplaceFile = Callable[[str, str, PinnedDirectory], None]
def _read_target(staged: StagedChange) -> Optional[FileSnapshot]:
return staged.directory.read_file(
staged.directory.name_for(staged.change.file_path),
staged.change.label,
staged.change.maximum_bytes,
)
def _read_private_path(
staged: StagedChange,
file_path: str,
@@ -36,18 +28,6 @@ def _read_private_path(
)
def _same_identity_and_contents(
current: Optional[FileSnapshot], expected: Optional[FileSnapshot]
) -> bool:
if current is None or expected is None:
return current is expected
return (
current.contents == expected.contents
and current.device == expected.device
and current.inode == expected.inode
)
def _recovery_path(staged: StagedChange) -> tuple[str, FileSnapshot]:
basename = os.path.basename(staged.change.file_path)
path = staged.directory.path_for(f".{basename}.{uuid4().hex}.recovery")
@@ -63,76 +43,6 @@ def _preserve_private_path(error: Exception, file_path: str) -> OSError:
return OSError(f"{error}; transaction data preserved at {file_path}")
def _release_rollback_source(staged: StagedChange, source_path: str) -> bool:
if staged.rollback_path is not None and os.path.normcase(
os.path.abspath(staged.rollback_path)
) == os.path.normcase(os.path.abspath(source_path)):
staged.rollback_path = None
return True
return False
def _preserve_source_path(
error: Exception,
file_path: str,
original_source: bool,
) -> OSError:
kind = "original content" if original_source else "transaction data"
return OSError(f"{error}; {kind} preserved at {file_path}")
def install_private_file(
staged: StagedChange,
source_path: str,
expected: FileSnapshot,
replace_file: ReplaceFile,
) -> None:
try:
replace_file(source_path, staged.change.file_path, staged.directory)
except Exception:
raise
captured = QuarantinedFile.capture(
staged.directory,
source_path,
f"{staged.change.label} recovery source",
)
original_source = _release_rollback_source(staged, source_path)
captured_snapshot = captured.text_snapshot(staged.change.maximum_bytes)
if not same_snapshot(captured_snapshot, expected):
try:
captured.restore(source_path)
except Exception as error:
preserved = captured.preserve() if captured.active else source_path
raise OSError(
f"{error}; concurrent data preserved at {preserved}"
) from error
raise OSError(
f"{staged.change.label} recovery source changed: {source_path}; "
f"concurrent data preserved at {source_path}"
)
try:
captured.link(staged.change.file_path)
except Exception as error:
preserved = captured.preserve() if captured.active else source_path
raise _preserve_source_path(
error,
preserved,
original_source,
) from error
restored = _read_target(staged)
if not same_snapshot(restored, expected):
preserved = captured.preserve()
raise _preserve_source_path(
OSError(
f"{staged.change.label} changed while installing recovery data: "
f"{staged.change.file_path}"
),
preserved,
original_source,
)
captured.discard()
def capture_recovery_target(
staged: StagedChange,
expected: FileSnapshot,
@@ -263,7 +173,7 @@ def remove_committed_creation(
replace_file,
capture_file,
)
if _read_target(staged) is not None:
if read_target(staged) is not None:
discard_private_file(staged, recovery_path, recovery_snapshot)
raise OSError(
f"{staged.change.label} changed after recovery capture: "
@@ -275,6 +185,5 @@ def remove_committed_creation(
__all__ = [
"capture_recovery_target",
"discard_private_file",
"install_private_file",
"remove_committed_creation",
]
@@ -46,6 +46,26 @@ def same_snapshot(
return current == expected
def read_target(staged: StagedChange) -> Optional[FileSnapshot]:
return staged.directory.read_file(
staged.directory.name_for(staged.change.file_path),
staged.change.label,
staged.change.maximum_bytes,
)
def same_identity_and_contents(
current: Optional[FileSnapshot], expected: Optional[FileSnapshot]
) -> bool:
if current is None or expected is None:
return current is expected
return (
current.contents == expected.contents
and current.device == expected.device
and current.inode == expected.inode
)
def remove_optional(directory: PinnedDirectory, file_path: Optional[str]) -> None:
if not file_path:
return
@@ -338,8 +358,7 @@ def _remove_exact_text(
!= (expected.device, expected.inode)
)
if not changed:
# An in-place rewrite keeps the inode, so the removal is only exact
# once the bytes also match the owned snapshot.
# An in-place rewrite keeps the inode; bytes must match too.
try:
snapshot = captured.text_snapshot(
len(expected.contents.encode("utf-8"))
@@ -210,12 +210,6 @@ def _current_user_sid() -> Tuple[Any, str]:
close_handle(int(token_value))
def current_user_sid_string() -> str:
"""Return the stable current-user SID used by the state namespace."""
_sid, sid_text = _current_user_sid()
return sid_text
def _security_descriptor(directory: bool) -> Tuple[int, int]:
_sid, sid_text = _current_user_sid()
inheritance = "OICI" if directory else ""
@@ -491,7 +485,6 @@ def verify_private_path(path: str, *, directory: bool) -> None:
__all__ = [
"close_handle",
"current_user_sid_string",
"ensure_private_directory",
"open_private_file",
"verify_private_path",
@@ -9,6 +9,9 @@ import os
import sys
from typing import Any, Dict, List, Optional, Set, Tuple
from elydora._runtime_paths import runtime_root
from ._runtime import same_agent_id, same_path
from ._strict_json import parse_json_object
@@ -69,10 +72,6 @@ def home_dir() -> str:
return os.path.expanduser("~")
def elydora_dir() -> str:
return os.path.join(home_dir(), ".elydora")
def resolve_config_path() -> str:
return os.path.join(home_dir(), ".augment", "settings.json")
@@ -162,16 +161,6 @@ def _parse_wrapper_command(command: str) -> Optional[str]:
return parser(command)
def _same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def _same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _managed_agent_id(handler: JsonObject, wrapper_name: str) -> Optional[str]:
if (
handler.get("type") != "command"
@@ -188,7 +177,7 @@ def _managed_agent_id(handler: JsonObject, wrapper_name: str) -> Optional[str]:
):
return None
agent_directory = os.path.dirname(wrapper_path)
if not _same_path(os.path.dirname(agent_directory), elydora_dir()):
if not same_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
return agent_id if agent_id not in {"", ".", ".."} else None
@@ -324,7 +313,7 @@ def _remove_managed(
for handler in group["hooks"]:
managed_id = _managed_agent_id(handler, wrapper_name)
remove = managed_id is not None and (
not agent_id or _same_agent_id(managed_id, agent_id)
not agent_id or same_agent_id(managed_id, agent_id)
)
if remove:
changed = True
@@ -376,12 +365,12 @@ def runtime_contracts(hooks: AugmentHooks) -> List[RuntimeContract]:
contracts: List[RuntimeContract] = []
for guard_id in sorted(guards, key=os.path.normcase):
audit_id = next(
(candidate for candidate in audits if _same_agent_id(candidate, guard_id)),
(candidate for candidate in audits if same_agent_id(candidate, guard_id)),
None,
)
if audit_id is None:
continue
agent_directory = os.path.join(elydora_dir(), guard_id)
agent_directory = os.path.join(runtime_root(), guard_id)
contracts.append(
RuntimeContract(
agent_id=guard_id,
@@ -3,107 +3,61 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional
from typing import List
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, file_change, write_changes
from .augment_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
AUDIT_WRAPPER,
GUARD_WRAPPER,
AugmentDocument,
RenderedAugmentDocument,
build_wrapper,
elydora_dir,
wrapper_paths,
)
from .augment_io import (
rendered_change,
validate_api_origin,
validate_private_key,
validate_runtime_tree,
)
from .augment_io import rendered_change
from .base import InstallConfig
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
WRAPPER_ARTIFACTS = (
(GUARD_WRAPPER, "Auggie guard wrapper"),
(AUDIT_WRAPPER, "Auggie audit wrapper"),
)
@dataclass(frozen=True)
class AugmentRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
class AugmentRuntimePaths(RuntimePaths):
guard_wrapper_path: str
audit_wrapper_path: str
def _same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> AugmentRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(elydora_dir(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not _same_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
f"Elydora guard runtime must use the managed agent directory: {guard_path}"
)
wrappers = wrapper_paths(agent_directory)
paths = resolve_runtime_paths(config)
wrappers = wrapper_paths(paths.agent_directory)
return AugmentRuntimePaths(
agent_id,
agent_directory,
guard_path,
audit_path,
paths.agent_id,
paths.agent_directory,
paths.guard_path,
paths.audit_path,
wrappers.guard_path,
wrappers.audit_path,
)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"Augment Code CLI installation requires agent_name {AGENT_KEY}"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
def _validate_runtime_tree(paths: AugmentRuntimePaths) -> None:
validate_runtime_tree(
paths.agent_directory, paths.agent_id, AGENT_KEY, "Auggie", WRAPPER_ARTIFACTS
)
def preflight_augment_installation(
@@ -111,26 +65,12 @@ def preflight_augment_installation(
) -> AugmentRuntimePaths:
if not document.config_path:
raise ValueError("Augment Code CLI installation requires a settings path")
_validate_install_config(config)
validate_install_config(config, AGENT_KEY, "Augment Code CLI")
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
_validate_runtime_tree(paths)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_augment_installation(
config: InstallConfig,
paths: AugmentRuntimePaths,
@@ -138,54 +78,35 @@ def prepare_augment_installation(
) -> List[FileChange]:
if not rendered.changed and rendered.document.raw is None:
raise ValueError("Auggie hook installation did not produce a settings document")
validate_runtime_tree(paths.agent_directory, paths.agent_id)
_validate_runtime_tree(paths)
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
changes = _present(
[
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
_runtime_change(
changes = [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([
file_change(
paths.guard_wrapper_path,
"Auggie guard wrapper",
build_wrapper(paths.guard_path),
0o700,
),
_runtime_change(
file_change(
paths.audit_wrapper_path,
"Auggie audit wrapper",
build_wrapper(paths.audit_path),
0o700,
),
rendered_change(rendered),
]
)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
]),
]
_validate_runtime_tree(paths)
return changes
@@ -193,10 +114,8 @@ def commit_augment_installation(changes: List[FileChange]) -> None:
write_changes(changes, "Install Augment Code CLI hooks")
def prepare_augment_uninstall(
rendered: RenderedAugmentDocument,
) -> List[FileChange]:
return _present([rendered_change(rendered)])
def prepare_augment_uninstall(rendered: RenderedAugmentDocument) -> List[FileChange]:
return present([rendered_change(rendered)])
def commit_augment_uninstall(changes: List[FileChange]) -> None:
+18 -209
View File
@@ -2,61 +2,36 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Optional
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from ._managed_files import MAX_CONFIG_BYTES, physical_directory_exists, read_physical_file
from ._runtime import runtime_contract_exists, same_path
from ._transaction import FileChange, source_change, write_changes
from .augment_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
AUDIT_WRAPPER,
GUARD_SCRIPT,
GUARD_WRAPPER,
AugmentDocument,
RenderedAugmentDocument,
RuntimeContract,
build_wrapper,
create_augment_document,
elydora_dir,
parse_augment_document,
resolve_config_path,
)
def _same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def _same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def read_augment_document() -> AugmentDocument:
config_path = resolve_config_path()
physical_directory_exists(
os.path.dirname(config_path), "Auggie configuration directory"
)
physical_directory_exists(os.path.dirname(config_path), "Auggie configuration directory")
snapshot = read_physical_file(config_path, "Auggie user settings", MAX_CONFIG_BYTES)
if snapshot is None:
return create_augment_document(config_path)
return parse_augment_document(config_path, snapshot.contents)
def rendered_change(
rendered: RenderedAugmentDocument,
) -> Optional[FileChange]:
def rendered_change(rendered: RenderedAugmentDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
@@ -75,192 +50,26 @@ def write_augment_document(rendered: RenderedAugmentDocument) -> None:
write_changes([change], "Uninstall Augment Code CLI hooks")
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"Elydora runtime config {field} is invalid: {config_path}")
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not _same_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime identity does not match Auggie hooks: {config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(file_path, "Elydora runtime config", MAX_CONFIG_BYTES)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
(GUARD_SCRIPT, "Elydora guard runtime"),
(AUDIT_SCRIPT, "Elydora audit runtime"),
(GUARD_WRAPPER, "Auggie guard wrapper"),
(AUDIT_WRAPPER, "Auggie audit wrapper"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not _same_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Auggie agent "
f"{agent_id}: {config_path}"
)
def _runtime_contract_exists(contract: RuntimeContract) -> bool:
root = elydora_dir()
def _wrappers_exist(contract: RuntimeContract) -> bool:
agent_directory = os.path.dirname(contract.guard_path)
if (
not _same_path(os.path.dirname(agent_directory), root)
or not _same_path(
contract.audit_path, os.path.join(agent_directory, AUDIT_SCRIPT)
)
or not _same_path(
contract.guard_wrapper_path,
os.path.join(agent_directory, GUARD_WRAPPER),
)
or not _same_path(
contract.audit_wrapper_path,
os.path.join(agent_directory, AUDIT_WRAPPER),
)
if not same_path(
contract.guard_wrapper_path, os.path.join(agent_directory, GUARD_WRAPPER)
) or not same_path(
contract.audit_wrapper_path, os.path.join(agent_directory, AUDIT_WRAPPER)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
guard_wrapper = read_physical_file(
contract.guard_wrapper_path, "Auggie guard wrapper"
)
audit_wrapper = read_physical_file(
contract.audit_wrapper_path, "Auggie audit wrapper"
)
if any(
item is None
for item in (config, key, guard, audit, guard_wrapper, audit_wrapper)
):
return False
assert config is not None
assert key is not None
assert guard is not None
assert audit is not None
assert guard_wrapper is not None
assert audit_wrapper is not None
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
guard_wrapper = read_physical_file(contract.guard_wrapper_path, "Auggie guard wrapper")
audit_wrapper = read_physical_file(contract.audit_wrapper_path, "Auggie audit wrapper")
return (
bool(guard.contents)
and bool(audit.contents)
guard_wrapper is not None
and audit_wrapper is not None
and guard_wrapper.contents == build_wrapper(contract.guard_path)
and audit_wrapper.contents == build_wrapper(contract.audit_path)
)
def augment_runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
return any(
runtime_contract_exists(contract, AGENT_KEY, "Auggie") and _wrappers_exist(contract)
for contract in contracts
)
@@ -11,6 +11,7 @@ import sys
from typing import Any, Dict, List, Optional, Tuple
import urllib.parse
from ._runtime import managed_script_reference, same_agent_id, same_path
from ._strict_json import JsonObject, parse_json_object
@@ -82,20 +83,6 @@ class _RuntimeReference:
script_path: str
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_claude_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_claude_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _require_known_keys(
value: JsonObject, allowed: frozenset[str], label: str
) -> None:
@@ -292,15 +279,8 @@ def _exact_managed_group(group: JsonObject) -> bool:
def _runtime_reference(
script_path: str, script_name: str
) -> Optional[_RuntimeReference]:
if not os.path.isabs(script_path) or os.path.basename(script_path) != script_name:
return None
agent_directory = os.path.dirname(script_path)
if not same_claude_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in ("", ".", ".."):
return None
return _RuntimeReference(agent_id, script_path)
reference = managed_script_reference(script_path, script_name)
return None if reference is None else _RuntimeReference(*reference)
def _legacy_reference(
@@ -314,7 +294,7 @@ def _legacy_reference(
if (
match is None
or not os.path.isabs(match.group(1))
or not same_claude_path(match.group(1), sys.executable)
or not same_path(match.group(1), sys.executable)
):
return None
return _runtime_reference(match.group(2), script_name)
@@ -334,7 +314,7 @@ def _managed_reference(
and handler.get("statusMessage") == status_message
and isinstance(handler.get("command"), str)
and os.path.isabs(str(handler["command"]))
and same_claude_path(str(handler["command"]), sys.executable)
and same_path(str(handler["command"]), sys.executable)
and isinstance(handler.get("args"), list)
and len(handler["args"]) == 1
and isinstance(handler["args"][0], str)
@@ -365,7 +345,7 @@ def _remove_from_groups(
)
owned = reference is not None and (
not agent_id
or same_claude_agent_id(reference.agent_id, agent_id)
or same_agent_id(reference.agent_id, agent_id)
)
if owned:
removed = True
@@ -485,7 +465,7 @@ def claude_runtime_contracts(
failure = failures.get(key, [])
if len(guard) != 1 or len(success) != 1 or len(failure) != 1:
continue
if not same_claude_path(success[0].script_path, failure[0].script_path):
if not same_path(success[0].script_path, failure[0].script_path):
continue
contracts.append(ClaudeRuntimeContract(
guard[0].agent_id,
@@ -2,121 +2,41 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional
from typing import List
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, file_change, write_changes
from .base import InstallConfig
from .claudecode_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
ClaudeDocument,
RenderedClaudeDocument,
runtime_root,
same_claude_path,
)
from .claudecode_io import (
rendered_change,
validate_api_origin,
validate_private_key,
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_install_config,
validate_runtime_tree,
)
from .hook_template import generate_guard_script, generate_hook_script
from ._transaction import FileChange, write_changes
from .base import InstallConfig
from .claudecode_contract import AGENT_KEY, ClaudeDocument, RenderedClaudeDocument
from .claudecode_io import rendered_change
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
class ClaudeRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> ClaudeRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_claude_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return ClaudeRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"Claude Code installation requires agent_name {AGENT_KEY}"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Claude Code"
def preflight_claude_installation(
config: InstallConfig, document: ClaudeDocument
) -> ClaudeRuntimePaths:
) -> RuntimePaths:
if not document.file_path:
raise ValueError("Claude Code installation requires a settings path")
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_claude_installation(
config: InstallConfig,
paths: ClaudeRuntimePaths,
paths: RuntimePaths,
rendered: RenderedClaudeDocument,
) -> List[FileChange]:
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
@@ -124,43 +44,22 @@ def prepare_claude_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
return _present([
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
rendered_change(rendered),
])
return [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([rendered_change(rendered)]),
]
def commit_claude_installation(changes: List[FileChange]) -> None:
write_changes(changes, "Install Claude Code hooks")
def prepare_claude_uninstall(
rendered: RenderedClaudeDocument,
) -> List[FileChange]:
return _present([rendered_change(rendered)])
def prepare_claude_uninstall(rendered: RenderedClaudeDocument) -> List[FileChange]:
return present([rendered_change(rendered)])
def commit_claude_uninstall(changes: List[FileChange]) -> None:
+11 -196
View File
@@ -2,21 +2,12 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Optional
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
MAX_SOURCE_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from ._transaction import FileChange, source_change, write_changes
from ._managed_files import MAX_SOURCE_BYTES, physical_directory_exists, read_physical_file
from ._runtime import rendered_source_change, runtime_contract_exists
from ._transaction import FileChange, write_changes
from .claudecode_contract import (
AGENT_KEY,
CONFIG_FILE,
@@ -25,9 +16,6 @@ from .claudecode_contract import (
RenderedClaudeDocument,
create_claude_document,
parse_claude_document,
runtime_root,
same_claude_agent_id,
same_claude_path,
)
@@ -43,29 +31,21 @@ def claude_settings_path() -> str:
def read_claude_document() -> ClaudeDocument:
file_path = claude_settings_path()
physical_directory_exists(
os.path.dirname(file_path), "Claude Code configuration directory"
)
snapshot = read_physical_file(
file_path, "Claude Code user settings", MAX_SOURCE_BYTES
)
physical_directory_exists(os.path.dirname(file_path), "Claude Code configuration directory")
snapshot = read_physical_file(file_path, "Claude Code user settings", MAX_SOURCE_BYTES)
if snapshot is None:
return create_claude_document(file_path)
return parse_claude_document(file_path, snapshot.contents)
def rendered_change(
rendered: RenderedClaudeDocument,
) -> Optional[FileChange]:
def rendered_change(rendered: RenderedClaudeDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
return rendered_source_change(
rendered.document.file_path,
"Claude Code user settings",
rendered.document.raw,
rendered.next_source,
0o600,
MAX_SOURCE_BYTES,
)
@@ -75,172 +55,7 @@ def write_claude_document(rendered: RenderedClaudeDocument) -> None:
write_changes([change], "Uninstall Claude Code hooks")
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
def claude_runtime_files_exist(contracts: List[ClaudeRuntimeContract]) -> bool:
return any(
runtime_contract_exists(contract, AGENT_KEY, "Claude Code") for contract in contracts
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(
value: Any, field: str, config_path: str
) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_claude_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime identity does not match Claude Code hooks: "
f"{config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = runtime_root()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
]
artifact_exists = any(artifact_states)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_claude_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Claude Code agent "
f"{agent_id}: {config_path}"
)
def _runtime_contract_exists(contract: ClaudeRuntimeContract) -> bool:
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_claude_path(os.path.dirname(agent_directory), root)
or not same_claude_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
return bool(guard.contents) and bool(audit.contents)
def claude_runtime_files_exist(
contracts: List[ClaudeRuntimeContract],
) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
+5 -17
View File
@@ -8,7 +8,9 @@ import json
import os
from typing import Any, Dict, Optional
from elydora._runtime_paths import resolve_agent_directory
from elydora._runtime_paths import resolve_agent_directory, runtime_root
from ._runtime import same_agent_id, same_path
AGENT_KEY = "cline"
@@ -58,20 +60,6 @@ def home_dir() -> str:
return os.path.expanduser("~")
def elydora_dir() -> str:
return os.path.join(home_dir(), ".elydora")
def same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def resolve_hooks_directory() -> str:
configured = os.environ.get("CLINE_DIR", "").strip()
cline_directory = (
@@ -267,10 +255,10 @@ def runtime_contract(
raise ValueError("Elydora Cline hook files contain mismatched event metadata")
if not same_agent_id(guard.agent_id, audit.agent_id):
raise ValueError("Elydora Cline hook files reference different agents")
agent_directory = resolve_agent_directory(elydora_dir(), guard.agent_id)
agent_directory = resolve_agent_directory(runtime_root(), guard.agent_id)
expected_guard = os.path.join(agent_directory, GUARD_SCRIPT)
expected_audit = os.path.join(agent_directory, AUDIT_SCRIPT)
if not _same_path(guard.runtime_path, expected_guard) or not _same_path(
if not same_path(guard.runtime_path, expected_guard) or not same_path(
audit.runtime_path, expected_audit
):
raise ValueError(
+34 -143
View File
@@ -2,107 +2,42 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
import sys
from typing import List, Optional, Sequence
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, file_change, source_change, write_changes
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
same_agent_id,
same_path,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, source_change, write_changes
from .base import InstallConfig
from .cline_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
HookFile,
assert_wrapper_integrity,
build_metadata,
build_wrapper,
elydora_dir,
resolve_hook_files,
runtime_contract,
same_agent_id,
)
from .cline_io import (
validate_api_origin,
validate_hook_tree,
validate_private_key,
validate_runtime_tree,
)
from .cline_io import validate_hook_tree
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
class ClineRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> ClineRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(elydora_dir(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not _same_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
f"Elydora guard runtime must use the managed agent directory: {guard_path}"
)
return ClineRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Cline installation requires agent_name {AGENT_KEY}")
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
if not os.path.isabs(sys.executable):
raise RuntimeError("Cline requires an absolute Python executable path")
_agent_paths(config)
PRODUCT = "Cline"
def _validate_hook_files(guard_file: HookFile, audit_file: HookFile) -> None:
expected = resolve_hook_files()
if not _same_path(guard_file.file_path, expected.guard_path) or not _same_path(
if not same_path(guard_file.file_path, expected.guard_path) or not same_path(
audit_file.file_path, expected.audit_path
):
raise ValueError("Cline installation received unexpected hook paths")
@@ -113,41 +48,23 @@ def preflight_cline_installation(
config: InstallConfig,
guard_file: HookFile,
audit_file: HookFile,
) -> ClineRuntimePaths:
_validate_install_config(config)
) -> RuntimePaths:
validate_install_config(config, AGENT_KEY, PRODUCT)
if not os.path.isabs(sys.executable):
raise RuntimeError("Cline requires an absolute Python executable path")
_validate_hook_files(guard_file, audit_file)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _hook_change(
file: HookFile, label: str, next_source: Optional[str]
) -> Optional[FileChange]:
original = file.source if file.exists else None
if file.exists and original is None:
raise RuntimeError(f"{label} snapshot is missing source")
return source_change(
file.file_path,
label,
original,
next_source,
0o700,
)
def _present(changes: Sequence[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
return source_change(file.file_path, label, original, next_source, 0o700)
def prepare_cline_installation(
@@ -169,37 +86,18 @@ def prepare_cline_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
changes = _present(
(
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
changes = [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([
_hook_change(guard_file, "Cline PreToolUse hook", guard_wrapper),
_hook_change(audit_file, "Cline PostToolUse hook", audit_wrapper),
)
)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
]),
]
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
validate_hook_tree(os.path.dirname(guard_file.file_path))
return changes
@@ -211,21 +109,14 @@ def commit_cline_installation(changes: Sequence[FileChange]) -> None:
def prepare_cline_uninstall(
files: Sequence[HookFile], agent_id: str = ""
) -> List[FileChange]:
owned = [
file
for file in files
if file.metadata is not None
and (not agent_id or same_agent_id(file.metadata.agent_id, agent_id))
]
changes: List[Optional[FileChange]] = []
for file in owned:
for file in files:
metadata = file.metadata
assert metadata is not None
if metadata is None or (agent_id and not same_agent_id(metadata.agent_id, agent_id)):
continue
assert_wrapper_integrity(file)
changes.append(
_hook_change(file, f"Cline {metadata.kind} hook", None)
)
return _present(changes)
changes.append(_hook_change(file, f"Cline {metadata.kind} hook", None))
return present(changes)
def commit_cline_uninstall(changes: Sequence[FileChange]) -> None:
+9 -220
View File
@@ -2,43 +2,17 @@
from __future__ import annotations
import base64
import os
from typing import Any, Optional
import urllib.parse
from typing import Tuple
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from .cline_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
HookFile,
RuntimeContract,
elydora_dir,
parse_metadata,
same_agent_id,
)
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
def _same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
from ._managed_files import physical_directory_exists, read_physical_file
from ._runtime import expected_runtime_scripts, runtime_contract_exists
from ._strict_json import JsonObject
from .cline_contract import AGENT_KEY, HookFile, RuntimeContract, parse_metadata
def validate_hook_tree(hooks_directory: str) -> None:
physical_directory_exists(
os.path.dirname(hooks_directory), "Cline configuration directory"
)
physical_directory_exists(os.path.dirname(hooks_directory), "Cline configuration directory")
physical_directory_exists(hooks_directory, "Cline hooks directory")
@@ -63,194 +37,9 @@ def require_available_hook_file(file: HookFile) -> None:
)
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"Elydora runtime config {field} is invalid: {config_path}")
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime identity does not match Cline hooks: {config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
(GUARD_SCRIPT, "Elydora guard runtime"),
(AUDIT_SCRIPT, "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Cline agent "
f"{agent_id}: {config_path}"
)
def _valid_contract_paths(contract: RuntimeContract) -> bool:
return (
_same_path(os.path.dirname(contract.agent_directory), elydora_dir())
and _same_path(
contract.guard_path,
os.path.join(contract.agent_directory, GUARD_SCRIPT),
)
and _same_path(
contract.audit_path,
os.path.join(contract.agent_directory, AUDIT_SCRIPT),
)
)
def _expected_scripts(agent_id: str, _config: JsonObject) -> Tuple[str, str]:
return expected_runtime_scripts(AGENT_KEY, agent_id)
def runtime_files_exist(contract: RuntimeContract) -> bool:
if not _valid_contract_paths(contract):
return False
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
contract.agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(contract.agent_directory, "config.json")
key_path = os.path.join(contract.agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if any(item is None for item in (config, key, guard, audit)):
return False
assert config is not None
assert key is not None
assert guard is not None
assert audit is not None
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
expected_audit = generate_hook_script(
org_id="",
agent_id=contract.agent_id,
kid="",
base_url="",
native_payload=True,
agent_name=AGENT_KEY,
)
return (
guard.contents == generate_guard_script(AGENT_KEY, contract.agent_id)
and audit.contents == expected_audit
)
__all__ = [
"read_hook_file",
"require_available_hook_file",
"runtime_files_exist",
"validate_api_origin",
"validate_hook_tree",
"validate_private_key",
"validate_runtime_tree",
]
return runtime_contract_exists(contract, AGENT_KEY, "Cline", _expected_scripts)
+29 -170
View File
@@ -2,133 +2,35 @@
from __future__ import annotations
import base64
import json
import os
import urllib.parse
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory
from elydora.utils import base64url_encode
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES
from ._transaction import FileChange
from ._runtime import (
DEFAULT_BASE_URL,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import write_changes
from .base import AgentPlugin, InstallConfig, PluginStatus
from .codex_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
AUDIT_STATUS,
GUARD_SCRIPT,
GUARD_STATUS,
build_handler,
remove_managed_hooks,
render_document,
runtime_contracts,
runtime_root,
same_path,
)
from .codex_io import (
read_document,
rendered_change,
runtime_change,
runtime_files_exist,
validate_hooks_directory,
validate_runtime_tree,
write_codex_changes,
)
from .hook_template import generate_guard_script, generate_hook_script
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def _agent_paths(config: InstallConfig) -> tuple[str, str, str, str]:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
configured_guard = config.get("guard_script_path", "")
if not same_path(configured_guard, guard_path):
raise ValueError(
f"Elydora guard runtime must use the managed agent directory: {guard_path}"
)
return agent_id, agent_directory, guard_path, audit_path
def _validate_private_key(value: str) -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
"private_key must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or base64url_encode(seed) != value:
raise ValueError("private_key must be a canonical 32-byte base64url value")
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Codex installation requires agent_name {AGENT_KEY}")
_validate_private_key(config["private_key"])
base_url = config["base_url"]
try:
parsed = urllib.parse.urlsplit(base_url)
hostname = parsed.hostname
parsed.port
has_credentials = parsed.username is not None or parsed.password is not None
except ValueError as error:
raise ValueError("base_url must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in base_url or any(
character.isspace() or ord(character) < 32 for character in base_url
)
valid_origin = (
parsed.scheme in ("http", "https")
and bool(parsed.netloc)
and hostname is not None
and not invalid_character
)
if not valid_origin:
raise ValueError("base_url must be an absolute HTTP or HTTPS URL")
if has_credentials or parsed.query or parsed.fragment:
raise ValueError(
"base_url must exclude credentials, query parameters, and fragments"
)
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Codex"
class CodexPlugin(AgentPlugin):
@@ -137,74 +39,39 @@ class CodexPlugin(AgentPlugin):
manages_guard_runtime = True
def preflight_install(self, config: InstallConfig) -> None:
_validate_install_config(config)
validate_install_config(config, AGENT_KEY, PRODUCT)
document = read_document()
validate_hooks_directory(document.file_path)
agent_id, agent_directory, _, _ = _agent_paths(config)
validate_runtime_tree(runtime_root(), agent_directory, agent_id)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
def install(self, config: InstallConfig) -> None:
self.preflight_install(config)
document = read_document()
agent_id, agent_directory, guard_path, audit_path = _agent_paths(config)
paths = resolve_runtime_paths(config)
hooks = remove_managed_hooks(document.hooks)
hooks["PreToolUse"] = [
*hooks.get("PreToolUse", []),
{
"matcher": "*",
"hooks": [build_handler(guard_path, GUARD_STATUS)],
},
{"matcher": "*", "hooks": [build_handler(paths.guard_path, GUARD_STATUS)]},
]
hooks["PostToolUse"] = [
*hooks.get("PostToolUse", []),
{
"matcher": "*",
"hooks": [build_handler(audit_path, AUDIT_STATUS)],
},
{"matcher": "*", "hooks": [build_handler(paths.audit_path, AUDIT_STATUS)]},
]
guard_script = generate_guard_script(AGENT_KEY, agent_id)
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=agent_id,
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
changes = _present(
[
runtime_change(
guard_path,
"Elydora guard runtime",
guard_script,
0o700,
),
runtime_change(
os.path.join(agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
runtime_change(
os.path.join(agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
runtime_change(
audit_path,
"Elydora audit runtime",
audit_script,
0o700,
),
rendered_change(render_document(document, hooks)),
]
)
write_codex_changes(changes, "Install Codex hooks")
changes = [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([rendered_change(render_document(document, hooks))]),
]
write_changes(changes, "Install Codex hooks")
print(f" Codex hooks: {document.file_path}")
print(" Codex trust: run /hooks and approve both Elydora command hooks.")
@@ -215,25 +82,17 @@ class CodexPlugin(AgentPlugin):
if change is None:
return
validate_hooks_directory(document.file_path)
write_codex_changes([change], "Uninstall Codex hooks")
write_changes([change], "Uninstall Codex hooks")
def status(self) -> PluginStatus:
document = read_document()
contracts = runtime_contracts(document.hooks)
if not contracts:
return PluginStatus(
installed=False,
agent=AGENT_KEY,
details="Not installed",
)
return PluginStatus(installed=False, agent=AGENT_KEY, details="Not installed")
installed = runtime_files_exist(contracts)
details = (
f"Config: {document.file_path}"
if installed
else f"Configured at {document.file_path}; runtime files missing"
)
return PluginStatus(
installed=installed,
agent=AGENT_KEY,
details=details,
)
return PluginStatus(installed=installed, agent=AGENT_KEY, details=details)
+27 -177
View File
@@ -2,17 +2,23 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import json
import ntpath
import os
import re
import shlex
import subprocess # nosec B404 - used only for legacy argument rendering
import sys
import subprocess
from typing import Any, Dict, List, Optional, Tuple
from elydora._runtime_paths import runtime_root
from ._runtime import managed_script_reference, same_agent_id, same_path
from ._shell_command import (
encoded_windows_command,
is_python_executable,
parse_encoded_windows_command,
parse_posix_command,
posix_source,
)
from ._strict_json import JsonObject, parse_json_object
@@ -51,164 +57,16 @@ class RuntimeContract:
audit_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def _same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot") if os.name == "nt" else None
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(
system_root,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
def _windows_command(script_path: str) -> str:
source = (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
encoded = base64.b64encode(source.encode("utf-16le")).decode("ascii")
return (
f'"{_windows_powershell_path()}" -NoLogo -NoProfile '
f"-NonInteractive -EncodedCommand {encoded}"
)
def build_handler(script_path: str, status_message: str) -> JsonObject:
return {
"type": "command",
"command": f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}",
"commandWindows": _windows_command(script_path),
"command": posix_source(script_path),
"commandWindows": encoded_windows_command(script_path),
"timeout": HOOK_TIMEOUT_SECONDS,
"statusMessage": status_message,
}
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_posix_command(command: Any) -> Optional[Tuple[str, str]]:
if not isinstance(command, str):
return None
executable = _read_posix_argument(command, 0)
if (
executable is None
or command[executable.next_index : executable.next_index + 1] != " "
):
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_powershell_source(source: str) -> Optional[Tuple[str, str]]:
if not source.startswith("& "):
return None
executable = _read_powershell_argument(source, 2)
if (
executable is None
or source[executable.next_index : executable.next_index + 1] != " "
):
return None
script = _read_powershell_argument(source, executable.next_index + 1)
if script is None or source[script.next_index :] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _parse_windows_command(command: Any) -> Optional[Tuple[str, str]]:
if not isinstance(command, str):
return None
match = re.fullmatch(
r'"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive '
r"-EncodedCommand ([A-Za-z0-9+/]+={0,2})",
command,
)
if (
match is None
or not ntpath.isabs(match.group(1))
or ntpath.basename(match.group(1)).lower() != "powershell.exe"
):
return None
try:
raw = base64.b64decode(match.group(2), validate=True)
if base64.b64encode(raw).decode("ascii") != match.group(2):
return None
source = raw.decode("utf-16le")
except (UnicodeDecodeError, ValueError):
return None
return _parse_powershell_source(source)
def _parse_legacy_commands(handler: JsonObject) -> Optional[Tuple[str, str]]:
command = handler.get("command")
command_windows = handler.get("commandWindows")
@@ -228,17 +86,6 @@ def _parse_legacy_commands(handler: JsonObject) -> Optional[Tuple[str, str]]:
return arguments[0], arguments[1]
def _is_python_executable(file_path: str) -> bool:
return (
re.fullmatch(
r"python(?:[0-9]+(?:\.[0-9]+)*)?(?:\.exe)?",
os.path.basename(file_path),
re.I,
)
is not None
)
def _managed_script_path(
handler: JsonObject,
status_message: str,
@@ -252,8 +99,14 @@ def _managed_script_path(
or handler.get("statusMessage") != status_message
):
return None
posix = _parse_posix_command(handler.get("command"))
windows = _parse_windows_command(handler.get("commandWindows"))
command = handler.get("command")
command_windows = handler.get("commandWindows")
posix = parse_posix_command(command) if isinstance(command, str) else None
windows = (
parse_encoded_windows_command(command_windows)
if isinstance(command_windows, str)
else None
)
if posix is None or windows is None:
legacy = _parse_legacy_commands(handler)
posix = legacy
@@ -263,8 +116,8 @@ def _managed_script_path(
or windows is None
or not os.path.isabs(posix[0])
or not os.path.isabs(posix[1])
or not _is_python_executable(posix[0])
or not _is_python_executable(windows[0])
or not is_python_executable(posix[0])
or not is_python_executable(windows[0])
or not same_path(posix[0], windows[0])
or not same_path(posix[1], windows[1])
):
@@ -278,13 +131,10 @@ def _managed_agent_id(
status_message: str,
) -> Optional[str]:
script_path = _managed_script_path(handler, status_message)
if script_path is None or os.path.basename(script_path) != script_name:
if script_path is None:
return None
agent_directory = os.path.dirname(script_path)
if not same_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
return agent_id if agent_id not in {"", ".", ".."} else None
reference = managed_script_reference(script_path, script_name)
return None if reference is None else reference[0]
def _read_hooks(value: Any, label: str) -> CodexHooks:
@@ -339,7 +189,7 @@ def _remove_from_groups(
for handler in group["hooks"]:
managed_id = _managed_agent_id(handler, script_name, status_message)
remove = managed_id is not None and (
not agent_id or _same_agent_id(managed_id, agent_id)
not agent_id or same_agent_id(managed_id, agent_id)
)
if not remove:
handlers.append(handler)
+7 -130
View File
@@ -4,17 +4,11 @@ from __future__ import annotations
import os
import stat
from typing import Any, List, Optional
from typing import List, Optional
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SOURCE_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from ._transaction import FileChange, source_change, write_changes
from ._managed_files import MAX_SOURCE_BYTES, physical_directory_exists, read_physical_file
from ._runtime import rendered_source_change, runtime_present
from ._transaction import FileChange
from .codex_contract import (
AGENT_KEY,
CONFIG_FILE,
@@ -26,12 +20,6 @@ from .codex_contract import (
)
def _same_agent_id(value: Any, expected: str) -> bool:
return isinstance(value, str) and os.path.normcase(value) == os.path.normcase(
expected
)
def codex_home_path() -> str:
configured = os.environ.get("CODEX_HOME")
if configured is None or configured == "":
@@ -64,130 +52,19 @@ def read_document() -> CodexDocument:
def validate_hooks_directory(file_path: str) -> None:
directory = os.path.dirname(file_path)
physical_directory_exists(directory, "Codex hooks directory")
physical_directory_exists(os.path.dirname(file_path), "Codex hooks directory")
def rendered_change(rendered: RenderedDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
return rendered_source_change(
rendered.document.file_path,
"Codex user hooks",
rendered.document.raw,
rendered.next_source,
0o600,
MAX_SOURCE_BYTES,
)
def runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
snapshot = read_physical_file(file_path, label, maximum_bytes)
return source_change(
file_path,
label,
None if snapshot is None else snapshot.contents,
next_source,
mode,
maximum_bytes,
)
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path,
"Elydora runtime config",
MAX_CONFIG_BYTES,
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents,
f"Elydora runtime config at {file_path}",
)
def validate_runtime_identity(agent_directory: str, agent_id: str) -> bool:
config_path_value = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path_value)
artifacts = [
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
]
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
]
artifact_exists = any(artifact_states)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return False
if config.get("agent_name") != AGENT_KEY or not _same_agent_id(
config.get("agent_id"),
agent_id,
):
raise ValueError(
f"Elydora runtime config identity does not match Codex agent {agent_id}: "
f"{config_path_value}"
)
return True
def validate_runtime_tree(
runtime_root: str, agent_directory: str, agent_id: str
) -> None:
if not physical_directory_exists(runtime_root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory,
"Elydora agent runtime directory",
):
return
validate_runtime_identity(agent_directory, agent_id)
def runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
for contract in contracts:
agent_directory = os.path.dirname(contract.guard_path)
if not physical_directory_exists(
agent_directory,
"Elydora agent runtime directory",
):
continue
config = _read_runtime_config(os.path.join(agent_directory, "config.json"))
if (
config is None
or config.get("agent_name") != AGENT_KEY
or not _same_agent_id(config.get("agent_id"), contract.agent_id)
):
continue
if all(
(
physical_file_exists(contract.guard_path, "Elydora guard runtime"),
physical_file_exists(contract.audit_path, "Elydora audit runtime"),
physical_file_exists(
os.path.join(agent_directory, "private.key"),
"Elydora private key",
),
)
):
return True
return False
def write_codex_changes(changes: List[FileChange], label: str) -> None:
write_changes(changes, label)
return any(runtime_present(contract, AGENT_KEY) for contract in contracts)
+24 -104
View File
@@ -7,10 +7,19 @@ import json
import os
import re
import shlex
import sys
from typing import Any, Dict, List, Optional, Tuple
from elydora._runtime_paths import runtime_root
from ._managed_files import FileSnapshot
from ._runtime import same_agent_id, same_path
from ._shell_command import (
is_python_executable,
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
)
from ._strict_json import parse_json_object
from .copilot_schema import CopilotHooks, validate_hooks
@@ -69,124 +78,35 @@ class RuntimeContract:
audit_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
@dataclass(frozen=True)
class _ManagedEntry:
agent_id: str
script_path: str
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def build_handler(script_path: str) -> JsonObject:
return {
"type": "command",
"bash": f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}",
"powershell": (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
),
"bash": posix_source(script_path),
"powershell": powershell_source(script_path),
"timeoutSec": HOOK_TIMEOUT_SECONDS,
}
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_generated_bash(command: Any) -> Optional[Tuple[str, str]]:
if not isinstance(command, str):
return None
executable = _read_posix_argument(command, 0)
if executable is None or command[executable.next_index:executable.next_index + 1] != " ":
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(
command: str, start: int
) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_generated_powershell(command: Any) -> Optional[Tuple[str, str]]:
if not isinstance(command, str) or not command.startswith("& "):
return None
executable = _read_powershell_argument(command, 2)
if executable is None or command[executable.next_index:executable.next_index + 1] != " ":
return None
script = _read_powershell_argument(command, executable.next_index + 1)
if script is None or command[script.next_index:] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _exact_handler_keys(handler: JsonObject) -> bool:
return set(handler) == {"type", "bash", "powershell", "timeoutSec"}
def _is_python_executable(file_path: str) -> bool:
return os.path.isabs(file_path) and re.fullmatch(
r"(?:python|pypy)(?:[0-9]+(?:\.[0-9]+)*)?(?:\.exe)?",
os.path.basename(file_path),
re.IGNORECASE,
) is not None
def _parse_generated_bash(command: Any) -> Optional[Tuple[str, str]]:
return parse_posix_command(command) if isinstance(command, str) else None
def _parse_generated_powershell(command: Any) -> Optional[Tuple[str, str]]:
return parse_powershell_source(command) if isinstance(command, str) else None
def _absolute_python(file_path: str) -> bool:
return os.path.isabs(file_path) and is_python_executable(file_path)
def _current_script_path(handler: JsonObject) -> Optional[str]:
@@ -201,7 +121,7 @@ def _current_script_path(handler: JsonObject) -> Optional[str]:
if (
bash is None
or powershell is None
or not _is_python_executable(bash[0])
or not _absolute_python(bash[0])
or not same_path(bash[0], powershell[0])
or not same_path(bash[1], powershell[1])
):
@@ -227,7 +147,7 @@ def _prior_script_path(handler: JsonObject) -> Optional[str]:
len(arguments) != 2
or shlex.join(arguments) != bash_command
or powershell is None
or not _is_python_executable(arguments[0])
or not _absolute_python(arguments[0])
or not same_path(arguments[0], powershell[0])
or not same_path(arguments[1], powershell[1])
):
@@ -3,48 +3,29 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
import sys
from typing import List, Optional, Sequence, Tuple
from typing import Optional, Sequence, Tuple
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import (
FileChange,
FilePrecondition,
file_change,
source_change,
write_changes,
)
from .base import InstallConfig
from .copilot_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
CopilotSources,
RenderedDocument,
runtime_root,
from ._managed_files import MAX_SOURCE_BYTES
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
resolve_runtime_paths,
runtime_file_changes,
same_path,
)
from .copilot_io import (
require_hooks_enabled,
validate_api_origin,
validate_private_key,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, FilePrecondition, source_change, write_changes
from .base import InstallConfig
from .copilot_contract import AGENT_KEY, CopilotSources, RenderedDocument
from .copilot_io import require_hooks_enabled
from .copilot_schema import validate_javascript_regexes
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
class CopilotRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
PRODUCT = "GitHub Copilot CLI"
@dataclass(frozen=True)
@@ -53,95 +34,25 @@ class PreparedCopilotInstallation:
preconditions: Tuple[FilePrecondition, ...]
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> CopilotRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return CopilotRuntimePaths(
agent_id,
agent_directory,
guard_path,
audit_path,
)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"GitHub Copilot CLI installation requires agent_name {AGENT_KEY}"
)
if not os.path.isabs(sys.executable):
raise RuntimeError(
"GitHub Copilot CLI requires an absolute Python executable path"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
def preflight_copilot_installation(
config: InstallConfig,
sources: CopilotSources,
) -> CopilotRuntimePaths:
) -> RuntimePaths:
if not sources.user.file_path:
raise ValueError(
"GitHub Copilot CLI installation requires a user hook path"
)
raise ValueError("GitHub Copilot CLI installation requires a user hook path")
require_hooks_enabled(sources)
hook_sources = [sources.user.hooks]
if sources.legacy is not None:
hook_sources.append(sources.legacy.hooks)
validate_javascript_regexes(hook_sources)
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
if not os.path.isabs(sys.executable):
raise RuntimeError("GitHub Copilot CLI requires an absolute Python executable path")
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, "Copilot")
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _document_change(rendered: RenderedDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
@@ -156,12 +67,6 @@ def _document_change(rendered: RenderedDocument) -> Optional[FileChange]:
)
def _present(
changes: Sequence[Optional[FileChange]],
) -> Tuple[FileChange, ...]:
return tuple(change for change in changes if change is not None)
def _document_preconditions(
rendered: Sequence[RenderedDocument],
) -> Tuple[FilePrecondition, ...]:
@@ -177,16 +82,9 @@ def _document_preconditions(
)
def _settings_preconditions(
sources: CopilotSources,
) -> Tuple[FilePrecondition, ...]:
def _settings_preconditions(sources: CopilotSources) -> Tuple[FilePrecondition, ...]:
return tuple(
FilePrecondition(
item.file_path,
item.label,
item.snapshot,
MAX_SOURCE_BYTES,
)
FilePrecondition(item.file_path, item.label, item.snapshot, MAX_SOURCE_BYTES)
for item in sources.settings_preconditions
)
@@ -206,9 +104,7 @@ def _validate_rendered_documents(
if same_path(item.document.file_path, document.file_path)
]
if len(matches) != 1:
raise ValueError(
"GitHub Copilot rendered source set contains unexpected paths"
)
raise ValueError("GitHub Copilot rendered source set contains unexpected paths")
def prepare_copilot_installation(
@@ -223,33 +119,18 @@ def prepare_copilot_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
changes = _present((
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
*(_document_change(item) for item in rendered),
))
document_changes = [
change for change in (_document_change(item) for item in rendered)
if change is not None
]
changes = (
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*document_changes,
)
preconditions = (
*_settings_preconditions(sources),
*_document_preconditions(rendered),
@@ -257,20 +138,17 @@ def prepare_copilot_installation(
return PreparedCopilotInstallation(changes, preconditions)
def commit_copilot_installation(
prepared: PreparedCopilotInstallation,
) -> None:
write_changes(
prepared.changes,
"Install GitHub Copilot hooks",
prepared.preconditions,
)
def commit_copilot_installation(prepared: PreparedCopilotInstallation) -> None:
write_changes(prepared.changes, "Install GitHub Copilot hooks", prepared.preconditions)
def prepare_copilot_uninstall(
rendered: Sequence[RenderedDocument],
) -> Tuple[FileChange, ...]:
return _present(tuple(_document_change(item) for item in rendered))
return tuple(
change for change in (_document_change(item) for item in rendered)
if change is not None
)
def commit_copilot_uninstall(changes: Sequence[FileChange]) -> None:
+13 -193
View File
@@ -2,40 +2,29 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import os
from typing import Any, List, Optional, Tuple
import urllib.parse
from typing import List, Optional, Tuple
from ._jsonc import parse_jsonc
from ._managed_files import (
FileSnapshot,
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
MAX_SOURCE_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._runtime import expected_runtime_scripts, runtime_contract_exists
from ._strict_json import JsonObject, parse_json_object
from .copilot_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
CONFIG_FILE,
GUARD_SCRIPT,
CopilotDocument,
CopilotSources,
RuntimeContract,
SourcePrecondition,
create_document,
parse_document,
runtime_root,
same_agent_id,
same_path,
)
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
@@ -216,186 +205,17 @@ def require_hooks_enabled(sources: CopilotSources) -> None:
)
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(
f"{label} must be an absolute HTTP or HTTPS URL"
) from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject,
contract: RuntimeContract,
config_path: str,
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_string(config.get("org_id"), "org_id", config_path)
_require_string(config.get("kid"), "kid", config_path)
agent_id = _require_string(config.get("agent_id"), "agent_id", config_path)
if (
not same_agent_id(agent_id, contract.agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime identity does not match Copilot hooks: {config_path}"
)
if "token" in config:
_require_string(config.get("token"), "token", config_path)
base_url = _require_string(config.get("base_url"), "base_url", config_path)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = runtime_root()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
(GUARD_SCRIPT, "Elydora guard runtime"),
(AUDIT_SCRIPT, "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Copilot agent "
f"{agent_id}: {config_path}"
)
def _valid_contract_paths(contract: RuntimeContract) -> bool:
agent_directory = os.path.dirname(contract.guard_path)
return (
same_path(os.path.dirname(agent_directory), runtime_root())
and same_path(
contract.guard_path,
os.path.join(agent_directory, GUARD_SCRIPT),
)
and same_path(
contract.audit_path,
os.path.join(agent_directory, AUDIT_SCRIPT),
)
)
def _runtime_contract_exists(contract: RuntimeContract) -> bool:
if not _valid_contract_paths(contract):
return False
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract, config_path)
validate_private_key(key.contents, "Elydora private key")
expected_guard = generate_guard_script(AGENT_KEY, contract.agent_id)
expected_audit = generate_hook_script(
org_id="",
agent_id=contract.agent_id,
kid="",
base_url="",
native_payload=True,
agent_name=AGENT_KEY,
)
return guard.contents == expected_guard and audit.contents == expected_audit
def _expected_scripts(agent_id: str, _config: JsonObject) -> Tuple[str, str]:
return expected_runtime_scripts(AGENT_KEY, agent_id)
def runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
return any(
runtime_contract_exists(
contract,
AGENT_KEY,
"Copilot",
_expected_scripts,
)
for contract in contracts
)
+42 -156
View File
@@ -2,78 +2,36 @@
from __future__ import annotations
import base64
import json
import os
import urllib.parse
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory
from elydora.utils import base64url_encode
from ._transaction import FileChange
from ._managed_files import physical_directory_exists
from ._runtime import (
DEFAULT_BASE_URL,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_private_key,
validate_runtime_tree,
)
from ._transaction import write_changes
from .base import AgentPlugin, InstallConfig, PluginStatus
from .cursor_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
build_handler,
remove_managed_hooks,
render_document,
runtime_contracts,
runtime_root,
same_path,
)
from .cursor_io import (
read_document,
rendered_change,
physical_directory_exists,
physical_file_exists,
require_runtime_directory,
runtime_change,
runtime_files_exist,
validate_config_directory,
validate_runtime_identity,
write_cursor_changes,
)
from .hook_template import generate_guard_script, generate_hook_script
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token", "")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def _agent_paths(config: InstallConfig) -> tuple[str, str, str]:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
configured_guard = config.get("guard_script_path", "")
if not same_path(configured_guard, guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return agent_id, agent_directory, guard_path
PRODUCT = "Cursor"
def _validate_install_config(config: InstallConfig) -> None:
@@ -83,27 +41,14 @@ def _validate_install_config(config: InstallConfig) -> None:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Cursor installation requires agent_name {AGENT_KEY}")
private_key = config["private_key"]
try:
padded = private_key + "=" * ((4 - len(private_key) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
"private_key must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or base64url_encode(seed) != private_key:
raise ValueError("private_key must be a canonical 32-byte base64url value")
base_url = config.get("base_url", "https://api.elydora.com")
validate_private_key(config["private_key"])
base_url = config.get("base_url", DEFAULT_BASE_URL)
if not isinstance(base_url, str):
raise ValueError("base_url must be a string")
parsed = urllib.parse.urlsplit(base_url)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError("base_url must be an absolute HTTP or HTTPS URL")
token = config.get("token", "")
if not isinstance(token, str):
if not isinstance(config.get("token", ""), str):
raise ValueError("token must be a string")
@@ -116,58 +61,32 @@ class CursorPlugin(AgentPlugin):
_validate_install_config(config)
read_document()
validate_config_directory()
agent_id, agent_directory, guard_path = _agent_paths(config)
if not physical_directory_exists(runtime_root()):
return
if not physical_directory_exists(agent_directory):
return
identity_exists = validate_runtime_identity(
os.path.join(agent_directory, "config.json"),
agent_id,
)
runtime_exists = False
for file_path, label in (
(guard_path, "Elydora guard runtime"),
(os.path.join(agent_directory, AUDIT_SCRIPT), "Elydora audit runtime"),
(os.path.join(agent_directory, "private.key"), "Elydora private key"),
(os.path.join(agent_directory, "chain-state.json"), "Elydora chain state"),
(os.path.join(agent_directory, "status-cache.json"), "Elydora status cache"),
(os.path.join(agent_directory, "error.log"), "Elydora error log"),
):
runtime_exists = physical_file_exists(file_path, label) or runtime_exists
if runtime_exists and not identity_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
def install(self, config: InstallConfig) -> None:
self.preflight_install(config)
document = read_document()
agent_id, agent_directory, guard_path = _agent_paths(config)
require_runtime_directory(agent_directory)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
runtime_config_path = os.path.join(agent_directory, "config.json")
validate_runtime_identity(runtime_config_path, agent_id)
paths = resolve_runtime_paths(config)
if not physical_directory_exists(
paths.agent_directory, "Elydora agent runtime directory"
):
raise FileNotFoundError(
f"Elydora agent runtime directory is missing: {paths.agent_directory}"
)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
hooks = remove_managed_hooks(document.hooks)
hooks["preToolUse"] = [
*hooks.get("preToolUse", []),
build_handler(guard_path),
]
hooks["postToolUse"] = [
*hooks.get("postToolUse", []),
build_handler(audit_path),
]
hooks["preToolUse"] = [*hooks.get("preToolUse", []), build_handler(paths.guard_path)]
hooks["postToolUse"] = [*hooks.get("postToolUse", []), build_handler(paths.audit_path)]
hooks["postToolUseFailure"] = [
*hooks.get("postToolUseFailure", []),
build_handler(audit_path),
build_handler(paths.audit_path),
]
hook_script = generate_hook_script(
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=agent_id,
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
success_output="{}\n",
fail_closed=True,
native_payload=True,
@@ -175,67 +94,34 @@ class CursorPlugin(AgentPlugin):
)
guard_script = generate_guard_script(
AGENT_KEY,
agent_id,
paths.agent_id,
success_output='{"permission":"allow"}\n',
fail_closed=True,
deny_protocol="cursor",
)
changes = _present([
runtime_change(
guard_path,
"Elydora guard runtime",
guard_script,
0o700,
),
runtime_change(
runtime_config_path,
"Elydora runtime config",
_json_source(_runtime_config(config, agent_id)),
0o600,
),
runtime_change(
os.path.join(agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
),
runtime_change(
audit_path,
"Elydora audit runtime",
hook_script,
0o700,
),
rendered_change(render_document(document, hooks)),
])
write_cursor_changes(changes, "Install Cursor hooks")
changes = [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([rendered_change(render_document(document, hooks))]),
]
write_changes(changes, "Install Cursor hooks")
print(f" Cursor hooks: {document.file_path}")
def uninstall(self, agent_id: str = "") -> None:
document = read_document()
hooks = remove_managed_hooks(document.hooks, agent_id)
change = rendered_change(render_document(document, hooks))
write_cursor_changes(
[] if change is None else [change],
"Uninstall Cursor hooks",
)
if change is not None:
write_changes([change], "Uninstall Cursor hooks")
def status(self) -> PluginStatus:
document = read_document()
contracts = runtime_contracts(document.hooks)
if not contracts:
return PluginStatus(
installed=False,
agent=AGENT_KEY,
details="Not installed",
)
return PluginStatus(installed=False, agent=AGENT_KEY, details="Not installed")
installed = runtime_files_exist(contracts)
details = (
f"Config: {document.file_path}"
if installed
else f"Configured at {document.file_path}; runtime files missing"
)
return PluginStatus(
installed=installed,
agent=AGENT_KEY,
details=details,
)
return PluginStatus(installed=installed, agent=AGENT_KEY, details=details)
+18 -116
View File
@@ -7,8 +7,17 @@ import json
import os
import re
import sys
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
from elydora._runtime_paths import runtime_root
from ._runtime import managed_script_reference, same_agent_id, same_path
from ._shell_command import (
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
)
from ._strict_json import JsonObject, parse_json_object
@@ -43,42 +52,8 @@ class RuntimeContract:
audit_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def _same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def build_handler(script_path: str) -> JsonObject:
if os.name == "nt":
command = (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
else:
command = f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
command = powershell_source(script_path) if os.name == "nt" else posix_source(script_path)
return {
"command": command,
"timeout": HOOK_TIMEOUT_SECONDS,
@@ -86,73 +61,6 @@ def build_handler(script_path: str) -> JsonObject:
}
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if (
executable is None
or command[executable.next_index : executable.next_index + 1] != " "
):
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(
command: str,
start: int,
) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_powershell_command(command: str) -> Optional[Tuple[str, str]]:
if not command.startswith("& "):
return None
executable = _read_powershell_argument(command, 2)
if (
executable is None
or command[executable.next_index : executable.next_index + 1] != " "
):
return None
script = _read_powershell_argument(command, executable.next_index + 1)
if script is None or command[script.next_index :] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _legacy_script_path(command: str) -> Optional[str]:
quoted = re.fullmatch(r'"([^"\r\n]+)" ([^\r\n]+)', command)
if quoted and same_path(quoted.group(1), sys.executable):
@@ -169,9 +77,9 @@ def _managed_script_path(handler: JsonObject) -> Optional[str]:
and handler.get("failClosed") is True
):
parsed = (
_parse_powershell_command(handler["command"])
parse_powershell_source(handler["command"])
if os.name == "nt"
else _parse_posix_command(handler["command"])
else parse_posix_command(handler["command"])
)
if (
parsed is not None
@@ -185,18 +93,12 @@ def _managed_script_path(handler: JsonObject) -> Optional[str]:
return _legacy_script_path(handler["command"])
def _managed_agent_id(
handler: JsonObject,
script_name: str,
) -> Optional[str]:
def _managed_agent_id(handler: JsonObject, script_name: str) -> Optional[str]:
script_path = _managed_script_path(handler)
if script_path is None or os.path.basename(script_path) != script_name:
if script_path is None:
return None
agent_directory = os.path.dirname(script_path)
if not same_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
return agent_id if agent_id not in {"", ".", ".."} else None
reference = managed_script_reference(script_path, script_name)
return None if reference is None else reference[0]
def _read_hooks(value: Any, label: str) -> CursorHooks:
@@ -256,7 +158,7 @@ def remove_managed_hooks(
for handler in result.get(event, []):
managed_id = _managed_agent_id(handler, script_name)
remove = managed_id is not None and (
not agent_id or _same_agent_id(managed_id, agent_id)
not agent_id or same_agent_id(managed_id, agent_id)
)
if not remove:
handlers.append(handler)
+11 -164
View File
@@ -3,19 +3,18 @@
from __future__ import annotations
import os
import stat
from typing import Any, List, Optional
from typing import List, Optional
from ._transaction import FileChange, source_change, write_changes
from ._managed_files import MAX_SOURCE_BYTES, physical_directory_exists, read_physical_file
from ._runtime import rendered_source_change, runtime_present
from ._transaction import FileChange
from .cursor_contract import (
AGENT_KEY,
CursorDocument,
JsonObject,
RenderedDocument,
RuntimeContract,
create_document,
parse_document,
parse_json_object,
)
@@ -26,180 +25,28 @@ def config_path() -> str:
return os.path.join(os.path.expanduser("~"), ".cursor", CONFIG_FILE)
def _read_optional_physical(file_path: str, label: str) -> Optional[str]:
try:
metadata = os.lstat(file_path)
except FileNotFoundError:
return None
except OSError as error:
raise OSError(f"Inspect {label} at {file_path}: {error}") from error
if not stat.S_ISREG(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise OSError(f"{label} path is not a physical file: {file_path}")
try:
with open(file_path, "r", encoding="utf-8", newline="") as file:
return file.read()
except OSError as error:
raise OSError(f"Read {label} at {file_path}: {error}") from error
def read_document() -> CursorDocument:
file_path = config_path()
raw = _read_optional_physical(file_path, "Cursor user hooks")
return create_document(file_path) if raw is None else parse_document(file_path, raw)
snapshot = read_physical_file(file_path, "Cursor user hooks", MAX_SOURCE_BYTES)
if snapshot is None:
return create_document(file_path)
return parse_document(file_path, snapshot.contents)
def validate_config_directory() -> None:
directory = os.path.dirname(config_path())
try:
metadata = os.lstat(directory)
except FileNotFoundError:
return
except OSError as error:
raise OSError(f"Inspect Cursor hooks directory at {directory}: {error}") from error
if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise OSError(f"Cursor hooks directory is not a physical directory: {directory}")
physical_directory_exists(os.path.dirname(config_path()), "Cursor hooks directory")
def rendered_change(rendered: RenderedDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
return rendered_source_change(
rendered.document.file_path,
"Cursor user hooks",
rendered.document.raw,
rendered.next_source,
0o600,
)
def runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
) -> Optional[FileChange]:
original = _read_optional_physical(file_path, label)
return source_change(file_path, label, original, next_source, mode)
def physical_file_exists(file_path: str, label: str) -> bool:
try:
metadata = os.lstat(file_path)
except FileNotFoundError:
return False
except OSError as error:
raise OSError(f"Inspect {label} at {file_path}: {error}") from error
if not stat.S_ISREG(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise OSError(f"{label} path is not a physical file: {file_path}")
return True
def require_runtime(file_path: str, label: str) -> None:
if not file_path:
raise ValueError(f"{label} path is required")
if not physical_file_exists(file_path, label):
raise FileNotFoundError(f"{label} is missing: {file_path}")
def physical_directory_exists(directory: str) -> bool:
try:
metadata = os.lstat(directory)
except FileNotFoundError:
return False
except OSError as error:
raise OSError(
f"Inspect Elydora agent runtime directory at {directory}: {error}"
) from error
if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise OSError(
f"Elydora agent runtime path is not a physical directory: {directory}"
)
return True
def require_runtime_directory(directory: str) -> None:
if not physical_directory_exists(directory):
raise FileNotFoundError(
f"Elydora agent runtime directory is missing: {directory}"
)
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
raw = _read_optional_physical(file_path, "Elydora runtime config")
if raw is None:
return None
return parse_json_object(raw, f"Elydora runtime config at {file_path}")
def validate_runtime_identity(
file_path: str,
agent_id: str,
) -> bool:
config = _read_runtime_config(file_path)
if config is None:
return False
if config.get("agent_name") != AGENT_KEY or not _same_agent_id(
config.get("agent_id"), agent_id
):
raise ValueError(
f"Elydora runtime config identity does not match Cursor agent {agent_id}: "
f"{file_path}"
)
return True
def _same_agent_id(value: Any, expected: str) -> bool:
return isinstance(value, str) and os.path.normcase(value) == os.path.normcase(
expected
)
def runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
for contract in contracts:
agent_directory = os.path.dirname(contract.guard_path)
if not physical_directory_exists(agent_directory):
continue
config_path_value = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path_value)
if (
config is None
or config.get("agent_name") != AGENT_KEY
or not _same_agent_id(config.get("agent_id"), contract.agent_id)
):
continue
if physical_file_exists(
contract.guard_path,
"Elydora guard runtime",
) and physical_file_exists(
contract.audit_path,
"Elydora audit runtime",
) and physical_file_exists(
os.path.join(agent_directory, "private.key"),
"Elydora private key",
):
return True
return False
def _ensure_physical_directory(directory: str, label: str) -> None:
try:
os.makedirs(directory, mode=0o700, exist_ok=True)
metadata = os.lstat(directory)
except OSError as error:
raise OSError(f"Prepare {label} directory at {directory}: {error}") from error
if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise OSError(f"{label} directory is not a physical directory: {directory}")
def write_cursor_changes(changes: List[FileChange], label: str) -> None:
effective = [change for change in changes if change.original != change.next_source]
if not effective:
return
directories = {
os.path.normcase(os.path.abspath(os.path.dirname(change.file_path))):
os.path.dirname(change.file_path)
for change in effective
}
for directory in directories.values():
_ensure_physical_directory(directory, label)
write_changes(effective, label)
return any(runtime_present(contract, AGENT_KEY) for contract in contracts)
+1 -1
View File
@@ -8,13 +8,13 @@ from typing import Dict, List, Optional, Tuple
from ._jsonc import JsonPathPart, JsoncEditor
from ._managed_files import FileSnapshot
from ._runtime import same_path
from .droid_contract import (
TOOL_EVENTS,
DroidHookMap,
JsonObject,
managed_removals,
read_hook_map,
same_path,
)
from .droid_policy import DroidPolicyState
+17 -116
View File
@@ -12,6 +12,16 @@ import subprocess # nosec B404
import sys
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
from elydora._runtime_paths import runtime_root
from ._runtime import managed_script_reference, same_agent_id, same_path
from ._shell_command import (
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
)
AGENT_KEY = "droid"
GUARD_SCRIPT = "guard.py"
@@ -22,7 +32,6 @@ TOOL_EVENTS = ("PreToolUse", "PostToolUse")
_HANDLER_KEYS = {"command", "timeout", "type"}
_GROUP_KEYS = {"hooks", "matcher"}
_REGEX_TIMEOUT_SECONDS = 10
_WINDOWS_EXIT_SUFFIX = "; exit $LASTEXITCODE"
_REGEX_VALIDATOR = """import fs from 'node:fs';
const entries = JSON.parse(fs.readFileSync(0, 'utf8'));
for (const entry of entries) {
@@ -55,39 +64,14 @@ class ManagedRemoval:
remove_group: bool
def elydora_dir() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_power_shell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def build_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Factory Droid hook commands require absolute executable and script paths"
)
if os.name == "nt":
return (
f"& {_quote_power_shell(sys.executable)} "
f"{_quote_power_shell(script_path)}{_WINDOWS_EXIT_SUFFIX}"
)
return f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
return powershell_source(script_path)
return posix_source(script_path)
def build_group(script_path: str) -> JsonObject:
@@ -214,83 +198,6 @@ def validate_javascript_regexes(sources: Sequence[DroidHookMap]) -> None:
)
def _read_power_shell_argument(
command: str,
start: int,
) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return value, index + 1
return None
_POSIX_APOSTROPHE = "'\"'\"'"
def _read_posix_argument(command: str, start: int) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command.startswith(_POSIX_APOSTROPHE, index):
value += "'"
index += len(_POSIX_APOSTROPHE)
continue
if command[index] == "'":
return value, index + 1
value += command[index]
index += 1
return None
def _parse_two_arguments(
command: str,
start: int,
) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, start)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_posix_argument(command, executable[1] + 1)
if script is None or script[1] != len(command):
return None
if not executable[0] or not script[0]:
return None
return executable[0], script[0]
def _parse_power_shell_command(command: str) -> Optional[Tuple[str, str]]:
if not command.startswith("& "):
return None
executable = _read_power_shell_argument(command, 2)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_power_shell_argument(command, executable[1] + 1)
if (
script is None
or command[script[1]:] != _WINDOWS_EXIT_SUFFIX
or not executable[0]
or not script[0]
):
return None
return executable[0], script[0]
def _parse_legacy_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(r'"([^"\r\n]+)" "([^"\r\n]+)"', command)
return (match.group(1), match.group(2)) if match else None
@@ -301,8 +208,8 @@ def _parse_generated_command(
include_legacy: bool,
) -> Optional[Tuple[str, str]]:
if os.name != "nt":
return _parse_two_arguments(command, 0)
current = _parse_power_shell_command(command)
return parse_posix_command(command)
current = parse_powershell_source(command)
return current or (
_parse_legacy_windows_command(command) if include_legacy else None
)
@@ -326,14 +233,8 @@ def managed_agent_id(
parsed = _parse_generated_command(command, include_legacy)
if parsed is None or not same_path(parsed[0], sys.executable):
return None
script_path = parsed[1]
if os.path.basename(script_path) != script_name:
return None
agent_directory = os.path.dirname(script_path)
if not same_path(os.path.dirname(agent_directory), elydora_dir()):
return None
agent_id = os.path.basename(agent_directory)
return agent_id if agent_id not in {"", ".", ".."} else None
reference = managed_script_reference(parsed[1], script_name)
return None if reference is None else reference[0]
def _exact_owned_group(group: JsonObject, indexes: Tuple[int, ...]) -> bool:
@@ -397,7 +298,7 @@ def runtime_contracts(hooks: DroidHookMap) -> List[RuntimeContract]:
for agent_id in sorted(guards):
if not any(same_agent_id(agent_id, audit_id) for audit_id in audits):
continue
root = os.path.join(elydora_dir(), agent_id)
root = os.path.join(runtime_root(), agent_id)
contracts.append(RuntimeContract(
agent_id,
os.path.join(root, GUARD_SCRIPT),
+44 -186
View File
@@ -3,21 +3,21 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
import sys
from typing import Optional, Sequence, Tuple
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import (
FileChange,
FilePrecondition,
file_change,
source_change,
write_changes,
from ._managed_files import MAX_SOURCE_BYTES
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
resolve_runtime_paths,
runtime_file_changes,
same_path,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, FilePrecondition, source_change, write_changes
from .base import InstallConfig
from .droid_config import (
DroidDocument,
@@ -26,30 +26,12 @@ from .droid_config import (
installation_documents,
source_documents,
)
from .droid_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
elydora_dir,
same_path,
validate_javascript_regexes,
)
from .droid_io import (
require_hooks_enabled,
validate_api_origin,
validate_private_key,
validate_runtime_tree,
)
from .droid_contract import AGENT_KEY, validate_javascript_regexes
from .droid_io import require_hooks_enabled
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
class DroidRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
PRODUCT = "Factory Droid"
@dataclass(frozen=True)
@@ -74,90 +56,20 @@ def _source_label(document: DroidDocument) -> str:
return "Factory Droid user hooks"
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> DroidRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(elydora_dir(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return DroidRuntimePaths(
agent_id,
agent_directory,
guard_path,
audit_path,
)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"Factory Droid installation requires agent_name {AGENT_KEY}"
)
if not os.path.isabs(sys.executable):
raise RuntimeError(
"Factory Droid requires an absolute Python executable path"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
def preflight_droid_installation(
config: InstallConfig,
sources: DroidSources,
) -> DroidRuntimePaths:
) -> RuntimePaths:
require_hooks_enabled(sources)
validate_javascript_regexes([
document.hooks for document in source_documents(sources)
])
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_javascript_regexes([document.hooks for document in source_documents(sources)])
validate_install_config(config, AGENT_KEY, PRODUCT)
if not os.path.isabs(sys.executable):
raise RuntimeError("Factory Droid requires an absolute Python executable path")
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _document_change(rendered: RenderedDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
@@ -173,10 +85,11 @@ def _document_change(rendered: RenderedDocument) -> Optional[FileChange]:
)
def _present(
changes: Sequence[Optional[FileChange]],
) -> Tuple[FileChange, ...]:
return tuple(change for change in changes if change is not None)
def _document_changes(rendered: Sequence[RenderedDocument]) -> Tuple[FileChange, ...]:
return tuple(
change for change in (_document_change(item) for item in rendered)
if change is not None
)
def _validate_rendered_documents(
@@ -188,18 +101,15 @@ def _validate_rendered_documents(
raise ValueError("Factory Droid rendered source set is incomplete")
for document in expected:
matches = [
item
for item in rendered
item for item in rendered
if same_path(item.document.file_path, document.file_path)
]
if len(matches) != 1:
raise ValueError(
"Factory Droid rendered source set contains unexpected paths"
)
raise ValueError("Factory Droid rendered source set contains unexpected paths")
def _source_preconditions(
sources: DroidSources,
def _unchanged_preconditions(
documents: Sequence[DroidDocument],
changed_paths: Sequence[str],
) -> Tuple[FilePrecondition, ...]:
return tuple(
@@ -209,11 +119,8 @@ def _source_preconditions(
document.snapshot,
MAX_SOURCE_BYTES,
)
for document in source_documents(sources)
if not any(
same_path(document.file_path, changed)
for changed in changed_paths
)
for document in documents
if not any(same_path(document.file_path, changed) for changed in changed_paths)
)
@@ -229,84 +136,35 @@ def prepare_droid_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
document_changes = _present(tuple(
_document_change(item) for item in rendered
))
changes = _present((
_runtime_change(
paths.guard_path,
"Elydora guard runtime",
guard_script,
0o700,
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path,
"Elydora audit runtime",
audit_script,
0o700,
),
document_changes = _document_changes(rendered)
changes = (
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*document_changes,
))
)
changed_paths = [change.file_path for change in document_changes]
preconditions = (
*_source_preconditions(sources, changed_paths),
*_unchanged_preconditions(source_documents(sources), changed_paths),
*sources.policy.preconditions,
)
return PreparedDroidInstallation(changes, tuple(preconditions))
return PreparedDroidInstallation(changes, preconditions)
def commit_droid_installation(
prepared: PreparedDroidInstallation,
) -> None:
write_changes(
prepared.changes,
"Install Factory Droid hooks",
prepared.preconditions,
)
def commit_droid_installation(prepared: PreparedDroidInstallation) -> None:
write_changes(prepared.changes, "Install Factory Droid hooks", prepared.preconditions)
def prepare_droid_uninstall(
rendered: Sequence[RenderedDocument],
) -> PreparedDroidUninstall:
changes = _present(tuple(_document_change(item) for item in rendered))
changes = _document_changes(rendered)
changed_paths = [change.file_path for change in changes]
preconditions = tuple(
FilePrecondition(
item.document.file_path,
_source_label(item.document),
item.document.snapshot,
MAX_SOURCE_BYTES,
)
for item in rendered
if not any(
same_path(item.document.file_path, changed)
for changed in changed_paths
)
)
return PreparedDroidUninstall(changes, preconditions)
documents = [item.document for item in rendered]
return PreparedDroidUninstall(changes, _unchanged_preconditions(documents, changed_paths))
def commit_droid_uninstall(prepared: PreparedDroidUninstall) -> None:
write_changes(
prepared.changes,
"Uninstall Factory Droid hooks",
prepared.preconditions,
)
write_changes(prepared.changes, "Uninstall Factory Droid hooks", prepared.preconditions)
+18 -212
View File
@@ -2,21 +2,13 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import os
from typing import Any, Optional
import urllib.parse
from typing import List, Optional, Tuple
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
MAX_SOURCE_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from ._managed_files import MAX_SOURCE_BYTES, physical_directory_exists, read_physical_file
from ._runtime import expected_runtime_scripts, runtime_contract_exists
from ._strict_json import JsonObject
from .droid_config import (
DroidDocument,
DroidSources,
@@ -27,18 +19,8 @@ from .droid_config import (
hook_block,
parse_document,
)
from .droid_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
RuntimeContract,
elydora_dir,
same_agent_id,
same_path,
)
from .droid_contract import AGENT_KEY, RuntimeContract
from .droid_policy import read_droid_policy
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
@@ -128,196 +110,20 @@ def require_hooks_enabled(sources: DroidSources) -> None:
)
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(
f"{label} must be an absolute HTTP or HTTPS URL"
) from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
def _expected_scripts(agent_id: str, _config: JsonObject) -> Tuple[str, str]:
return expected_runtime_scripts(AGENT_KEY, agent_id)
def runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
return any(
runtime_contract_exists(
contract,
AGENT_KEY,
"Factory Droid",
_expected_scripts,
)
for contract in contracts
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject,
contract: RuntimeContract,
config_path: str,
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": '
f"{config_path}"
)
_require_string(config.get("org_id"), "org_id", config_path)
_require_string(config.get("kid"), "kid", config_path)
agent_id = _require_string(config.get("agent_id"), "agent_id", config_path)
if (
not same_agent_id(agent_id, contract.agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime identity does not match Factory Droid hooks: "
f"{config_path}"
)
if "token" in config:
_require_string(config.get("token"), "token", config_path)
base_url = _require_string(config.get("base_url"), "base_url", config_path)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path,
"Elydora runtime config",
MAX_CONFIG_BYTES,
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents,
f"Elydora runtime config at {file_path}",
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory,
"Elydora agent runtime directory",
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
(GUARD_SCRIPT, "Elydora guard runtime"),
(AUDIT_SCRIPT, "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Factory Droid agent "
f"{agent_id}: {config_path}"
)
def _valid_contract_paths(contract: RuntimeContract) -> bool:
agent_directory = os.path.dirname(contract.guard_path)
return (
same_path(os.path.dirname(agent_directory), elydora_dir())
and same_path(
contract.guard_path,
os.path.join(agent_directory, GUARD_SCRIPT),
)
and same_path(
contract.audit_path,
os.path.join(agent_directory, AUDIT_SCRIPT),
)
)
def _runtime_contract_exists(contract: RuntimeContract) -> bool:
if not _valid_contract_paths(contract):
return False
root = elydora_dir()
agent_directory = os.path.dirname(contract.guard_path)
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory,
"Elydora agent runtime directory",
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract, config_path)
validate_private_key(key.contents, "Elydora private key")
expected_guard = generate_guard_script(AGENT_KEY, contract.agent_id)
expected_audit = generate_hook_script(
org_id="",
agent_id=contract.agent_id,
kid="",
base_url="",
native_payload=True,
agent_name=AGENT_KEY,
)
return guard.contents == expected_guard and audit.contents == expected_audit
def runtime_files_exist(contracts: list[RuntimeContract]) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
def display_config_path(sources: DroidSources) -> str:
+30 -157
View File
@@ -2,14 +2,28 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import ntpath
import os
import re
import sys
from typing import Optional, Tuple
from ._runtime import managed_script_reference, same_path
from ._shell_command import (
decode_powershell_source,
encode_powershell_source,
is_powershell_executable,
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
quote_powershell,
read_powershell_argument,
windows_powershell_path,
)
_ENCODED_FLAGS = " -NoLogo -NoProfile -NonInteractive -EncodedCommand "
@dataclass(frozen=True)
class GeminiRuntimeReference:
@@ -17,163 +31,33 @@ class GeminiRuntimeReference:
script_path: str
def same_gemini_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_gemini_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot")
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(
system_root,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
def _windows_command(script_path: str) -> str:
source = (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
encoded = base64.b64encode(source.encode("utf-16le")).decode("ascii")
return (
f"& {_quote_powershell(_windows_powershell_path())} "
"-NoLogo -NoProfile -NonInteractive -EncodedCommand "
f"{encoded}"
)
def build_gemini_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Gemini CLI hook commands require absolute executable and script paths"
)
if os.name == "nt":
return _windows_command(script_path)
return f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
def _read_posix_argument(command: str, start: int) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return value, index + 1
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_posix_argument(command, executable[1] + 1)
if script is None or script[1] != len(command):
return None
return executable[0], script[0]
def _read_powershell_argument(
command: str, start: int
) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return value, index + 1
return None
def _parse_powershell_source(source: str) -> Optional[Tuple[str, str]]:
if not source.startswith("& "):
return None
executable = _read_powershell_argument(source, 2)
if executable is None or executable[1] >= len(source):
return None
if source[executable[1]] != " ":
return None
script = _read_powershell_argument(source, executable[1] + 1)
if script is None or source[script[1] :] != "; exit $LASTEXITCODE":
return None
return executable[0], script[0]
encoded = encode_powershell_source(powershell_source(script_path))
return f"& {quote_powershell(windows_powershell_path())}{_ENCODED_FLAGS}{encoded}"
return posix_source(script_path)
def _parse_windows_command(command: str) -> Optional[Tuple[str, str]]:
if not command.startswith("& "):
return None
powershell = _read_powershell_argument(command, 2)
if (
powershell is None
or not ntpath.isabs(powershell[0])
or ntpath.basename(powershell[0]).lower() != "powershell.exe"
):
powershell = read_powershell_argument(command, 2)
if powershell is None or not is_powershell_executable(powershell[0]):
return None
prefix = " -NoLogo -NoProfile -NonInteractive -EncodedCommand "
if not command.startswith(prefix, powershell[1]):
if not command.startswith(_ENCODED_FLAGS, powershell[1]):
return None
encoded = command[powershell[1] + len(prefix) :]
if re.fullmatch(r"[A-Za-z0-9+/]+={0,2}", encoded) is None:
return None
try:
payload = base64.b64decode(encoded, validate=True)
if base64.b64encode(payload).decode("ascii") != encoded:
return None
if len(payload) % 2:
return None
source = payload.decode("utf-16le")
except (ValueError, UnicodeDecodeError):
return None
return _parse_powershell_source(source)
source = decode_powershell_source(command[powershell[1] + len(_ENCODED_FLAGS) :])
return None if source is None else parse_powershell_source(source)
def _parse_legacy_command(
command: str, script_name: str
) -> Optional[Tuple[str, str]]:
def _parse_legacy_command(command: str, script_name: str) -> Optional[Tuple[str, str]]:
if script_name == "guard.py":
match = re.fullmatch(r'"([^"\r\n]+)" ([^\r\n]+)', command)
if match is None or not same_gemini_path(match.group(1), sys.executable):
if match is None or not same_path(match.group(1), sys.executable):
return None
return match.group(1), match.group(2)
if "\r" in command or "\n" in command:
@@ -186,21 +70,10 @@ def gemini_runtime_reference(
script_name: str,
include_legacy: bool = False,
) -> Optional[GeminiRuntimeReference]:
parsed = _parse_posix_command(command) or _parse_windows_command(command)
parsed = parse_posix_command(command) or _parse_windows_command(command)
if parsed is None and include_legacy:
parsed = _parse_legacy_command(command, script_name)
if (
parsed is None
or not same_gemini_path(parsed[0], sys.executable)
or not os.path.isabs(parsed[1])
or os.path.basename(parsed[1]) != script_name
):
if parsed is None or not same_path(parsed[0], sys.executable):
return None
agent_directory = os.path.dirname(parsed[1])
runtime_root = os.path.join(os.path.expanduser("~"), ".elydora")
if not same_gemini_path(os.path.dirname(agent_directory), runtime_root):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return GeminiRuntimeReference(agent_id, parsed[1])
reference = managed_script_reference(parsed[1], script_name)
return None if reference is None else GeminiRuntimeReference(*reference)
@@ -7,11 +7,11 @@ import math
import os
from typing import Any, Dict, List, Optional, Tuple
from ._runtime import same_agent_id
from .gemini_command import (
GeminiRuntimeReference,
build_gemini_command,
gemini_runtime_reference,
same_gemini_agent_id,
)
@@ -63,10 +63,6 @@ class ManagedRemoval:
remove_group: bool
def elydora_dir() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def _optional_string(value: JsonObject, field: str, label: str) -> None:
if field in value and not isinstance(value[field], str):
raise ValueError(f'{label} field "{field}" must be a string')
@@ -286,7 +282,7 @@ def managed_gemini_removals(
is not None
and (
agent_id is None
or same_gemini_agent_id(reference.agent_id, agent_id)
or same_agent_id(reference.agent_id, agent_id)
)
)
if indexes:
@@ -2,159 +2,58 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional
from typing import List
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, file_change, write_changes
from .base import InstallConfig
from .gemini_command import same_gemini_path
from .gemini_config import GeminiDocument, RenderedGeminiDocument
from .gemini_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
elydora_dir,
)
from .gemini_io import (
rendered_change,
validate_api_origin,
validate_private_key,
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_install_config,
validate_runtime_tree,
)
from .hook_template import generate_guard_script, generate_hook_script
from ._transaction import FileChange, write_changes
from .base import InstallConfig
from .gemini_config import GeminiDocument, RenderedGeminiDocument
from .gemini_contract import AGENT_KEY
from .gemini_io import rendered_change
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
class GeminiRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> GeminiRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(elydora_dir(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_gemini_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return GeminiRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"Gemini CLI installation requires agent_name {AGENT_KEY}"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Gemini CLI"
def preflight_gemini_installation(
config: InstallConfig, document: GeminiDocument
) -> GeminiRuntimePaths:
) -> RuntimePaths:
if not document.file_path:
raise ValueError("Gemini CLI installation requires a settings path")
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_gemini_installation(
config: InstallConfig,
paths: GeminiRuntimePaths,
paths: RuntimePaths,
rendered: RenderedGeminiDocument,
) -> List[FileChange]:
guard_script = generate_guard_script(
AGENT_KEY,
paths.agent_id,
success_output="{}\n",
)
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id, success_output="{}\n")
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
success_output="{}\n",
native_payload=True,
agent_name=AGENT_KEY,
)
return _present([
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
rendered_change(rendered),
])
return [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([rendered_change(rendered)]),
]
def commit_gemini_installation(changes: List[FileChange]) -> None:
+13 -204
View File
@@ -2,34 +2,19 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Optional
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
MAX_SOURCE_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from ._transaction import FileChange, source_change, write_changes
from .gemini_command import same_gemini_agent_id, same_gemini_path
from ._managed_files import MAX_SOURCE_BYTES, physical_directory_exists, read_physical_file
from ._runtime import rendered_source_change, runtime_contract_exists
from ._transaction import FileChange, write_changes
from .gemini_config import (
GeminiDocument,
RenderedGeminiDocument,
create_gemini_document,
parse_gemini_document,
)
from .gemini_contract import (
AGENT_KEY,
CONFIG_FILE,
GeminiRuntimeContract,
elydora_dir,
)
from .gemini_contract import AGENT_KEY, CONFIG_FILE, GeminiRuntimeContract
def gemini_configuration_directory() -> str:
@@ -44,33 +29,21 @@ def gemini_settings_path() -> str:
def read_gemini_document() -> GeminiDocument:
file_path = gemini_settings_path()
physical_directory_exists(
os.path.dirname(file_path), "Gemini CLI configuration directory"
)
snapshot = read_physical_file(
file_path, "Gemini CLI user settings", MAX_SOURCE_BYTES
)
physical_directory_exists(os.path.dirname(file_path), "Gemini CLI configuration directory")
snapshot = read_physical_file(file_path, "Gemini CLI user settings", MAX_SOURCE_BYTES)
if snapshot is None:
return create_gemini_document(file_path)
return parse_gemini_document(
exists=True,
file_path=file_path,
raw=snapshot.contents,
)
return parse_gemini_document(exists=True, file_path=file_path, raw=snapshot.contents)
def rendered_change(
rendered: RenderedGeminiDocument,
) -> Optional[FileChange]:
def rendered_change(rendered: RenderedGeminiDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
return rendered_source_change(
rendered.document.file_path,
"Gemini CLI user settings",
rendered.document.raw if rendered.document.exists else None,
rendered.next_source,
0o600,
MAX_SOURCE_BYTES,
)
@@ -80,171 +53,7 @@ def write_gemini_document(rendered: RenderedGeminiDocument) -> None:
write_changes([change], "Uninstall Gemini CLI hooks")
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
def gemini_runtime_files_exist(contracts: List[GeminiRuntimeContract]) -> bool:
return any(
runtime_contract_exists(contract, AGENT_KEY, "Gemini CLI") for contract in contracts
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(
value: Any, field: str, config_path: str
) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_gemini_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime identity does not match Gemini CLI hooks: "
f"{config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_gemini_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Gemini CLI agent "
f"{agent_id}: {config_path}"
)
def _runtime_contract_exists(contract: GeminiRuntimeContract) -> bool:
root = elydora_dir()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_gemini_path(os.path.dirname(agent_directory), root)
or not same_gemini_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
return bool(guard.contents) and bool(audit.contents)
def gemini_runtime_files_exist(
contracts: List[GeminiRuntimeContract],
) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
+17 -171
View File
@@ -2,16 +2,23 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import ntpath
import os
import re
import shlex
import subprocess # nosec B404 - used only for legacy command rendering
import subprocess
import sys
from typing import Optional, Tuple
from ._runtime import managed_script_reference
from ._shell_command import (
encoded_windows_command,
is_python_executable,
parse_encoded_windows_command,
parse_posix_command,
posix_source,
)
@dataclass(frozen=True)
class GrokRuntimeReference:
@@ -19,154 +26,14 @@ class GrokRuntimeReference:
script_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_grok_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_grok_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot") if os.name == "nt" else None
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(
system_root,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
def _windows_command(script_path: str) -> str:
source = (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
encoded = base64.b64encode(source.encode("utf-16le")).decode("ascii")
return (
f'"{_windows_powershell_path()}" -NoLogo -NoProfile '
f"-NonInteractive -EncodedCommand {encoded}"
)
def build_grok_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Grok hook commands require absolute executable and script paths"
)
if os.name == "nt":
return _windows_command(script_path)
return f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if executable is None or command[executable.next_index : executable.next_index + 1] != " ":
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(
command: str, start: int
) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_powershell_source(source: str) -> Optional[Tuple[str, str]]:
if not source.startswith("& "):
return None
executable = _read_powershell_argument(source, 2)
if executable is None or source[executable.next_index : executable.next_index + 1] != " ":
return None
script = _read_powershell_argument(source, executable.next_index + 1)
if script is None or source[script.next_index :] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _parse_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(
r'"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive '
r"-EncodedCommand ([A-Za-z0-9+/]+={0,2})",
command,
)
if (
match is None
or not ntpath.isabs(match.group(1))
or ntpath.basename(match.group(1)).lower() != "powershell.exe"
):
return None
try:
raw = base64.b64decode(match.group(2), validate=True)
if base64.b64encode(raw).decode("ascii") != match.group(2):
return None
source = raw.decode("utf-16le")
except (UnicodeDecodeError, ValueError):
return None
return _parse_powershell_source(source)
return encoded_windows_command(script_path)
return posix_source(script_path)
def _parse_legacy_posix_command(command: str) -> Optional[Tuple[str, str]]:
@@ -193,40 +60,19 @@ def _parse_legacy_windows_command(command: str) -> Optional[Tuple[str, str]]:
return arguments[0], arguments[1]
def _is_python_executable(file_path: str) -> bool:
return (
re.fullmatch(
r"python(?:[0-9]+(?:\.[0-9]+)*)?(?:\.exe)?",
os.path.basename(file_path),
re.IGNORECASE,
)
is not None
)
def grok_runtime_reference(
command: str, script_name: str
) -> Optional[GrokRuntimeReference]:
parsed = (
_parse_posix_command(command)
or _parse_windows_command(command)
parse_posix_command(command)
or parse_encoded_windows_command(command)
or _parse_legacy_posix_command(command)
or _parse_legacy_windows_command(command)
)
if parsed is None:
return None
executable, script_path = parsed
if (
not os.path.isabs(executable)
or not os.path.isabs(script_path)
or not _is_python_executable(executable)
or os.path.basename(script_path) != script_name
):
if not os.path.isabs(executable) or not is_python_executable(executable):
return None
agent_directory = os.path.dirname(script_path)
if not same_grok_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return GrokRuntimeReference(agent_id, script_path)
reference = managed_script_reference(script_path, script_name)
return None if reference is None else GrokRuntimeReference(*reference)
+3 -4
View File
@@ -7,13 +7,12 @@ import json
import os
from typing import Any, Dict, List, Optional, Tuple
from ._runtime import same_agent_id, same_path
from ._strict_json import JsonObject, parse_json_object
from .grok_command import (
GrokRuntimeReference,
build_grok_command,
grok_runtime_reference,
same_grok_agent_id,
same_grok_path,
)
@@ -183,7 +182,7 @@ def _remove_from_groups(
for handler in group["hooks"]:
reference = _managed_reference(handler, script_name)
remove = reference is not None and (
not agent_id or same_grok_agent_id(reference.agent_id, agent_id)
not agent_id or same_agent_id(reference.agent_id, agent_id)
)
if not remove:
kept.append(handler)
@@ -286,7 +285,7 @@ def grok_runtime_contracts(hooks: GrokHooks) -> List[GrokRuntimeContract]:
failure = failures.get(key, [])
if len(guard) != 1 or len(success) != 1 or len(failure) != 1:
continue
if not same_grok_path(success[0].script_path, failure[0].script_path):
if not same_path(success[0].script_path, failure[0].script_path):
continue
contracts.append(GrokRuntimeContract(
agent_id=guard[0].agent_id,
+27 -139
View File
@@ -2,168 +2,56 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional
from typing import List
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, file_change, source_change, write_changes
from .base import InstallConfig
from .grok_command import runtime_root, same_grok_path
from .grok_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
GrokDocument,
RenderedGrokDocument,
)
from .grok_io import (
validate_api_origin,
validate_private_key,
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_install_config,
validate_runtime_tree,
)
from .hook_template import generate_guard_script, generate_hook_script
from ._transaction import FileChange, write_changes
from .base import InstallConfig
from .grok_contract import AGENT_KEY, GrokDocument, RenderedGrokDocument
from .grok_io import rendered_change
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
@dataclass(frozen=True)
class GrokRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> GrokRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_grok_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return GrokRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Grok installation requires agent_name {AGENT_KEY}")
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Grok"
def preflight_grok_installation(
config: InstallConfig, document: GrokDocument
) -> GrokRuntimePaths:
) -> RuntimePaths:
if not document.config_path:
raise ValueError("Grok installation requires a hook configuration path")
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def rendered_change(
rendered: RenderedGrokDocument,
) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
rendered.document.config_path,
"Grok user hooks",
rendered.document.raw,
rendered.next_source,
0o600,
MAX_SOURCE_BYTES,
)
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_grok_installation(
config: InstallConfig,
paths: GrokRuntimePaths,
paths: RuntimePaths,
rendered: RenderedGrokDocument,
) -> List[FileChange]:
guard_script = generate_guard_script(
AGENT_KEY, paths.agent_id, deny_protocol="grok"
)
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id, deny_protocol="grok")
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
return _present([
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
rendered_change(rendered),
])
return [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([rendered_change(rendered)]),
]
def commit_grok_installation(changes: List[FileChange]) -> None:
@@ -171,7 +59,7 @@ def commit_grok_installation(changes: List[FileChange]) -> None:
def prepare_grok_uninstall(rendered: RenderedGrokDocument) -> List[FileChange]:
return _present([rendered_change(rendered)])
return present([rendered_change(rendered)])
def commit_grok_uninstall(changes: List[FileChange]) -> None:
+14 -175
View File
@@ -2,24 +2,17 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Optional
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from .grok_command import runtime_root, same_grok_agent_id, same_grok_path
from ._managed_files import physical_directory_exists, read_physical_file
from ._runtime import rendered_source_change, runtime_contract_exists
from ._transaction import FileChange
from .grok_contract import (
AGENT_KEY,
GrokDocument,
GrokRuntimeContract,
RenderedGrokDocument,
create_grok_document,
parse_grok_document,
)
@@ -41,9 +34,7 @@ def grok_config_path() -> str:
def read_grok_document() -> GrokDocument:
config_path = grok_config_path()
hooks_directory = os.path.dirname(config_path)
physical_directory_exists(
os.path.dirname(hooks_directory), "Grok home directory"
)
physical_directory_exists(os.path.dirname(hooks_directory), "Grok home directory")
physical_directory_exists(hooks_directory, "Grok hooks directory")
snapshot = read_physical_file(config_path, "Grok user hooks")
if snapshot is None:
@@ -51,168 +42,16 @@ def read_grok_document() -> GrokDocument:
return parse_grok_document(config_path, snapshot.contents)
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_grok_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime identity does not match Grok hooks: {config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
def rendered_change(rendered: RenderedGrokDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
return rendered_source_change(
rendered.document.config_path,
"Grok user hooks",
rendered.document.raw,
rendered.next_source,
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = runtime_root()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
]
artifact_exists = any(artifact_states)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_grok_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime config identity does not match Grok agent {agent_id}: "
f"{config_path}"
)
def _runtime_contract_exists(contract: GrokRuntimeContract) -> bool:
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_grok_path(os.path.dirname(agent_directory), root)
or not same_grok_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
return bool(guard.contents) and bool(audit.contents)
def grok_runtime_files_exist(contracts: List[GrokRuntimeContract]) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
return any(runtime_contract_exists(contract, AGENT_KEY, "Grok") for contract in contracts)
@@ -5,11 +5,8 @@ from __future__ import annotations
import sys
from .error_log_template import ERROR_LOG_RUNTIME
from .guard_template import generate_guard_script
from .runtime_reader_template import PROTECTED_RUNTIME_READER
__all__ = ["generate_guard_script", "generate_hook_script"]
def generate_hook_script(
*,
+17 -171
View File
@@ -2,16 +2,23 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import ntpath
import os
import re
import shlex
import subprocess # nosec B404 - used only for legacy command rendering
import subprocess
import sys
from typing import Optional, Tuple
from ._runtime import managed_script_reference
from ._shell_command import (
encoded_windows_command,
is_python_executable,
parse_encoded_windows_command,
parse_posix_command,
posix_source,
)
@dataclass(frozen=True)
class KimiRuntimeReference:
@@ -19,154 +26,14 @@ class KimiRuntimeReference:
script_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
def runtime_root() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def same_kimi_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_kimi_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot") if os.name == "nt" else None
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(
system_root,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
def _windows_command(script_path: str) -> str:
source = (
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
encoded = base64.b64encode(source.encode("utf-16le")).decode("ascii")
return (
f'"{_windows_powershell_path()}" -NoLogo -NoProfile '
f"-NonInteractive -EncodedCommand {encoded}"
)
def build_kimi_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Kimi hook commands require absolute executable and script paths"
)
if os.name == "nt":
return _windows_command(script_path)
return f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if executable is None or command[executable.next_index : executable.next_index + 1] != " ":
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(
command: str, start: int
) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_powershell_source(source: str) -> Optional[Tuple[str, str]]:
if not source.startswith("& "):
return None
executable = _read_powershell_argument(source, 2)
if executable is None or source[executable.next_index : executable.next_index + 1] != " ":
return None
script = _read_powershell_argument(source, executable.next_index + 1)
if script is None or source[script.next_index :] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _parse_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(
r'"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive '
r"-EncodedCommand ([A-Za-z0-9+/]+={0,2})",
command,
)
if (
match is None
or not ntpath.isabs(match.group(1))
or ntpath.basename(match.group(1)).lower() != "powershell.exe"
):
return None
try:
raw = base64.b64decode(match.group(2), validate=True)
if base64.b64encode(raw).decode("ascii") != match.group(2):
return None
source = raw.decode("utf-16le")
except (UnicodeDecodeError, ValueError):
return None
return _parse_powershell_source(source)
return encoded_windows_command(script_path)
return posix_source(script_path)
def _parse_legacy_posix_command(command: str) -> Optional[Tuple[str, str]]:
@@ -193,40 +60,19 @@ def _parse_legacy_windows_command(command: str) -> Optional[Tuple[str, str]]:
return arguments[0], arguments[1]
def _is_python_executable(file_path: str) -> bool:
return (
re.fullmatch(
r"python(?:[0-9]+(?:\.[0-9]+)*)?(?:\.exe)?",
os.path.basename(file_path),
re.IGNORECASE,
)
is not None
)
def kimi_runtime_reference(
command: str, script_name: str
) -> Optional[KimiRuntimeReference]:
parsed = (
_parse_posix_command(command)
or _parse_windows_command(command)
parse_posix_command(command)
or parse_encoded_windows_command(command)
or _parse_legacy_posix_command(command)
or _parse_legacy_windows_command(command)
)
if parsed is None:
return None
executable, script_path = parsed
if (
not os.path.isabs(executable)
or not os.path.isabs(script_path)
or not _is_python_executable(executable)
or os.path.basename(script_path) != script_name
):
if not os.path.isabs(executable) or not is_python_executable(executable):
return None
agent_directory = os.path.dirname(script_path)
if not same_kimi_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return KimiRuntimeReference(agent_id, script_path)
reference = managed_script_reference(script_path, script_name)
return None if reference is None else KimiRuntimeReference(*reference)
+3 -4
View File
@@ -10,12 +10,11 @@ from typing import Any, Dict, FrozenSet, List, Optional, Tuple
from tomlkit import aot, document, dumps, inline_table, parse, table
from tomlkit.items import AoT, Array
from ._runtime import same_agent_id, same_path
from .kimi_command import (
KimiRuntimeReference,
build_kimi_command,
kimi_runtime_reference,
same_kimi_agent_id,
same_kimi_path,
)
@@ -216,7 +215,7 @@ def remove_managed_kimi_hooks(
else None
)
remove = reference is not None and (
not agent_id or same_kimi_agent_id(reference.agent_id, agent_id)
not agent_id or same_agent_id(reference.agent_id, agent_id)
)
if not remove:
result.append(hook)
@@ -318,7 +317,7 @@ def kimi_runtime_contracts(
failure = failures.get(key, [])
if len(guard) != 1 or len(success) != 1 or len(failure) != 1:
continue
if not same_kimi_path(success[0].script_path, failure[0].script_path):
if not same_path(success[0].script_path, failure[0].script_path):
continue
contracts.append(
KimiRuntimeContract(
+27 -142
View File
@@ -2,120 +2,39 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, file_change, source_change, write_changes
from .base import InstallConfig
from .hook_template import generate_guard_script, generate_hook_script
from .kimi_command import runtime_root, same_kimi_path
from .kimi_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
KimiDocument,
RenderedKimiDocument,
)
from .kimi_io import (
validate_api_origin,
validate_private_key,
from ._managed_files import MAX_SOURCE_BYTES
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, source_change, write_changes
from .base import InstallConfig
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .kimi_contract import AGENT_KEY, KimiDocument, RenderedKimiDocument
@dataclass(frozen=True)
class KimiRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> KimiRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
configured_guard = config.get("guard_script_path", "")
if not same_kimi_path(configured_guard, guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return KimiRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Kimi installation requires agent_name {AGENT_KEY}")
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Kimi"
def preflight_kimi_installation(
config: InstallConfig, documents: List[KimiDocument]
) -> KimiRuntimePaths:
) -> RuntimePaths:
if not documents:
raise ValueError("Kimi installation requires at least one hook contract")
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(
file_path,
label,
next_source,
mode,
maximum_bytes,
)
def rendered_change(
rendered: RenderedKimiDocument,
) -> Optional[FileChange]:
def rendered_change(rendered: RenderedKimiDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
source = rendered.document
@@ -129,66 +48,32 @@ def rendered_change(
)
def _present(changes: List[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_kimi_installation(
config: InstallConfig,
paths: KimiRuntimePaths,
paths: RuntimePaths,
rendered: List[RenderedKimiDocument],
) -> List[FileChange]:
config_changes = _present([rendered_change(item) for item in rendered])
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
audit_script = generate_hook_script(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
runtime_changes = _present(
[
_runtime_change(
paths.guard_path,
"Elydora guard runtime",
guard_script,
0o700,
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path,
"Elydora audit runtime",
audit_script,
0o700,
),
]
)
return [*runtime_changes, *config_changes]
return [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([rendered_change(item) for item in rendered]),
]
def commit_kimi_installation(changes: List[FileChange]) -> None:
write_changes(changes, "Install Kimi hooks")
def prepare_kimi_uninstall(
rendered: List[RenderedKimiDocument],
) -> List[FileChange]:
return _present([rendered_change(item) for item in rendered])
def prepare_kimi_uninstall(rendered: List[RenderedKimiDocument]) -> List[FileChange]:
return present([rendered_change(item) for item in rendered])
def commit_kimi_uninstall(changes: List[FileChange]) -> None:
+5 -189
View File
@@ -2,24 +2,11 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from .kimi_command import (
runtime_root,
same_kimi_agent_id,
same_kimi_path,
)
from ._managed_files import physical_directory_exists, read_physical_file
from ._runtime import runtime_contract_exists, same_path
from .kimi_contract import (
AGENT_KEY,
LEGACY_EVENTS,
@@ -76,7 +63,7 @@ def resolve_kimi_contracts() -> List[KimiContract]:
legacy_home = os.path.join(home, ".kimi")
stable = _stable_contract(os.path.join(stable_home, "config.toml"))
legacy = _legacy_contract(os.path.join(legacy_home, "config.toml"))
if same_kimi_path(stable.config_path, legacy.config_path):
if same_path(stable.config_path, legacy.config_path):
return [stable]
stable_detected = explicit_home is not None or _entry_exists(
@@ -101,176 +88,5 @@ def read_kimi_documents() -> List[KimiDocument]:
return [_read_kimi_document(contract) for contract in resolve_kimi_contracts()]
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"),
validate=True,
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(
value: Any, field: str, config_path: str
) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject,
expected_agent_id: str,
config_path: str,
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_kimi_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime identity does not match Kimi hooks: {config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path,
"Elydora runtime config",
MAX_CONFIG_BYTES,
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents,
f"Elydora runtime config at {file_path}",
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = runtime_root()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = [
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
]
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
]
artifact_exists = any(artifact_states)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_kimi_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
f"Elydora runtime config identity does not match Kimi agent {agent_id}: "
f"{config_path}"
)
def _runtime_contract_exists(contract: KimiRuntimeContract) -> bool:
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_kimi_path(os.path.dirname(agent_directory), root)
or not same_kimi_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
return bool(guard.contents) and bool(audit.contents)
def kimi_runtime_files_exist(contracts: List[KimiRuntimeContract]) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
return any(runtime_contract_exists(contract, AGENT_KEY, "Kimi") for contract in contracts)
+26 -89
View File
@@ -2,15 +2,13 @@
from __future__ import annotations
import json
import os
import shlex
import stat
import subprocess
import sys
import tempfile
from typing import Any, Dict, List, Optional, Tuple
from ._file_io import read_json, regular_file_exists, remove_file, write_json_atomic, write_text_atomic
from .base import AgentPlugin, InstallConfig, PluginStatus
from .hook_template import generate_hook_script
@@ -42,11 +40,11 @@ class KiroCliPlugin(AgentPlugin):
raise ValueError("agent_id is required")
if not guard_path:
raise ValueError("guard_script_path is required")
if not _regular_file_exists(guard_path, "Elydora guard runtime"):
if not regular_file_exists(guard_path, "Elydora guard runtime"):
raise FileNotFoundError(f"Elydora guard runtime is missing: {guard_path}")
v2_settings = _read_json(V2_AGENT_PATH, "Kiro CLI v2 agent config") or {}
v3_settings = _read_json(V3_HOOKS_PATH, "Kiro CLI v3 hooks config") or {}
v2_settings = read_json(V2_AGENT_PATH, "Kiro CLI v2 agent config") or {}
v3_settings = read_json(V3_HOOKS_PATH, "Kiro CLI v3 hooks config") or {}
v2_hooks = _hooks_object(v2_settings, "Kiro CLI v2 agent config")
current_v3_hooks = _v3_hooks(v3_settings)
@@ -108,22 +106,28 @@ class KiroCliPlugin(AgentPlugin):
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
)
_write_json_atomic(os.path.join(agent_dir, "config.json"), runtime_config, 0o600)
_write_text_atomic(
write_json_atomic(
os.path.join(agent_dir, "config.json"),
runtime_config,
0o600,
"Elydora runtime config",
)
write_text_atomic(
os.path.join(agent_dir, "private.key"),
config.get("private_key", ""),
0o600,
"Elydora private key",
)
_write_text_atomic(hook_path, hook_script, 0o700)
_write_json_atomic(V2_AGENT_PATH, next_v2_settings, 0o600)
_write_json_atomic(V3_HOOKS_PATH, next_v3_settings, 0o600)
write_text_atomic(hook_path, hook_script, 0o700, "Elydora audit runtime")
write_json_atomic(V2_AGENT_PATH, next_v2_settings, 0o600, "Kiro CLI v2 agent config")
write_json_atomic(V3_HOOKS_PATH, next_v3_settings, 0o600, "Kiro CLI v3 hooks config")
print('Kiro CLI v2: start with "kiro-cli --agent elydora-audit".')
print('Kiro CLI v3: start with "kiro-cli --v3"; global hooks load automatically.')
def uninstall(self, agent_id: str = "") -> None:
v2_settings = _read_json(V2_AGENT_PATH, "Kiro CLI v2 agent config")
v3_settings = _read_json(V3_HOOKS_PATH, "Kiro CLI v3 hooks config")
v2_settings = read_json(V2_AGENT_PATH, "Kiro CLI v2 agent config")
v3_settings = read_json(V3_HOOKS_PATH, "Kiro CLI v3 hooks config")
next_v2: Optional[Tuple[JsonObject, bool]] = None
if v2_settings is not None:
@@ -153,22 +157,22 @@ class KiroCliPlugin(AgentPlugin):
if next_v2 is not None:
settings, remove = next_v2
if remove:
_remove_file(V2_AGENT_PATH, "Kiro CLI v2 agent config")
remove_file(V2_AGENT_PATH, "Kiro CLI v2 agent config")
else:
_write_json_atomic(V2_AGENT_PATH, settings, 0o600)
write_json_atomic(V2_AGENT_PATH, settings, 0o600, "Kiro CLI v2 agent config")
if next_v3 is not None:
settings, remove = next_v3
if remove:
_remove_file(V3_HOOKS_PATH, "Kiro CLI v3 hooks config")
remove_file(V3_HOOKS_PATH, "Kiro CLI v3 hooks config")
else:
_write_json_atomic(V3_HOOKS_PATH, settings, 0o600)
write_json_atomic(V3_HOOKS_PATH, settings, 0o600, "Kiro CLI v3 hooks config")
print("Elydora hooks uninstalled from Kiro CLI.")
def status(self) -> PluginStatus:
v2_settings = _read_json(V2_AGENT_PATH, "Kiro CLI v2 agent config")
v3_settings = _read_json(V3_HOOKS_PATH, "Kiro CLI v3 hooks config")
v2_settings = read_json(V2_AGENT_PATH, "Kiro CLI v2 agent config")
v3_settings = read_json(V3_HOOKS_PATH, "Kiro CLI v3 hooks config")
contracts: List[HookContract] = []
if v2_settings is not None:
@@ -204,63 +208,6 @@ class KiroCliPlugin(AgentPlugin):
return PluginStatus(installed=installed, agent=AGENT_KEY, details=details)
def _read_json(path: str, label: str) -> Optional[JsonObject]:
try:
with open(path, "r", encoding="utf-8") as file:
raw = file.read()
except FileNotFoundError:
return None
except OSError as error:
raise OSError(f"Read {label} at {path}: {error}") from error
try:
value = json.loads(raw)
except json.JSONDecodeError as error:
raise ValueError(f"Failed to parse {label} at {path}: {error}") from error
if not isinstance(value, dict):
raise ValueError(f"{label} at {path} must contain a JSON object")
return value
def _write_text_atomic(path: str, content: str, mode: int) -> None:
directory = os.path.dirname(path) or "."
os.makedirs(directory, exist_ok=True)
descriptor, temporary_path = tempfile.mkstemp(
dir=directory,
prefix=f".{os.path.basename(path)}.",
suffix=".tmp",
text=True,
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as file:
file.write(content)
os.chmod(temporary_path, mode)
os.replace(temporary_path, path)
except BaseException as write_error:
try:
os.remove(temporary_path)
except FileNotFoundError:
pass
except OSError as cleanup_error:
raise RuntimeError(
f"Write {path} failed and temporary file cleanup also failed: {cleanup_error}"
) from write_error
raise
def _write_json_atomic(path: str, value: JsonObject, mode: int) -> None:
_write_text_atomic(path, json.dumps(value, indent=2) + "\n", mode)
def _remove_file(path: str, label: str) -> None:
try:
os.remove(path)
except FileNotFoundError:
return
except OSError as error:
raise OSError(f"Remove {label} at {path}: {error}") from error
def _build_command(script_path: str) -> str:
arguments = [sys.executable, script_path]
return subprocess.list2cmdline(arguments) if os.name == "nt" else shlex.join(arguments)
@@ -385,16 +332,6 @@ def _find_v3_command(
return None
def _regular_file_exists(path: str, label: str) -> bool:
try:
metadata = os.stat(path)
except FileNotFoundError:
return False
except OSError as error:
raise OSError(f"Read {label} at {path}: {error}") from error
return stat.S_ISREG(metadata.st_mode)
def _runtime_scripts_exist(contracts: List[HookContract]) -> bool:
try:
with os.scandir(ELYDORA_DIR) as iterator:
@@ -421,10 +358,10 @@ def _runtime_scripts_exist(contracts: List[HookContract]) -> bool:
continue
config_path = os.path.join(entry.path, "config.json")
config = _read_json(config_path, "Elydora runtime config")
config = read_json(config_path, "Elydora runtime config")
if config is None or config.get("agent_name") != AGENT_KEY:
continue
return _regular_file_exists(
return regular_file_exists(
guard_path, "Elydora guard runtime"
) and _regular_file_exists(hook_path, "Elydora audit runtime")
) and regular_file_exists(hook_path, "Elydora audit runtime")
return False
+3 -3
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from ._runtime import same_agent_id
from .base import AgentPlugin, InstallConfig, PluginStatus
from .kiroide_contract import (
AGENT_KEY,
@@ -21,7 +22,6 @@ from .kiroide_installation import (
prepare_kiroide_installation,
prepare_kiroide_uninstall,
)
from .kiroide_command import same_kiroide_agent_id
from .kiroide_io import (
kiroide_runtime_files_exist,
legacy_kiroide_contract_matches_agent,
@@ -78,13 +78,13 @@ class KiroIdePlugin(AgentPlugin):
agent_ids: list[str] = []
for discovered_agent_id in discovered_agent_ids:
if not any(
same_kiroide_agent_id(discovered_agent_id, existing_agent_id)
same_agent_id(discovered_agent_id, existing_agent_id)
for existing_agent_id in agent_ids
):
agent_ids.append(discovered_agent_id)
for managed_agent_id in agent_ids:
owns_workspace_runtime = any(
same_kiroide_agent_id(contract.agent_id, managed_agent_id)
same_agent_id(contract.agent_id, managed_agent_id)
for contract in contracts
)
require_kiroide_workspace_owner(
+23 -189
View File
@@ -2,15 +2,23 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import ntpath
import os
import re
import sys
from typing import Optional, Tuple
from elydora._runtime_paths import runtime_root
from ._runtime import managed_script_reference, same_path
from ._shell_command import (
encoded_windows_command,
is_python_executable,
parse_encoded_windows_command,
parse_posix_command,
posix_source,
windows_powershell_path,
)
_PREAMBLE = "$ErrorActionPreference = 'Stop'; $global:LASTEXITCODE = 1; "
@dataclass(frozen=True)
@@ -19,71 +27,10 @@ class KiroIdeRuntimeReference:
script_path: str
@dataclass(frozen=True)
class _ParsedArgument:
value: str
next_index: int
def same_kiroide_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_kiroide_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _windows_powershell_path() -> str:
configured = os.environ.get("SystemRoot") if os.name == "nt" else None
system_root = (
configured
if configured
and ntpath.isabs(configured)
and re.search(r'["%\r\n]', configured) is None
else r"C:\Windows"
)
return ntpath.join(
system_root,
"System32",
"WindowsPowerShell",
"v1.0",
"powershell.exe",
)
def _same_windows_path(left: str, right: str) -> bool:
return ntpath.normcase(ntpath.abspath(left)) == ntpath.normcase(
ntpath.abspath(right)
)
def _windows_command(script_path: str) -> str:
source = (
"$ErrorActionPreference = 'Stop'; $global:LASTEXITCODE = 1; "
f"& {_quote_powershell(sys.executable)} "
f"{_quote_powershell(script_path)}; exit $LASTEXITCODE"
)
encoded = base64.b64encode(source.encode("utf-16le")).decode("ascii")
return (
f'"{_windows_powershell_path()}" -NoLogo -NoProfile '
f"-NonInteractive -EncodedCommand {encoded}"
)
def build_kiroide_command(script_path: str) -> str:
if (
not os.path.isabs(sys.executable)
or not _is_python_executable(sys.executable)
or not is_python_executable(sys.executable)
or not os.path.isabs(script_path)
):
raise ValueError(
@@ -91,136 +38,25 @@ def build_kiroide_command(script_path: str) -> str:
"and script path"
)
if os.name == "nt":
return _windows_command(script_path)
return f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
def _read_posix_argument(command: str, start: int) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return _ParsedArgument(value, index + 1)
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if (
executable is None
or command[executable.next_index : executable.next_index + 1] != " "
):
return None
script = _read_posix_argument(command, executable.next_index + 1)
if script is None or script.next_index != len(command):
return None
return executable.value, script.value
def _read_powershell_argument(
command: str, start: int
) -> Optional[_ParsedArgument]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return _ParsedArgument(value, index + 1)
return None
def _parse_powershell_source(source: str) -> Optional[Tuple[str, str]]:
prefix = "$ErrorActionPreference = 'Stop'; $global:LASTEXITCODE = 1; & "
if not source.startswith(prefix):
return None
executable = _read_powershell_argument(source, len(prefix))
if (
executable is None
or source[executable.next_index : executable.next_index + 1] != " "
):
return None
script = _read_powershell_argument(source, executable.next_index + 1)
if script is None or source[script.next_index :] != "; exit $LASTEXITCODE":
return None
return executable.value, script.value
def _parse_windows_command(command: str) -> Optional[Tuple[str, str]]:
match = re.fullmatch(
r'"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive '
r"-EncodedCommand ([A-Za-z0-9+/]+={0,2})",
command,
)
if (
match is None
or not ntpath.isabs(match.group(1))
or not _same_windows_path(match.group(1), _windows_powershell_path())
):
return None
try:
raw = base64.b64decode(match.group(2), validate=True)
if base64.b64encode(raw).decode("ascii") != match.group(2):
return None
source = raw.decode("utf-16le")
except (UnicodeDecodeError, ValueError):
return None
return _parse_powershell_source(source)
def _is_python_executable(file_path: str) -> bool:
return (
re.fullmatch(
r"(?:python(?:[0-9]+(?:\.[0-9]+)*)?[td]?|"
r"pypy(?:[0-9]+(?:\.[0-9]+)*)?)(?:\.exe)?",
os.path.basename(file_path),
re.IGNORECASE,
)
is not None
)
return encoded_windows_command(script_path, _PREAMBLE)
return posix_source(script_path)
def _runtime_reference(
executable: str, script_path: str, script_name: str
) -> Optional[KiroIdeRuntimeReference]:
if (
not os.path.isabs(executable)
or not _is_python_executable(executable)
or not os.path.isabs(script_path)
or os.path.basename(script_path) != script_name
):
if not os.path.isabs(executable) or not is_python_executable(executable):
return None
agent_directory = os.path.dirname(script_path)
if not same_kiroide_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return KiroIdeRuntimeReference(agent_id, script_path)
reference = managed_script_reference(script_path, script_name)
return None if reference is None else KiroIdeRuntimeReference(*reference)
def _parse_kiroide_command(command: str) -> Optional[Tuple[str, str]]:
return (
_parse_windows_command(command)
if os.name == "nt"
else _parse_posix_command(command)
)
if os.name == "nt":
return parse_encoded_windows_command(
command, _PREAMBLE + "& ", windows_powershell_path()
)
return parse_posix_command(command)
def owned_kiroide_runtime_reference(
@@ -236,9 +72,7 @@ def kiroide_runtime_reference(
command: str, script_name: str
) -> Optional[KiroIdeRuntimeReference]:
parsed = _parse_kiroide_command(command)
if parsed is None:
return None
if not same_kiroide_path(parsed[0], sys.executable):
if parsed is None or not same_path(parsed[0], sys.executable):
return None
return _runtime_reference(parsed[0], parsed[1], script_name)
@@ -8,6 +8,7 @@ import os
from typing import Any, Callable, Dict, List, Optional, Tuple
from ._managed_files import FileSnapshot
from ._runtime import same_agent_id, same_path
from ._strict_json import JsonObject, parse_json_object
from .kiroide_command import (
KiroIdeRuntimeReference,
@@ -16,8 +17,6 @@ from .kiroide_command import (
legacy_audit_reference,
legacy_guard_reference,
owned_kiroide_runtime_reference,
same_kiroide_agent_id,
same_kiroide_path,
)
@@ -240,7 +239,7 @@ def _managed_pair(
if (
guard is None
or audit is None
or not same_kiroide_agent_id(guard.agent_id, audit.agent_id)
or not same_agent_id(guard.agent_id, audit.agent_id)
):
return None
return (guards[0][0], guard), (audits[0][0], audit)
@@ -289,7 +288,7 @@ def without_managed_kiroide_hooks(
for index, hook in enumerate(hooks):
reference = owned.get(index)
remove = reference is not None and (
not agent_id or same_kiroide_agent_id(reference.agent_id, agent_id)
not agent_id or same_agent_id(reference.agent_id, agent_id)
)
if not remove:
result.append(hook)
@@ -382,8 +381,8 @@ def legacy_kiroide_runtime_contract(
if (
guard is None
or audit is None
or not same_kiroide_agent_id(guard.agent_id, audit.agent_id)
or not same_kiroide_path(
or not same_agent_id(guard.agent_id, audit.agent_id)
or not same_path(
os.path.dirname(guard.script_path), os.path.dirname(audit.script_path)
)
):
@@ -3,12 +3,9 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional, Sequence, Tuple
from elydora._runtime_paths import resolve_agent_directory, runtime_root
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
@@ -16,6 +13,14 @@ from ._managed_files import (
read_physical_file,
)
from ._opaque_removal import OpaqueRemovalChange
from ._runtime import (
DEFAULT_BASE_URL,
present,
resolve_runtime_paths,
runtime_config_source,
same_agent_id,
validate_install_config,
)
from ._transaction import (
DirectoryPrecondition,
FileChange,
@@ -34,11 +39,8 @@ from ._runtime_removal import (
from .base import InstallConfig
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .kiroide_command import same_kiroide_agent_id, same_kiroide_path
from .kiroide_contract import (
AGENT_KEY,
AUDIT_SCRIPT,
GUARD_SCRIPT,
RenderedKiroIdeDocument,
kiroide_runtime_contracts,
)
@@ -54,11 +56,13 @@ from .kiroide_io import (
LegacyKiroIdeDocument,
legacy_kiroide_contract_matches_agent,
require_physical_legacy_directory,
validate_api_origin,
validate_kiroide_runtime_config,
validate_private_key,
validate_runtime_tree,
)
PRODUCT = "Kiro IDE"
@dataclass(frozen=True)
class KiroIdeRuntimePaths:
agent_id: str
@@ -74,6 +78,8 @@ class KiroIdeRuntimePaths:
class PreparedKiroIdeInstallation:
changes: List[FileChange]
runtime_preconditions: List[FilePrecondition]
@dataclass(frozen=True)
class PreparedKiroIdeUninstall:
changes: List[FileChange]
@@ -92,79 +98,30 @@ def _require_runtime_state(
)
def _runtime_config(
config: InstallConfig, agent_id: str, workspace_root: str
) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
"workspace_root": workspace_root,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> KiroIdeRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
root = runtime_root()
paths = resolve_runtime_paths(config)
root = os.path.dirname(paths.agent_directory)
runtime_parent = os.path.dirname(root)
agent_directory = resolve_agent_directory(root, agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_kiroide_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
directories = inspect_runtime_directories(
runtime_parent, root, agent_directory
)
return KiroIdeRuntimePaths(
agent_id,
paths.agent_id,
runtime_parent,
root,
agent_directory,
guard_path,
audit_path,
directories,
paths.agent_directory,
paths.guard_path,
paths.audit_path,
inspect_runtime_directories(runtime_parent, root, paths.agent_directory),
)
def _validate_install_config(config: InstallConfig) -> KiroIdeRuntimePaths:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(f"Kiro IDE installation requires agent_name {AGENT_KEY}")
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
return _agent_paths(config)
def preflight_kiroide_installation(
config: InstallConfig, sources: KiroIdeSources
) -> KiroIdeRuntimePaths:
if not sources.document.file_path:
raise ValueError("Kiro IDE installation requires a workspace hook path")
paths = _validate_install_config(config)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = _agent_paths(config)
owns_workspace_runtime = any(
same_kiroide_agent_id(contract.agent_id, paths.agent_id)
same_agent_id(contract.agent_id, paths.agent_id)
for contract in kiroide_runtime_contracts(sources.document.hooks)
)
owns_global_legacy_runtime = legacy_kiroide_contract_matches_agent(
@@ -229,7 +186,7 @@ def _removes_legacy(
) -> bool:
return legacy.contract is not None and (
not agent_id
or same_kiroide_agent_id(legacy.contract.agent_id, agent_id)
or same_agent_id(legacy.contract.agent_id, agent_id)
)
@@ -252,10 +209,6 @@ def _legacy_change(
)
def _present(changes: Sequence[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_kiroide_installation(
config: InstallConfig,
paths: KiroIdeRuntimePaths,
@@ -269,7 +222,7 @@ def prepare_kiroide_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
@@ -280,12 +233,11 @@ def prepare_kiroide_installation(
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(
_runtime_config(
config,
paths.agent_id,
sources.paths.workspace_root,
)
runtime_config_source(
config,
paths.agent_id,
AGENT_KEY,
workspace_root=sources.paths.workspace_root,
),
0o600,
MAX_CONFIG_BYTES,
@@ -303,7 +255,7 @@ def prepare_kiroide_installation(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
]
changes = _present(
changes = present(
[
*(change for change, _condition in runtime),
_rendered_change(rendered),
@@ -420,7 +372,7 @@ def prepare_kiroide_uninstall(
candidates = runtime_agent_ids or ((agent_id,) if agent_id else ())
for runtime_agent_id in candidates:
if any(
same_kiroide_agent_id(runtime_agent_id, selected_agent_id)
same_agent_id(runtime_agent_id, selected_agent_id)
for selected_agent_id in selected_agent_ids
):
continue
@@ -436,7 +388,7 @@ def prepare_kiroide_uninstall(
config_path,
sources.paths.workspace_root,
allow_missing_workspace_root=any(
same_kiroide_agent_id(
same_agent_id(
contract.agent_id, runtime_agent_id
)
for contract in kiroide_runtime_contracts(
@@ -454,7 +406,7 @@ def prepare_kiroide_uninstall(
)
if runtime_removal is not None:
runtime_removals.append(runtime_removal)
provider_changes = _present(
provider_changes = present(
[_rendered_change(rendered), _legacy_change(sources.legacy, agent_id)]
)
return PreparedKiroIdeUninstall(
+36 -132
View File
@@ -2,11 +2,9 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory, runtime_root
@@ -20,10 +18,17 @@ from ._managed_files import (
read_physical_directory,
read_physical_file,
)
from ._runtime import (
RUNTIME_ARTIFACTS,
RUNTIME_CONFIG_FIELDS,
expected_runtime_scripts,
require_non_empty_string,
same_agent_id,
same_path,
validate_api_origin,
validate_private_key,
)
from ._strict_json import JsonObject, parse_json_object
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .kiroide_command import same_kiroide_agent_id, same_kiroide_path
from .kiroide_contract import (
AGENT_KEY,
CONFIG_FILE,
@@ -187,67 +192,7 @@ def require_physical_legacy_directory(legacy: LegacyKiroIdeDocument) -> None:
)
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(value: Any, field: str, config_path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"Elydora runtime config {field} is invalid: {config_path}")
return value
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def _validate_runtime_config(
def validate_kiroide_runtime_config(
config: JsonObject,
expected_agent_id: str,
config_path: str,
@@ -255,27 +200,19 @@ def _validate_runtime_config(
allow_missing_workspace_root: bool = False,
allow_legacy_ownerless_config: bool = False,
) -> None:
supported = {
"org_id",
"agent_id",
"kid",
"base_url",
"token",
"agent_name",
"workspace_root",
}
supported = RUNTIME_CONFIG_FIELDS | {"workspace_root"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
require_non_empty_string(config.get("org_id"), "org_id", config_path)
require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_kiroide_agent_id(agent_id, expected_agent_id)
not same_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
@@ -289,8 +226,8 @@ def _validate_runtime_config(
if "token" in config and not (
legacy_ownerless_config and config.get("token") == ""
):
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
require_non_empty_string(config.get("token"), "token", config_path)
base_url = require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
@@ -298,39 +235,20 @@ def _validate_runtime_config(
allow_missing_workspace_root or legacy_ownerless_config
):
return
workspace_root = _require_non_empty_string(
workspace_root = require_non_empty_string(
configured_workspace, "workspace_root", config_path
)
if not os.path.isabs(workspace_root):
raise ValueError(
f"Elydora runtime config workspace_root must be absolute: {config_path}"
)
if not same_kiroide_path(workspace_root, expected_workspace_root):
if not same_path(workspace_root, expected_workspace_root):
raise ValueError(
"Elydora Kiro IDE agent is bound to another workspace: "
f"{workspace_root}"
)
def validate_kiroide_runtime_config(
config: JsonObject,
expected_agent_id: str,
config_path: str,
expected_workspace_root: str,
allow_missing_workspace_root: bool = False,
allow_legacy_ownerless_config: bool = False,
) -> None:
"""Validate a runtime config already read through a pinned directory."""
_validate_runtime_config(
config,
expected_agent_id,
config_path,
expected_workspace_root,
allow_missing_workspace_root,
allow_legacy_ownerless_config,
)
def validate_runtime_tree(
agent_directory: str,
agent_id: str,
@@ -346,27 +264,21 @@ def validate_runtime_tree(
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifact_exists = any(
config_snapshot = read_physical_file(config_path, "Elydora runtime config", MAX_CONFIG_BYTES)
artifact_states = [
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
)
if config is None:
for name, label in RUNTIME_ARTIFACTS
]
artifact_exists = any(artifact_states)
if config_snapshot is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
_validate_runtime_config(
config,
validate_kiroide_runtime_config(
parse_json_object(config_snapshot.contents, f"Elydora runtime config at {config_path}"),
agent_id,
config_path,
workspace_root,
@@ -381,8 +293,8 @@ def _runtime_contract_exists(
root = runtime_root()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_kiroide_path(os.path.dirname(agent_directory), root)
or not same_kiroide_path(
not same_path(os.path.dirname(agent_directory), root)
or not same_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
@@ -414,7 +326,7 @@ def _runtime_contract_exists(
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(
validate_kiroide_runtime_config(
config,
contract.agent_id,
config_path,
@@ -436,15 +348,7 @@ def _runtime_contract_exists(
f"its owner: {config_path}"
)
validate_private_key(key.contents, "Elydora private key")
expected_guard = generate_guard_script(AGENT_KEY, contract.agent_id)
expected_audit = generate_hook_script(
org_id=str(config["org_id"]),
agent_id=contract.agent_id,
kid=str(config["kid"]),
base_url=str(config["base_url"]),
native_payload=True,
agent_name=AGENT_KEY,
)
expected_guard, expected_audit = expected_runtime_scripts(AGENT_KEY, contract.agent_id, config)
return guard.contents == expected_guard and audit.contents == expected_audit
@@ -478,15 +382,15 @@ def legacy_kiroide_contract_matches_agent(
legacy: LegacyKiroIdeDocument, agent_id: str
) -> bool:
contract = legacy.contract
if contract is None or not same_kiroide_agent_id(
if contract is None or not same_agent_id(
contract.agent_id, agent_id
):
return False
agent_directory = resolve_agent_directory(runtime_root(), agent_id)
return same_kiroide_path(
return same_path(
contract.guard_path,
os.path.join(agent_directory, "guard.py"),
) and same_kiroide_path(
) and same_path(
contract.audit_path,
os.path.join(agent_directory, "hook.py"),
)
+2 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Dict
from ._runtime import RuntimePaths
from .base import AgentPlugin, InstallConfig, PluginStatus
from .letta_config import render_letta_document
from .letta_contract import (
@@ -13,7 +14,6 @@ from .letta_contract import (
letta_runtime_contracts,
)
from .letta_installation import (
LettaRuntimePaths,
commit_letta_installation,
commit_letta_uninstall,
preflight_letta_installation,
@@ -24,7 +24,7 @@ from .letta_io import letta_runtime_files_exist
from .letta_sources import read_letta_sources
def _installed_groups(paths: LettaRuntimePaths) -> Dict[str, JsonObject]:
def _installed_groups(paths: RuntimePaths) -> Dict[str, JsonObject]:
return {
"PreToolUse": build_letta_group(paths.guard_path),
"PostToolUse": build_letta_group(paths.audit_path),
+17 -115
View File
@@ -4,11 +4,17 @@ from __future__ import annotations
from dataclasses import dataclass
import os
import re
import sys
from typing import Optional, Tuple
from typing import Optional
from elydora._runtime_paths import runtime_root
from ._runtime import managed_script_reference
from ._shell_command import (
is_python_executable,
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
)
@dataclass(frozen=True)
@@ -18,103 +24,12 @@ class LettaRuntimeReference:
executable_path: Optional[str] = None
def same_letta_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_letta_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def build_letta_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Letta Code hook commands require absolute executable and script paths"
)
invocation = (
f"{_quote_powershell(sys.executable)} {_quote_powershell(script_path)}"
if os.name == "nt"
else f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
)
return f"& {invocation}; exit $LASTEXITCODE" if os.name == "nt" else invocation
def _read_posix_argument(command: str, start: int) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return value, index + 1
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_posix_argument(command, executable[1] + 1)
if script is None or script[1] != len(command):
return None
return executable[0], script[0]
def _read_powershell_argument(
command: str, start: int
) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return value, index + 1
return None
def _parse_powershell_command(command: str) -> Optional[Tuple[str, str]]:
if not command.startswith("& "):
return None
executable = _read_powershell_argument(command, 2)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_powershell_argument(command, executable[1] + 1)
if script is None or command[script[1] :] != "; exit $LASTEXITCODE":
return None
return executable[0], script[0]
def _is_python_executable(file_path: str) -> bool:
return bool(re.fullmatch(r"python(?:3(?:\.\d+)?)?(?:\.exe)?", os.path.basename(file_path), re.IGNORECASE))
return powershell_source(script_path) if os.name == "nt" else posix_source(script_path)
def _runtime_reference(
@@ -122,30 +37,21 @@ def _runtime_reference(
script_name: str,
executable_path: Optional[str] = None,
) -> Optional[LettaRuntimeReference]:
if not os.path.isabs(script_path) or os.path.basename(script_path) != script_name:
reference = managed_script_reference(script_path, script_name)
if reference is None:
return None
agent_directory = os.path.dirname(script_path)
if not same_letta_path(os.path.dirname(agent_directory), runtime_root()):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return LettaRuntimeReference(agent_id, script_path, executable_path)
return LettaRuntimeReference(reference[0], reference[1], executable_path)
def letta_runtime_reference(
command: str, script_name: str
) -> Optional[LettaRuntimeReference]:
parsed = (
_parse_powershell_command(command)
parse_powershell_source(command)
if os.name == "nt"
else _parse_posix_command(command)
else parse_posix_command(command)
)
if (
parsed is None
or not os.path.isabs(parsed[0])
or not _is_python_executable(parsed[0])
):
if parsed is None or not os.path.isabs(parsed[0]) or not is_python_executable(parsed[0]):
return None
return _runtime_reference(parsed[1], script_name, parsed[0])
@@ -160,11 +66,7 @@ def letta_legacy_guard_reference(
return None
executable = command[1:closing]
script_path = command[closing + 2 :]
if (
not os.path.isabs(executable)
or not _is_python_executable(executable)
or not script_path
):
if not os.path.isabs(executable) or not is_python_executable(executable) or not script_path:
return None
return _runtime_reference(script_path, script_name, executable)
+4 -11
View File
@@ -8,16 +8,13 @@ import os
import sys
from typing import Any, Dict, List, Optional, Tuple
from elydora._runtime_paths import runtime_root
from ._runtime import same_agent_id, same_path
from .letta_command import (
LettaRuntimeReference,
build_letta_command,
letta_legacy_audit_reference,
letta_legacy_guard_reference,
letta_runtime_reference,
same_letta_agent_id,
same_letta_path,
)
@@ -63,10 +60,6 @@ class ManagedRemoval:
remove_group: bool
def elydora_dir() -> str:
return runtime_root()
def _validate_timeout(value: JsonObject, label: str) -> None:
if "timeout" not in value:
return
@@ -222,7 +215,7 @@ def managed_letta_removals(
is not None
and (
agent_id is None
or same_letta_agent_id(reference.agent_id, agent_id)
or same_agent_id(reference.agent_id, agent_id)
)
)
if indexes:
@@ -247,7 +240,7 @@ def _references_for_event(
if (
reference is None
or reference.executable_path is None
or not same_letta_path(reference.executable_path, sys.executable)
or not same_path(reference.executable_path, sys.executable)
):
continue
key = os.path.normcase(reference.agent_id)
@@ -267,7 +260,7 @@ def letta_runtime_contracts(hooks: LettaHooks) -> List[LettaRuntimeContract]:
failure = failures.get(key, [])
if len(guard) != 1 or len(post) != 1 or len(failure) != 1:
continue
if not same_letta_path(post[0].script_path, failure[0].script_path):
if not same_path(post[0].script_path, failure[0].script_path):
continue
contracts.append(LettaRuntimeContract(
guard[0].agent_id,
+30 -132
View File
@@ -2,113 +2,41 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional, Sequence
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import (
FileChange,
FilePrecondition,
file_change,
source_change,
write_changes,
from ._managed_files import MAX_SOURCE_BYTES
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
same_path,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, FilePrecondition, source_change, write_changes
from .base import InstallConfig
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .letta_command import same_letta_path
from .letta_config import RenderedLettaDocument, letta_document_label
from .letta_contract import AGENT_KEY, AUDIT_SCRIPT, GUARD_SCRIPT, elydora_dir
from .letta_io import validate_api_origin, validate_private_key, validate_runtime_tree
from .letta_contract import AGENT_KEY
from .letta_sources import LettaSources, require_letta_hooks_enabled
@dataclass(frozen=True)
class LettaRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> LettaRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(elydora_dir(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_letta_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return LettaRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"Letta Code installation requires agent_name {AGENT_KEY}"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Letta Code"
def preflight_letta_installation(
config: InstallConfig, sources: LettaSources
) -> LettaRuntimePaths:
) -> RuntimePaths:
require_letta_hooks_enabled(sources)
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _rendered_change(
rendered: RenderedLettaDocument,
) -> Optional[FileChange]:
def _rendered_change(rendered: RenderedLettaDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
@@ -122,13 +50,9 @@ def _rendered_change(
)
def _present(changes: Sequence[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_letta_installation(
config: InstallConfig,
paths: LettaRuntimePaths,
paths: RuntimePaths,
rendered: RenderedLettaDocument,
) -> List[FileChange]:
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
@@ -136,50 +60,28 @@ def prepare_letta_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
fail_closed=True,
native_payload=True,
agent_name=AGENT_KEY,
)
return _present([
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
_rendered_change(rendered),
])
return [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([_rendered_change(rendered)]),
]
def _read_only_preconditions(
sources: LettaSources, changed_path: Optional[str]
sources: LettaSources, changed_path: str
) -> List[FilePrecondition]:
return [
condition
for condition in sources.preconditions
if changed_path is None
or not same_letta_path(condition.file_path, changed_path)
if not same_path(condition.file_path, changed_path)
]
def commit_letta_installation(
changes: List[FileChange], sources: LettaSources
) -> None:
def commit_letta_installation(changes: List[FileChange], sources: LettaSources) -> None:
write_changes(
changes,
"Install Letta Code hooks",
@@ -187,15 +89,11 @@ def commit_letta_installation(
)
def prepare_letta_uninstall(
rendered: RenderedLettaDocument,
) -> Optional[FileChange]:
def prepare_letta_uninstall(rendered: RenderedLettaDocument) -> Optional[FileChange]:
return _rendered_change(rendered)
def commit_letta_uninstall(
change: Optional[FileChange], sources: LettaSources
) -> None:
def commit_letta_uninstall(change: Optional[FileChange], sources: LettaSources) -> None:
if change is None:
return
write_changes(
+12 -193
View File
@@ -2,200 +2,19 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Tuple
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .letta_command import same_letta_agent_id, same_letta_path
from .letta_contract import AGENT_KEY, LettaRuntimeContract, elydora_dir
from ._runtime import expected_runtime_scripts, runtime_contract_exists
from ._strict_json import JsonObject
from .letta_contract import AGENT_KEY, LettaRuntimeContract
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
def _expected_scripts(agent_id: str, config: JsonObject) -> Tuple[str, str]:
return expected_runtime_scripts(AGENT_KEY, agent_id, config, fail_closed=True)
def letta_runtime_files_exist(contracts: List[LettaRuntimeContract]) -> bool:
return any(
runtime_contract_exists(contract, AGENT_KEY, "Letta Code", _expected_scripts)
for contract in contracts
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(
value: Any, field: str, config_path: str
) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_letta_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime identity does not match Letta Code hooks: "
f"{config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_letta_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Letta Code agent "
f"{agent_id}: {config_path}"
)
def _runtime_contract_exists(contract: LettaRuntimeContract) -> bool:
root = elydora_dir()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_letta_path(os.path.dirname(agent_directory), root)
or not same_letta_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
expected_guard = generate_guard_script(AGENT_KEY, contract.agent_id)
expected_audit = generate_hook_script(
org_id=str(config["org_id"]),
agent_id=contract.agent_id,
kid=str(config["kid"]),
base_url=str(config["base_url"]),
fail_closed=True,
native_payload=True,
agent_name=AGENT_KEY,
)
return guard.contents == expected_guard and audit.contents == expected_audit
def letta_runtime_files_exist(
contracts: List[LettaRuntimeContract],
) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
+4 -13
View File
@@ -4,15 +4,15 @@ from __future__ import annotations
from dataclasses import dataclass
import os
from typing import Dict, Optional, Tuple
from typing import Optional, Tuple
from ._managed_files import (
MAX_SOURCE_BYTES,
physical_directory_exists,
read_physical_file,
)
from ._runtime import same_path, unique_preconditions
from ._transaction import FilePrecondition
from .letta_command import same_letta_path
from .letta_config import (
LettaDocument,
create_letta_document,
@@ -76,15 +76,6 @@ def _source_precondition(document: LettaDocument) -> FilePrecondition:
)
def _deduplicate_preconditions(
values: list[FilePrecondition],
) -> Tuple[FilePrecondition, ...]:
result: Dict[str, FilePrecondition] = {}
for value in values:
result.setdefault(_comparison_path(value.file_path), value)
return tuple(result.values())
def _effective_disable(
global_settings: LettaDocument,
project: LettaDocument,
@@ -111,7 +102,7 @@ def read_letta_sources() -> LettaSources:
physical_directory_exists(
global_directory, "Letta Code global configuration directory"
)
if not same_letta_path(project_directory, global_directory):
if not same_path(project_directory, global_directory):
physical_directory_exists(
project_directory, "Letta Code project configuration directory"
)
@@ -141,7 +132,7 @@ def read_letta_sources() -> LettaSources:
_effective_disable(
global_settings, project, project_local, project_active
),
_deduplicate_preconditions(preconditions),
unique_preconditions(preconditions),
)
+2 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Dict
from ._runtime import RuntimePaths
from .base import AgentPlugin, InstallConfig, PluginStatus
from .qwen_config import render_qwen_document
from .qwen_contract import (
@@ -15,7 +16,6 @@ from .qwen_contract import (
qwen_runtime_contracts,
)
from .qwen_installation import (
QwenRuntimePaths,
commit_qwen_installation,
commit_qwen_uninstall,
preflight_qwen_installation,
@@ -26,7 +26,7 @@ from .qwen_io import qwen_runtime_files_exist
from .qwen_sources import read_qwen_sources
def _installed_groups(paths: QwenRuntimePaths) -> Dict[str, JsonObject]:
def _installed_groups(paths: RuntimePaths) -> Dict[str, JsonObject]:
return {
"PreToolUse": build_qwen_group(paths.guard_path, GUARD_HOOK_NAME),
"PostToolUse": build_qwen_group(paths.audit_path, AUDIT_HOOK_NAME),
+15 -105
View File
@@ -5,7 +5,15 @@ from __future__ import annotations
from dataclasses import dataclass
import os
import sys
from typing import Optional, Tuple
from typing import Optional
from ._runtime import managed_script_reference, same_path
from ._shell_command import (
parse_posix_command,
parse_powershell_source,
posix_source,
powershell_source,
)
@dataclass(frozen=True)
@@ -14,121 +22,23 @@ class QwenRuntimeReference:
script_path: str
def same_qwen_path(left: str, right: str) -> bool:
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(
os.path.abspath(right)
)
def same_qwen_agent_id(left: str, right: str) -> bool:
return os.path.normcase(left) == os.path.normcase(right)
def _quote_posix(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _quote_powershell(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def build_qwen_command(script_path: str) -> str:
if not os.path.isabs(sys.executable) or not os.path.isabs(script_path):
raise ValueError(
"Qwen Code hook commands require absolute executable and script paths"
)
invocation = (
f"{_quote_powershell(sys.executable)} {_quote_powershell(script_path)}"
if os.name == "nt"
else f"{_quote_posix(sys.executable)} {_quote_posix(script_path)}"
)
return f"& {invocation}; exit $LASTEXITCODE" if os.name == "nt" else invocation
def _read_posix_argument(command: str, start: int) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
apostrophe = "'\"'\"'"
value = ""
index = start + 1
while index < len(command):
if command.startswith(apostrophe, index):
value += "'"
index += len(apostrophe)
continue
if command[index] == "'":
return value, index + 1
value += command[index]
index += 1
return None
def _parse_posix_command(command: str) -> Optional[Tuple[str, str]]:
executable = _read_posix_argument(command, 0)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_posix_argument(command, executable[1] + 1)
if script is None or script[1] != len(command):
return None
return executable[0], script[0]
def _read_powershell_argument(
command: str, start: int
) -> Optional[Tuple[str, int]]:
if start >= len(command) or command[start] != "'":
return None
value = ""
index = start + 1
while index < len(command):
if command[index] != "'":
value += command[index]
index += 1
continue
if index + 1 < len(command) and command[index + 1] == "'":
value += "'"
index += 2
continue
return value, index + 1
return None
def _parse_powershell_command(command: str) -> Optional[Tuple[str, str]]:
if not command.startswith("& "):
return None
executable = _read_powershell_argument(command, 2)
if executable is None or executable[1] >= len(command):
return None
if command[executable[1]] != " ":
return None
script = _read_powershell_argument(command, executable[1] + 1)
if script is None or command[script[1] :] != "; exit $LASTEXITCODE":
return None
return executable[0], script[0]
return powershell_source(script_path) if os.name == "nt" else posix_source(script_path)
def qwen_runtime_reference(
command: str, script_name: str
) -> Optional[QwenRuntimeReference]:
parsed = (
_parse_powershell_command(command)
parse_powershell_source(command)
if os.name == "nt"
else _parse_posix_command(command)
else parse_posix_command(command)
)
if (
parsed is None
or not same_qwen_path(parsed[0], sys.executable)
or not os.path.isabs(parsed[1])
or os.path.basename(parsed[1]) != script_name
):
if parsed is None or not same_path(parsed[0], sys.executable):
return None
agent_directory = os.path.dirname(parsed[1])
runtime_root = os.path.join(os.path.expanduser("~"), ".elydora")
if not same_qwen_path(os.path.dirname(agent_directory), runtime_root):
return None
agent_id = os.path.basename(agent_directory)
if agent_id in {"", ".", ".."}:
return None
return QwenRuntimeReference(agent_id, parsed[1])
reference = managed_script_reference(parsed[1], script_name)
return None if reference is None else QwenRuntimeReference(*reference)
+3 -8
View File
@@ -10,12 +10,11 @@ import shutil
import subprocess # nosec B404
from typing import Any, Dict, List, Optional, Sequence, Tuple
from ._runtime import same_agent_id, same_path
from .qwen_command import (
QwenRuntimeReference,
build_qwen_command,
qwen_runtime_reference,
same_qwen_agent_id,
same_qwen_path,
)
@@ -102,10 +101,6 @@ class ManagedRemoval:
remove_group: bool
def elydora_dir() -> str:
return os.path.join(os.path.expanduser("~"), ".elydora")
def _optional_string(value: JsonObject, field: str, label: str) -> None:
if field in value and not isinstance(value[field], str):
raise ValueError(f'{label} field "{field}" must be a string')
@@ -352,7 +347,7 @@ def managed_qwen_removals(
is not None
and (
agent_id is None
or same_qwen_agent_id(reference.agent_id, agent_id)
or same_agent_id(reference.agent_id, agent_id)
)
)
if indexes:
@@ -398,7 +393,7 @@ def qwen_runtime_contracts(hooks: QwenHooks) -> List[QwenRuntimeContract]:
failure = failures.get(key, [])
if len(guard) != 1 or len(post) != 1 or len(failure) != 1:
continue
if not same_qwen_path(post[0].script_path, failure[0].script_path):
if not same_path(post[0].script_path, failure[0].script_path):
continue
contracts.append(QwenRuntimeContract(
guard[0].agent_id,
+31 -127
View File
@@ -2,107 +2,41 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import List, Optional, Sequence
from typing import List, Optional
from elydora._runtime_paths import resolve_agent_directory
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
from ._transaction import FileChange, FilePrecondition, file_change, source_change, write_changes
from ._managed_files import MAX_SOURCE_BYTES
from ._runtime import (
DEFAULT_BASE_URL,
RuntimePaths,
present,
resolve_runtime_paths,
runtime_file_changes,
same_path,
validate_install_config,
validate_runtime_tree,
)
from ._transaction import FileChange, FilePrecondition, source_change, write_changes
from .base import InstallConfig
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .qwen_command import same_qwen_path
from .qwen_config import RenderedQwenDocument, qwen_document_label
from .qwen_contract import AGENT_KEY, AUDIT_SCRIPT, GUARD_SCRIPT, elydora_dir
from .qwen_io import validate_api_origin, validate_private_key, validate_runtime_tree
from .qwen_contract import AGENT_KEY
from .qwen_sources import QwenSources, require_qwen_hooks_enabled
@dataclass(frozen=True)
class QwenRuntimePaths:
agent_id: str
agent_directory: str
guard_path: str
audit_path: str
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
value = {
"org_id": config.get("org_id", ""),
"agent_id": agent_id,
"kid": config.get("kid", ""),
"base_url": config.get("base_url", "https://api.elydora.com"),
"agent_name": AGENT_KEY,
}
token = config.get("token")
if token:
value["token"] = token
return value
def _json_source(value: dict) -> str:
return json.dumps(value, indent=2) + "\n"
def _agent_paths(config: InstallConfig) -> QwenRuntimePaths:
agent_id = config.get("agent_id", "")
if not agent_id:
raise ValueError("agent_id is required")
agent_directory = resolve_agent_directory(elydora_dir(), agent_id)
guard_path = os.path.join(agent_directory, GUARD_SCRIPT)
audit_path = os.path.join(agent_directory, AUDIT_SCRIPT)
if not same_qwen_path(config.get("guard_script_path", ""), guard_path):
raise ValueError(
"Elydora guard runtime must use the managed agent directory: "
f"{guard_path}"
)
return QwenRuntimePaths(agent_id, agent_directory, guard_path, audit_path)
def _validate_install_config(config: InstallConfig) -> None:
for field in ("org_id", "agent_id", "kid", "private_key", "base_url"):
value = config.get(field)
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
if config.get("agent_name") != AGENT_KEY:
raise ValueError(
f"Qwen Code installation requires agent_name {AGENT_KEY}"
)
validate_private_key(config["private_key"])
validate_api_origin(config["base_url"])
if "token" in config:
token = config["token"]
if not isinstance(token, str) or not token:
raise ValueError("token must be a non-empty string when provided")
_agent_paths(config)
PRODUCT = "Qwen Code"
def preflight_qwen_installation(
config: InstallConfig, sources: QwenSources
) -> QwenRuntimePaths:
) -> RuntimePaths:
require_qwen_hooks_enabled(sources)
_validate_install_config(config)
paths = _agent_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id)
validate_install_config(config, AGENT_KEY, PRODUCT)
paths = resolve_runtime_paths(config)
validate_runtime_tree(paths.agent_directory, paths.agent_id, AGENT_KEY, PRODUCT)
return paths
def _runtime_change(
file_path: str,
label: str,
next_source: str,
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
) -> Optional[FileChange]:
return file_change(file_path, label, next_source, mode, maximum_bytes)
def _rendered_change(
rendered: RenderedQwenDocument,
) -> Optional[FileChange]:
def _rendered_change(rendered: RenderedQwenDocument) -> Optional[FileChange]:
if not rendered.changed:
return None
return source_change(
@@ -116,13 +50,9 @@ def _rendered_change(
)
def _present(changes: Sequence[Optional[FileChange]]) -> List[FileChange]:
return [change for change in changes if change is not None]
def prepare_qwen_installation(
config: InstallConfig,
paths: QwenRuntimePaths,
paths: RuntimePaths,
rendered: RenderedQwenDocument,
) -> List[FileChange]:
guard_script = generate_guard_script(AGENT_KEY, paths.agent_id)
@@ -130,49 +60,27 @@ def prepare_qwen_installation(
org_id=config.get("org_id", ""),
agent_id=paths.agent_id,
kid=config.get("kid", ""),
base_url=config.get("base_url", "https://api.elydora.com"),
base_url=config.get("base_url", DEFAULT_BASE_URL),
native_payload=True,
agent_name=AGENT_KEY,
)
return _present([
_runtime_change(
paths.guard_path, "Elydora guard runtime", guard_script, 0o700
),
_runtime_change(
os.path.join(paths.agent_directory, "config.json"),
"Elydora runtime config",
_json_source(_runtime_config(config, paths.agent_id)),
0o600,
MAX_CONFIG_BYTES,
),
_runtime_change(
os.path.join(paths.agent_directory, "private.key"),
"Elydora private key",
config.get("private_key", ""),
0o600,
MAX_SECRET_BYTES,
),
_runtime_change(
paths.audit_path, "Elydora audit runtime", audit_script, 0o700
),
_rendered_change(rendered),
])
return [
*runtime_file_changes(config, paths, AGENT_KEY, guard_script, audit_script),
*present([_rendered_change(rendered)]),
]
def _read_only_preconditions(
sources: QwenSources, changed_path: Optional[str]
sources: QwenSources, changed_path: str
) -> List[FilePrecondition]:
return [
condition
for condition in sources.preconditions
if changed_path is None
or not same_qwen_path(condition.file_path, changed_path)
if not same_path(condition.file_path, changed_path)
]
def commit_qwen_installation(
changes: List[FileChange], sources: QwenSources
) -> None:
def commit_qwen_installation(changes: List[FileChange], sources: QwenSources) -> None:
write_changes(
changes,
"Install Qwen Code hooks",
@@ -180,15 +88,11 @@ def commit_qwen_installation(
)
def prepare_qwen_uninstall(
rendered: RenderedQwenDocument,
) -> Optional[FileChange]:
def prepare_qwen_uninstall(rendered: RenderedQwenDocument) -> Optional[FileChange]:
return _rendered_change(rendered)
def commit_qwen_uninstall(
change: Optional[FileChange], sources: QwenSources
) -> None:
def commit_qwen_uninstall(change: Optional[FileChange], sources: QwenSources) -> None:
if change is None:
return
write_changes(
+12 -192
View File
@@ -2,199 +2,19 @@
from __future__ import annotations
import base64
import os
from typing import Any, List, Optional
import urllib.parse
from typing import List, Tuple
from ._managed_files import (
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
physical_directory_exists,
physical_file_exists,
read_physical_file,
)
from ._strict_json import JsonObject, parse_json_object
from .guard_template import generate_guard_script
from .hook_template import generate_hook_script
from .qwen_command import same_qwen_agent_id, same_qwen_path
from .qwen_contract import AGENT_KEY, QwenRuntimeContract, elydora_dir
from ._runtime import expected_runtime_scripts, runtime_contract_exists
from ._strict_json import JsonObject
from .qwen_contract import AGENT_KEY, QwenRuntimeContract
def validate_api_origin(value: str, label: str = "base_url") -> None:
try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
parsed.port
except ValueError as error:
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL") from error
invalid_character = "\\" in value or any(
character.isspace() or ord(character) < 32 for character in value
def _expected_scripts(agent_id: str, config: JsonObject) -> Tuple[str, str]:
return expected_runtime_scripts(AGENT_KEY, agent_id, config)
def qwen_runtime_files_exist(contracts: List[QwenRuntimeContract]) -> bool:
return any(
runtime_contract_exists(contract, AGENT_KEY, "Qwen Code", _expected_scripts)
for contract in contracts
)
if (
parsed.scheme not in ("http", "https")
or not parsed.netloc
or hostname is None
or invalid_character
):
raise ValueError(f"{label} must be an absolute HTTP or HTTPS URL")
if (
parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
f"{label} must exclude credentials, query parameters, and fragments"
)
def validate_private_key(value: str, label: str = "private_key") -> None:
try:
padded = value + "=" * ((4 - len(value) % 4) % 4)
seed = base64.b64decode(
padded.replace("-", "+").replace("_", "/"), validate=True
)
canonical = base64.urlsafe_b64encode(seed).rstrip(b"=").decode("ascii")
except (ValueError, UnicodeEncodeError) as error:
raise ValueError(
f"{label} must be a canonical 32-byte base64url value"
) from error
if len(seed) != 32 or canonical != value:
raise ValueError(f"{label} must be a canonical 32-byte base64url value")
def _require_non_empty_string(
value: Any, field: str, config_path: str
) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"Elydora runtime config {field} is invalid: {config_path}"
)
return value
def _validate_runtime_config(
config: JsonObject, expected_agent_id: str, config_path: str
) -> None:
supported = {"org_id", "agent_id", "kid", "base_url", "token", "agent_name"}
extra = next((key for key in config if key not in supported), None)
if extra is not None:
raise ValueError(
f'Elydora runtime config has unsupported field "{extra}": {config_path}'
)
_require_non_empty_string(config.get("org_id"), "org_id", config_path)
_require_non_empty_string(config.get("kid"), "kid", config_path)
agent_id = _require_non_empty_string(
config.get("agent_id"), "agent_id", config_path
)
if (
not same_qwen_agent_id(agent_id, expected_agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime identity does not match Qwen Code hooks: "
f"{config_path}"
)
if "token" in config:
_require_non_empty_string(config.get("token"), "token", config_path)
base_url = _require_non_empty_string(
config.get("base_url"), "base_url", config_path
)
validate_api_origin(base_url, "Elydora runtime config base_url")
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
snapshot = read_physical_file(
file_path, "Elydora runtime config", MAX_CONFIG_BYTES
)
if snapshot is None:
return None
return parse_json_object(
snapshot.contents, f"Elydora runtime config at {file_path}"
)
def validate_runtime_tree(agent_directory: str, agent_id: str) -> None:
root = elydora_dir()
if not physical_directory_exists(root, "Elydora runtime directory"):
return
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return
config_path = os.path.join(agent_directory, "config.json")
config = _read_runtime_config(config_path)
artifacts = (
("private.key", "Elydora private key"),
("guard.py", "Elydora guard runtime"),
("hook.py", "Elydora audit runtime"),
("chain-state.json", "Elydora chain state"),
("status-cache.json", "Elydora status cache"),
("error.log", "Elydora error log"),
)
artifact_exists = any(
physical_file_exists(os.path.join(agent_directory, name), label)
for name, label in artifacts
)
if config is None:
if artifact_exists:
raise ValueError(
"Elydora runtime identity cannot be verified without config.json: "
f"{agent_directory}"
)
return
configured_id = config.get("agent_id")
if (
not isinstance(configured_id, str)
or not same_qwen_agent_id(configured_id, agent_id)
or config.get("agent_name") != AGENT_KEY
):
raise ValueError(
"Elydora runtime config identity does not match Qwen Code agent "
f"{agent_id}: {config_path}"
)
def _runtime_contract_exists(contract: QwenRuntimeContract) -> bool:
root = elydora_dir()
agent_directory = os.path.dirname(contract.guard_path)
if (
not same_qwen_path(os.path.dirname(agent_directory), root)
or not same_qwen_path(
contract.audit_path, os.path.join(agent_directory, "hook.py")
)
):
return False
if not physical_directory_exists(root, "Elydora runtime directory"):
return False
if not physical_directory_exists(
agent_directory, "Elydora agent runtime directory"
):
return False
config_path = os.path.join(agent_directory, "config.json")
key_path = os.path.join(agent_directory, "private.key")
config = _read_runtime_config(config_path)
key = read_physical_file(key_path, "Elydora private key", MAX_SECRET_BYTES)
guard = read_physical_file(contract.guard_path, "Elydora guard runtime")
audit = read_physical_file(contract.audit_path, "Elydora audit runtime")
if config is None or key is None or guard is None or audit is None:
return False
_validate_runtime_config(config, contract.agent_id, config_path)
validate_private_key(key.contents, "Elydora private key")
expected_guard = generate_guard_script(AGENT_KEY, contract.agent_id)
expected_audit = generate_hook_script(
org_id=str(config["org_id"]),
agent_id=contract.agent_id,
kid=str(config["kid"]),
base_url=str(config["base_url"]),
native_payload=True,
agent_name=AGENT_KEY,
)
return guard.contents == expected_guard and audit.contents == expected_audit
def qwen_runtime_files_exist(
contracts: List[QwenRuntimeContract],
) -> bool:
return any(_runtime_contract_exists(contract) for contract in contracts)
+4 -13
View File
@@ -10,9 +10,9 @@ import tempfile
from typing import Dict, List, Optional, Set, Tuple
from ._dotenv import parse_dotenv
from ._managed_files import MAX_SOURCE_BYTES, FileSnapshot, read_physical_file
from ._managed_files import MAX_SOURCE_BYTES, read_physical_file
from ._runtime import same_path, unique_preconditions
from ._transaction import FilePrecondition
from .qwen_command import same_qwen_path
from .qwen_config import (
QwenDocument,
create_qwen_document,
@@ -127,7 +127,7 @@ def _resolve_qwen_routing() -> _RoutingResult:
discovered_home = values.get("QWEN_HOME")
if discovered_home and discovered_home != initial_qwen_home:
discovered_directory = _resolve_config_path(discovered_home)
if not same_qwen_path(discovered_directory, initial_directory):
if not same_path(discovered_directory, initial_directory):
read_candidate(os.path.join(discovered_directory, ".env"))
resolved_home = values.get("QWEN_HOME")
return _RoutingResult(
@@ -288,15 +288,6 @@ def _source_precondition(document: QwenDocument) -> FilePrecondition:
)
def _deduplicate_preconditions(
values: List[FilePrecondition],
) -> Tuple[FilePrecondition, ...]:
result: Dict[str, FilePrecondition] = {}
for value in values:
result.setdefault(_comparison_path(value.file_path), value)
return tuple(result.values())
def read_qwen_sources() -> QwenSources:
routing = _resolve_qwen_routing()
system_path = _system_settings_path()
@@ -355,7 +346,7 @@ def read_qwen_sources() -> QwenSources:
system,
workspace_active and workspace_trusted,
),
preconditions=_deduplicate_preconditions(preconditions),
preconditions=unique_preconditions(preconditions),
)