eda-bridge-runtime 0.1.0a5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ """Agent-neutral execution runtime for EDA bridges."""
2
+
3
+ from ._version import __version__
4
+ from .adapter import Adapter, AdapterContext, AdapterResult
5
+ from .connections import ConnectionRegistry, ConnectionSpec, discover_connection_origin
6
+ from .context import EDAContext, capability_digest, stable_origin_id
7
+ from .jobs import JobStore
8
+ from .ledger import ExecutionLedger
9
+ from .protocol import ActorIdentity, RequestEnvelope, ResponseEnvelope, project_run
10
+ from .runtime import Runtime
11
+ from .supervisor import run_job_worker, spawn_detached_worker
12
+
13
+ __all__ = [
14
+ "__version__",
15
+ "ActorIdentity",
16
+ "Adapter",
17
+ "AdapterContext",
18
+ "AdapterResult",
19
+ "ConnectionRegistry",
20
+ "ConnectionSpec",
21
+ "discover_connection_origin",
22
+ "EDAContext",
23
+ "capability_digest",
24
+ "ExecutionLedger",
25
+ "JobStore",
26
+ "RequestEnvelope",
27
+ "ResponseEnvelope",
28
+ "Runtime",
29
+ "project_run",
30
+ "run_job_worker",
31
+ "spawn_detached_worker",
32
+ "stable_origin_id",
33
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0a5"
@@ -0,0 +1,36 @@
1
+ """Minimal adapter SDK. Vendor semantics stay outside the runtime core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ from .protocol import RequestEnvelope
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AdapterResult:
15
+ status: str
16
+ result: dict[str, Any] = field(default_factory=dict)
17
+ artifacts: tuple[dict[str, Any], ...] = ()
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class AdapterContext:
22
+ emit: Callable[[str, dict[str, Any]], None]
23
+ fencing_token: int | None = None
24
+
25
+
26
+ class Adapter(ABC):
27
+ name: str
28
+ version: str
29
+
30
+ @abstractmethod
31
+ def capabilities(self, target: dict[str, Any] | None = None) -> dict[str, Any]:
32
+ """Return typed operations and explicit escape lanes for an optional target."""
33
+
34
+ @abstractmethod
35
+ def execute(self, request: RequestEnvelope, context: AdapterContext) -> AdapterResult:
36
+ """Execute one idempotent request and return normalized evidence."""
@@ -0,0 +1,152 @@
1
+ """Agent-host audit events captured outside the model context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ from collections.abc import Mapping
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .ledger import ExecutionLedger
14
+ from .protocol import RUN_VIEW_PROTOCOL, ActorIdentity
15
+
16
+ AGENT_AUDIT_PROTOCOL = "eda-runtime.agent-audit/v1"
17
+ _RUNTIME_TOOL = re.compile(r"^mcp__(?P<server>.+)__(?P<tool>.+)$")
18
+ _TOOL_NAMES = {
19
+ "eda_capabilities": "eda.capabilities",
20
+ "eda_submit": "eda.submit",
21
+ "eda_job_status": "eda.job.status",
22
+ "eda_job_events": "eda.job.events",
23
+ "eda_connections_list": "eda.connections.list",
24
+ "eda_context_resolve": "eda.context.resolve",
25
+ "eda.capabilities": "eda.capabilities",
26
+ "eda.submit": "eda.submit",
27
+ "eda.job.status": "eda.job.status",
28
+ "eda.job.events": "eda.job.events",
29
+ "eda.connections.list": "eda.connections.list",
30
+ "eda.context.resolve": "eda.context.resolve",
31
+ }
32
+
33
+
34
+ def default_agent_audit_path() -> Path:
35
+ root = Path(os.environ.get("EDA_RUNTIME_HOME", Path.home() / ".eda-bridge-runtime"))
36
+ return root / "agent-audit.sqlite3"
37
+
38
+
39
+ def record_codex_hook(
40
+ event: Mapping[str, Any],
41
+ *,
42
+ phase: str,
43
+ database: str | Path | None = None,
44
+ ) -> bool:
45
+ """Record one Codex lifecycle fact without changing the pending tool call."""
46
+ if phase not in {"pre", "post"}:
47
+ raise ValueError(f"unsupported hook phase: {phase}")
48
+ tool_name = str(event.get("tool_name") or "")
49
+ match = _RUNTIME_TOOL.fullmatch(tool_name)
50
+ if match is None:
51
+ return False
52
+ server_name = match.group("server").lower().replace("-", "_")
53
+ native_tool_name = match.group("tool")
54
+ if server_name != "eda_bridge_runtime" or native_tool_name not in _TOOL_NAMES:
55
+ return False
56
+ tool_call_id = str(event.get("tool_use_id") or "").strip()
57
+ session_id = str(event.get("session_id") or "").strip()
58
+ if not tool_call_id or not session_id:
59
+ return False
60
+ tool_input = event.get("tool_input")
61
+ tool_input = tool_input if isinstance(tool_input, Mapping) else {}
62
+ purpose = str(tool_input.get("purpose") or "unspecified EDA operation")[:240]
63
+ fingerprint = _fingerprint(tool_name, tool_input)
64
+ actor = ActorIdentity.detect(
65
+ observed={
66
+ key: value
67
+ for key, value in {
68
+ "agent_family": "codex",
69
+ "model": event.get("model"),
70
+ "session_id": session_id,
71
+ "turn_id": event.get("turn_id"),
72
+ "tool_call_id": tool_call_id,
73
+ "permission_mode": event.get("permission_mode"),
74
+ }.items()
75
+ if value
76
+ },
77
+ inferred={"harness": "mcp"},
78
+ )
79
+ run_id = _audit_run_id(tool_call_id)
80
+ request_id = str(event.get("turn_id") or session_id)
81
+ payload: dict[str, Any] = {
82
+ "protocol": AGENT_AUDIT_PROTOCOL,
83
+ "actor": actor.to_dict(),
84
+ "tool": _TOOL_NAMES[native_tool_name],
85
+ "purpose": purpose,
86
+ "input_sha256": fingerprint,
87
+ }
88
+ event_type = "agent.tool.requested"
89
+ if phase == "post":
90
+ event_type = "agent.tool.completed"
91
+ payload["execution"] = _execution_reference(event.get("tool_response"))
92
+ with ExecutionLedger(database or default_agent_audit_path()) as ledger:
93
+ ledger.append(
94
+ run_id=run_id,
95
+ request_id=request_id,
96
+ event_type=event_type,
97
+ source="codex-hook",
98
+ payload=payload,
99
+ )
100
+ if phase == "post":
101
+ ledger.finalize(run_id)
102
+ return True
103
+
104
+
105
+ def audit_events(database: str | Path | None = None, *, limit: int = 20) -> list[dict[str, Any]]:
106
+ with ExecutionLedger(database or default_agent_audit_path()) as ledger:
107
+ events = ledger.events()
108
+ return events[-max(1, min(limit, 1000)) :]
109
+
110
+
111
+ def _audit_run_id(tool_call_id: str) -> str:
112
+ digest = hashlib.sha256(tool_call_id.encode("utf-8")).hexdigest()[:32]
113
+ return f"agent_{digest}"
114
+
115
+
116
+ def _fingerprint(tool_name: str, tool_input: Mapping[str, Any]) -> str:
117
+ material = {"tool_name": tool_name, "tool_input": tool_input}
118
+ canonical = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
119
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
120
+
121
+
122
+ def _execution_reference(value: Any) -> dict[str, Any]:
123
+ run = _find_run_view(value)
124
+ if run is None:
125
+ return {"linked": False}
126
+ return {
127
+ "linked": True,
128
+ "run_id": str(run.get("run_id") or "") or None,
129
+ "request_id": str(run.get("request_id") or "") or None,
130
+ "job_id": str(run.get("job_id") or "") or None,
131
+ "state": str(run.get("state") or "unknown"),
132
+ "terminal": bool(run.get("terminal", False)),
133
+ }
134
+
135
+
136
+ def _find_run_view(value: Any, *, depth: int = 0) -> Mapping[str, Any] | None:
137
+ if depth > 8:
138
+ return None
139
+ if isinstance(value, Mapping):
140
+ if value.get("protocol") == RUN_VIEW_PROTOCOL:
141
+ return value
142
+ run = value.get("run")
143
+ if isinstance(run, Mapping) and run.get("protocol") == RUN_VIEW_PROTOCOL:
144
+ return run
145
+ for item in value.values():
146
+ if found := _find_run_view(item, depth=depth + 1):
147
+ return found
148
+ elif isinstance(value, list):
149
+ for item in value:
150
+ if found := _find_run_view(item, depth=depth + 1):
151
+ return found
152
+ return None
@@ -0,0 +1,43 @@
1
+ """Content-addressed artifact evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import asdict, dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Artifact:
12
+ logical_name: str
13
+ path: str
14
+ sha256: str
15
+ size: int
16
+ media_type: str = "application/octet-stream"
17
+ retention_days: int | None = None
18
+
19
+ @classmethod
20
+ def from_path(
21
+ cls,
22
+ path: str | Path,
23
+ *,
24
+ logical_name: str | None = None,
25
+ media_type: str = "application/octet-stream",
26
+ retention_days: int | None = None,
27
+ ) -> Artifact:
28
+ source = Path(path)
29
+ digest = hashlib.sha256()
30
+ with source.open("rb") as stream:
31
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
32
+ digest.update(chunk)
33
+ return cls(
34
+ logical_name or source.name,
35
+ str(source),
36
+ digest.hexdigest(),
37
+ source.stat().st_size,
38
+ media_type,
39
+ retention_days,
40
+ )
41
+
42
+ def to_dict(self) -> dict[str, str | int | None]:
43
+ return asdict(self)
@@ -0,0 +1,154 @@
1
+ """Small operational CLI; EDA-specific commands are supplied by adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ._version import __version__
11
+ from .connections import ConnectionRegistry, ConnectionSpec, discover_connection_origin
12
+ from .context import EDAContext
13
+ from .ledger import ExecutionLedger
14
+ from .protocol import ActorIdentity, RuntimeFacts
15
+
16
+
17
+ def _parser() -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(prog="eda-runtime")
19
+ parser.add_argument("--version", action="version", version=__version__)
20
+ sub = parser.add_subparsers(dest="command", required=True)
21
+ sub.add_parser("doctor")
22
+ context = sub.add_parser("context")
23
+ context_sub = context.add_subparsers(dest="context_command", required=True)
24
+ decode = context_sub.add_parser("decode")
25
+ decode.add_argument("token")
26
+ ledger = sub.add_parser("ledger")
27
+ ledger_sub = ledger.add_subparsers(dest="ledger_command", required=True)
28
+ verify = ledger_sub.add_parser("verify")
29
+ verify.add_argument("database", type=Path)
30
+ verify.add_argument("run_id")
31
+ export = ledger_sub.add_parser("export")
32
+ export.add_argument("database", type=Path)
33
+ export.add_argument("destination", type=Path)
34
+ connection = sub.add_parser("connection")
35
+ connection_sub = connection.add_subparsers(dest="connection_command", required=True)
36
+ for action in ("list", "set", "remove"):
37
+ item = connection_sub.add_parser(action)
38
+ item.add_argument("--registry", type=Path)
39
+ if action in {"set", "remove"}:
40
+ item.add_argument("connection_id")
41
+ if action == "set":
42
+ item.add_argument("--eda", required=True)
43
+ item.add_argument("--kind", choices=("local", "ssh"), required=True)
44
+ item.add_argument("--host")
45
+ item.add_argument("--origin-id")
46
+ item.add_argument("--no-origin-probe", action="store_true")
47
+ item.add_argument("--ssh-option", action="append", default=[])
48
+ item.add_argument("--timeout-seconds", type=float, default=30)
49
+ item.add_argument("launch_command", nargs=argparse.REMAINDER)
50
+ mcp = sub.add_parser("mcp")
51
+ mcp_sub = mcp.add_subparsers(dest="mcp_command", required=True)
52
+ mcp_serve = mcp_sub.add_parser("serve")
53
+ mcp_serve.add_argument("--registry", type=Path)
54
+ hook = sub.add_parser("hook")
55
+ hook_sub = hook.add_subparsers(dest="hook_command", required=True)
56
+ for phase in ("codex-pre-tool-use", "codex-post-tool-use"):
57
+ item = hook_sub.add_parser(phase)
58
+ item.add_argument("--database", type=Path)
59
+ audit = sub.add_parser("audit")
60
+ audit_sub = audit.add_subparsers(dest="audit_command", required=True)
61
+ audit_list = audit_sub.add_parser("list")
62
+ audit_list.add_argument("--database", type=Path)
63
+ audit_list.add_argument("--limit", type=int, default=20)
64
+ return parser
65
+
66
+
67
+ def main(argv: list[str] | None = None) -> int:
68
+ args = _parser().parse_args(argv)
69
+ if args.command == "doctor":
70
+ print(
71
+ json.dumps(
72
+ {
73
+ "status": "ok",
74
+ "runtime": RuntimeFacts(__version__).to_dict(),
75
+ "actor": ActorIdentity.detect().to_dict(),
76
+ "protocols": {"request": 1, "context": [1, 2], "handshake": 1},
77
+ },
78
+ indent=2,
79
+ )
80
+ )
81
+ return 0
82
+ if args.command == "context" and args.context_command == "decode":
83
+ print(json.dumps(EDAContext.decode(args.token).__dict__, indent=2))
84
+ return 0
85
+ if args.command == "connection":
86
+ registry = ConnectionRegistry(args.registry)
87
+ if args.connection_command == "list":
88
+ print(
89
+ json.dumps(
90
+ {
91
+ "status": "ready",
92
+ "connections": [item.to_dict() for item in registry.list()],
93
+ },
94
+ indent=2,
95
+ )
96
+ )
97
+ return 0
98
+ if args.connection_command == "remove":
99
+ removed = registry.remove(args.connection_id)
100
+ print(json.dumps({"status": "removed" if removed else "not_found"}))
101
+ return 0 if removed else 1
102
+ launch_command = list(args.launch_command)
103
+ if launch_command and launch_command[0] == "--":
104
+ launch_command.pop(0)
105
+ spec = ConnectionSpec(
106
+ connection_id=args.connection_id,
107
+ eda=args.eda,
108
+ kind=args.kind,
109
+ command=tuple(launch_command),
110
+ host=args.host,
111
+ ssh_options=tuple(args.ssh_option),
112
+ timeout_seconds=args.timeout_seconds,
113
+ origin_id=args.origin_id,
114
+ )
115
+ if not spec.origin_id and not args.no_origin_probe:
116
+ spec = discover_connection_origin(spec)
117
+ registry.upsert(spec)
118
+ print(json.dumps({"status": "ready", "connection": spec.to_dict()}, indent=2))
119
+ return 0
120
+ if args.command == "mcp" and args.mcp_command == "serve":
121
+ from .mcp_server import serve_mcp
122
+
123
+ serve_mcp(registry=ConnectionRegistry(args.registry))
124
+ return 0
125
+ if args.command == "hook":
126
+ from .agent_audit import record_codex_hook
127
+
128
+ try:
129
+ event = json.load(sys.stdin)
130
+ phase = "pre" if args.hook_command == "codex-pre-tool-use" else "post"
131
+ record_codex_hook(event, phase=phase, database=args.database)
132
+ except Exception:
133
+ # Audit is fail-open: telemetry must never alter or block an EDA call.
134
+ return 0
135
+ return 0
136
+ if args.command == "audit" and args.audit_command == "list":
137
+ from .agent_audit import audit_events
138
+
139
+ print(json.dumps({"events": audit_events(args.database, limit=args.limit)}, indent=2))
140
+ return 0
141
+ if args.command == "ledger":
142
+ with ExecutionLedger(args.database) as ledger:
143
+ if args.ledger_command == "verify":
144
+ valid = ledger.verify(args.run_id)
145
+ print(json.dumps({"run_id": args.run_id, "valid": valid}))
146
+ return 0 if valid else 1
147
+ ledger.export_ndjson(args.destination)
148
+ print(json.dumps({"status": "ok", "destination": str(args.destination)}))
149
+ return 0
150
+ return 2
151
+
152
+
153
+ if __name__ == "__main__":
154
+ sys.exit(main())
@@ -0,0 +1,192 @@
1
+ """Deterministic local and SSH adapter connection registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import tempfile
9
+ from dataclasses import asdict, dataclass, replace
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .protocol import RequestEnvelope
14
+ from .transport import PersistentStdioTransport, SSHStdioTransport, Transport
15
+
16
+ _ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$")
17
+
18
+
19
+ def runtime_home() -> Path:
20
+ return Path(os.environ.get("EDA_RUNTIME_HOME", Path.home() / ".eda-bridge-runtime"))
21
+
22
+
23
+ def default_connections_path() -> Path:
24
+ return runtime_home() / "connections.json"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ConnectionSpec:
29
+ connection_id: str
30
+ eda: str
31
+ kind: str
32
+ command: tuple[str, ...]
33
+ host: str | None = None
34
+ ssh_options: tuple[str, ...] = ()
35
+ timeout_seconds: float = 30
36
+ origin_id: str | None = None
37
+
38
+ def __post_init__(self) -> None:
39
+ if not _ID.fullmatch(self.connection_id):
40
+ raise ValueError("connection_id must be 1..64 safe identifier characters")
41
+ if not self.eda.strip() or not self.command:
42
+ raise ValueError("eda and command are required")
43
+ if self.kind not in {"local", "ssh"}:
44
+ raise ValueError("connection kind must be local or ssh")
45
+ if self.kind == "ssh" and not self.host:
46
+ raise ValueError("SSH connections require host")
47
+ if self.kind == "local" and self.host:
48
+ raise ValueError("local connections must not define host")
49
+ if self.timeout_seconds <= 0:
50
+ raise ValueError("timeout_seconds must be positive")
51
+ if self.origin_id and not _ID.fullmatch(self.origin_id):
52
+ raise ValueError("origin_id must be 1..64 safe identifier characters")
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ value = asdict(self)
56
+ value["command"] = list(self.command)
57
+ value["ssh_options"] = list(self.ssh_options)
58
+ return value
59
+
60
+ @classmethod
61
+ def from_dict(cls, value: dict[str, Any]) -> ConnectionSpec:
62
+ data = dict(value)
63
+ data["command"] = tuple(data.get("command", ()))
64
+ data["ssh_options"] = tuple(data.get("ssh_options", ()))
65
+ return cls(**data)
66
+
67
+ def open(self) -> Transport:
68
+ if self.kind == "local":
69
+ return PersistentStdioTransport(self.command, timeout_seconds=self.timeout_seconds)
70
+ return SSHStdioTransport(
71
+ str(self.host),
72
+ self.command,
73
+ ssh_options=self.ssh_options,
74
+ timeout_seconds=self.timeout_seconds,
75
+ )
76
+
77
+
78
+ def discover_connection_origin(
79
+ spec: ConnectionSpec, *, transport: Transport | None = None
80
+ ) -> ConnectionSpec:
81
+ """Read one adapter identity during connection setup and bind its stable origin."""
82
+
83
+ owned = transport is None
84
+ selected = transport or spec.open()
85
+ try:
86
+ response = selected.request(
87
+ RequestEnvelope(
88
+ purpose="Bind one registered EDA connection to its stable origin",
89
+ target={"eda": spec.eda, "connection_id": spec.connection_id},
90
+ operation="runtime.capabilities",
91
+ payload={"mutating": False},
92
+ )
93
+ )
94
+ finally:
95
+ if owned:
96
+ selected.close()
97
+ data = response.result.get("data") if isinstance(response.result, dict) else None
98
+ capabilities = data.get("capabilities") if isinstance(data, dict) else None
99
+ origin_id = str((capabilities or {}).get("origin_id") or "")
100
+ if response.status != "passed" or not _ID.fullmatch(origin_id):
101
+ raise ValueError("EDA adapter did not return a valid stable origin_id")
102
+ return replace(spec, origin_id=origin_id)
103
+
104
+
105
+ class ConnectionRegistry:
106
+ def __init__(self, path: str | Path | None = None):
107
+ self.path = Path(path) if path else default_connections_path()
108
+
109
+ def list(self) -> list[ConnectionSpec]:
110
+ if not self.path.is_file():
111
+ return []
112
+ data = json.loads(self.path.read_text(encoding="utf-8"))
113
+ if data.get("schema_version") != 1 or not isinstance(data.get("connections"), list):
114
+ raise ValueError(f"unsupported connection registry: {self.path}")
115
+ return sorted(
116
+ (ConnectionSpec.from_dict(item) for item in data["connections"]),
117
+ key=lambda item: item.connection_id,
118
+ )
119
+
120
+ def upsert(self, spec: ConnectionSpec) -> ConnectionSpec:
121
+ values = {item.connection_id: item for item in self.list()}
122
+ values[spec.connection_id] = spec
123
+ self._write(list(values.values()))
124
+ return spec
125
+
126
+ def remove(self, connection_id: str) -> bool:
127
+ values = {item.connection_id: item for item in self.list()}
128
+ removed = values.pop(connection_id, None) is not None
129
+ if removed:
130
+ self._write(list(values.values()))
131
+ return removed
132
+
133
+ def resolve(
134
+ self,
135
+ *,
136
+ connection_id: str | None = None,
137
+ eda: str | None = None,
138
+ origin_id: str | None = None,
139
+ ) -> ConnectionSpec:
140
+ values = self.list()
141
+ if connection_id:
142
+ match = next((item for item in values if item.connection_id == connection_id), None)
143
+ if not match:
144
+ raise ValueError(f"unknown EDA connection: {connection_id}")
145
+ if eda and match.eda != eda:
146
+ raise ValueError(f"connection {connection_id} does not target {eda}")
147
+ if origin_id and match.origin_id not in {None, origin_id}:
148
+ raise ValueError(f"connection {connection_id} does not target origin {origin_id}")
149
+ return match
150
+ if origin_id:
151
+ matches = [
152
+ item
153
+ for item in values
154
+ if item.origin_id == origin_id and (not eda or item.eda == eda)
155
+ ]
156
+ if len(matches) == 1:
157
+ return matches[0]
158
+ legacy = [item for item in values if not eda or item.eda == eda]
159
+ if len(matches) == 0 and len(legacy) == 1 and legacy[0].origin_id is None:
160
+ return legacy[0]
161
+ raise ValueError(
162
+ f"origin {origin_id} resolves to {len(matches)} connections; "
163
+ "register one exact origin binding"
164
+ )
165
+ matches = [item for item in values if not eda or item.eda == eda]
166
+ if len(matches) != 1:
167
+ target = eda or "requested EDA"
168
+ raise ValueError(
169
+ f"{target} resolves to {len(matches)} connections; provide a captured context "
170
+ "with connection_id or select one connection"
171
+ )
172
+ return matches[0]
173
+
174
+ def _write(self, values: list[ConnectionSpec]) -> None:
175
+ self.path.parent.mkdir(parents=True, exist_ok=True)
176
+ payload = {
177
+ "schema_version": 1,
178
+ "connections": [
179
+ item.to_dict() for item in sorted(values, key=lambda x: x.connection_id)
180
+ ],
181
+ }
182
+ handle, temporary = tempfile.mkstemp(
183
+ prefix="connections-", suffix=".json", dir=self.path.parent
184
+ )
185
+ try:
186
+ with os.fdopen(handle, "w", encoding="utf-8") as stream:
187
+ json.dump(payload, stream, indent=2, sort_keys=True)
188
+ stream.write("\n")
189
+ os.replace(temporary, self.path)
190
+ finally:
191
+ if os.path.exists(temporary):
192
+ os.unlink(temporary)