Fix audit findings: security, types, KID, retry, and version alignment

- Fix critical OpenCode plugin: read private key from file instead of embedding in JS
- Add default KID (agent_id + '-key-1') matching Node and Go SDKs
- Fix Operation.subject and Operation.action types from str to Dict[str, Any]
- Fix retry off-by-one to give max_retries+1 total attempts
- Fix OpenCode operation_type from 'tool_use' to 'ai.tool_use'
- Update plugin path to .config/opencode/plugins matching Node SDK

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-02 15:56:31 -05:00
1 parent 21c63dbb28
commit 347d51febf
4 files changed
+18 -24

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ from .types import (
HealthResponse, HealthResponse,
) )
__version__ = "1.0.0" __version__ = "1.1.0"
__all__ = [ __all__ = [
"ElydoraClient", "ElydoraClient",
+4 -4
View File
@@ -72,7 +72,7 @@ class ElydoraClient:
self.token = token self.token = token
self._prev_chain_hash = GENESIS_CHAIN_HASH self._prev_chain_hash = GENESIS_CHAIN_HASH
self._kid = "" self._kid = agent_id + '-key-1'
self._session = requests.Session() self._session = requests.Session()
def set_kid(self, kid: str) -> None: def set_kid(self, kid: str) -> None:
@@ -106,14 +106,14 @@ class ElydoraClient:
hdrs = headers or self._headers() hdrs = headers or self._headers()
last_exc: Optional[Exception] = None last_exc: Optional[Exception] = None
for attempt in range(self.max_retries): for attempt in range(0, self.max_retries + 1):
try: try:
resp = self._session.request( resp = self._session.request(
method, url, json=json_body, params=params, headers=hdrs, timeout=30 method, url, json=json_body, params=params, headers=hdrs, timeout=30
) )
# Retry on 429 or 5xx # Retry on 429 or 5xx
if resp.status_code == 429 or resp.status_code >= 500: if resp.status_code == 429 or resp.status_code >= 500:
if attempt < self.max_retries - 1: if attempt < self.max_retries:
retry_after = resp.headers.get("Retry-After") retry_after = resp.headers.get("Retry-After")
if retry_after and retry_after.isdigit(): if retry_after and retry_after.isdigit():
delay = int(retry_after) delay = int(retry_after)
@@ -126,7 +126,7 @@ class ElydoraClient:
raise raise
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc: except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc:
last_exc = exc last_exc = exc
if attempt < self.max_retries - 1: if attempt < self.max_retries:
time.sleep(min(2 ** attempt, 8)) time.sleep(min(2 ** attempt, 8))
continue continue
raise raise
+11 -17
View File
@@ -10,8 +10,8 @@ import sys
from .base import AgentPlugin, InstallConfig, PluginStatus from .base import AgentPlugin, InstallConfig, PluginStatus
PLUGIN_DIR = os.path.join(os.path.expanduser("~"), ".opencode", "plugins") PLUGIN_DIR = os.path.join(os.path.expanduser("~"), ".config", "opencode", "plugins")
PLUGIN_FILENAME = "elydora-audit.js" PLUGIN_FILENAME = "elydora-audit.mjs"
ELYDORA_DIR = os.path.join(os.path.expanduser("~"), ".elydora") ELYDORA_DIR = os.path.join(os.path.expanduser("~"), ".elydora")
@@ -62,11 +62,7 @@ class OpenCodePlugin(AgentPlugin):
pass # chmod may fail on Windows pass # chmod may fail on Windows
js_content = _generate_js_plugin( js_content = _generate_js_plugin(
org_id=org_id,
agent_id=agent_id, agent_id=agent_id,
private_key=private_key,
kid=kid,
base_url=base_url,
guard_script_path=guard_script_path, guard_script_path=guard_script_path,
) )
@@ -101,11 +97,7 @@ class OpenCodePlugin(AgentPlugin):
def _generate_js_plugin( def _generate_js_plugin(
*, *,
org_id: str,
agent_id: str, agent_id: str,
private_key: str,
kid: str,
base_url: str,
guard_script_path: str = "", guard_script_path: str = "",
) -> str: ) -> str:
guard_path_escaped = guard_script_path.replace("\\", "\\\\") if guard_script_path else "" guard_path_escaped = guard_script_path.replace("\\", "\\\\") if guard_script_path else ""
@@ -121,13 +113,15 @@ const path = require("path");
const os = require("os"); const os = require("os");
const {{ spawnSync }} = require("child_process"); const {{ spawnSync }} = require("child_process");
const ORG_ID = {_js_str(org_id)}; const CONFIG_DIR = path.join(os.homedir(), ".elydora", {_js_str(agent_id)});
const AGENT_ID = {_js_str(agent_id)}; const CONFIG = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, "config.json"), "utf-8"));
const PRIVATE_KEY = {_js_str(private_key)}; const PRIVATE_KEY = fs.readFileSync(path.join(CONFIG_DIR, "private.key"), "utf-8").trim();
const KID = {_js_str(kid)}; const ORG_ID = CONFIG.org_id;
const BASE_URL = {_js_str(base_url)}; const AGENT_ID = CONFIG.agent_id;
const KID = CONFIG.kid;
const BASE_URL = CONFIG.base_url;
const CHAIN_STATE_PATH = path.join(os.homedir(), ".elydora", AGENT_ID, "chain-state.json"); const CHAIN_STATE_PATH = path.join(CONFIG_DIR, "chain-state.json");
function base64urlEncode(buf) {{ function base64urlEncode(buf) {{
return Buffer.from(buf).toString("base64url"); return Buffer.from(buf).toString("base64url");
@@ -262,7 +256,7 @@ module.exports = {{
issued_at: issuedAt, issued_at: issuedAt,
ttl_ms: 30000, ttl_ms: 30000,
nonce: nonce, nonce: nonce,
operation_type: "tool_use", operation_type: "ai.tool_use",
subject: {{ type: "tool", id: toolName }}, subject: {{ type: "tool", id: toolName }},
action: {{ type: "execute", tool: toolName }}, action: {{ type: "execute", tool: toolName }},
payload: payload, payload: payload,
+2 -2
View File
@@ -79,8 +79,8 @@ class Operation(TypedDict):
issued_at: int issued_at: int
ttl_ms: int ttl_ms: int
nonce: str nonce: str
subject: str subject: Dict[str, Any]
action: str action: Dict[str, Any]
payload_hash: str payload_hash: str
prev_chain_hash: str prev_chain_hash: str
chain_hash: str chain_hash: str