dgc-sdk 0.5.2__tar.gz
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.
- dgc_sdk-0.5.2/PKG-INFO +57 -0
- dgc_sdk-0.5.2/README.md +36 -0
- dgc_sdk-0.5.2/dgc_sdk/__init__.py +40 -0
- dgc_sdk-0.5.2/dgc_sdk/_mcp_bridge.py +248 -0
- dgc_sdk-0.5.2/dgc_sdk/_version.py +5 -0
- dgc_sdk-0.5.2/dgc_sdk/audit.py +86 -0
- dgc_sdk-0.5.2/dgc_sdk/client.py +467 -0
- dgc_sdk-0.5.2/dgc_sdk/errors.py +27 -0
- dgc_sdk-0.5.2/dgc_sdk/policy.py +210 -0
- dgc_sdk-0.5.2/dgc_sdk/py.typed +0 -0
- dgc_sdk-0.5.2/dgc_sdk/retry.py +24 -0
- dgc_sdk-0.5.2/dgc_sdk/runtime.py +148 -0
- dgc_sdk-0.5.2/dgc_sdk/schema.py +122 -0
- dgc_sdk-0.5.2/dgc_sdk/session.py +1259 -0
- dgc_sdk-0.5.2/dgc_sdk/tools.py +8 -0
- dgc_sdk-0.5.2/dgc_sdk/types.py +256 -0
- dgc_sdk-0.5.2/dgc_sdk/usage.py +119 -0
- dgc_sdk-0.5.2/dgc_sdk/wire/__init__.py +1 -0
- dgc_sdk-0.5.2/dgc_sdk/wire/client.py +711 -0
- dgc_sdk-0.5.2/dgc_sdk/wire/editor_protocol.py +832 -0
- dgc_sdk-0.5.2/dgc_sdk/wire/protocol.py +156 -0
- dgc_sdk-0.5.2/dgc_sdk.egg-info/PKG-INFO +57 -0
- dgc_sdk-0.5.2/dgc_sdk.egg-info/SOURCES.txt +25 -0
- dgc_sdk-0.5.2/dgc_sdk.egg-info/dependency_links.txt +1 -0
- dgc_sdk-0.5.2/dgc_sdk.egg-info/top_level.txt +1 -0
- dgc_sdk-0.5.2/pyproject.toml +34 -0
- dgc_sdk-0.5.2/setup.cfg +4 -0
dgc_sdk-0.5.2/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dgc-sdk
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Embed the DGC coding harness in applications and CI
|
|
5
|
+
Author: Mohit Kalra
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://vibedgc.com/sdk/
|
|
8
|
+
Project-URL: Documentation, https://docs.vibedgc.com/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/OpenPeach-ai/dgc
|
|
10
|
+
Project-URL: Issues, https://github.com/OpenPeach-ai/dgc/issues
|
|
11
|
+
Project-URL: Release, https://github.com/OpenPeach-ai/dgc/releases/tag/sdk-v0.5.2
|
|
12
|
+
Keywords: dgc,agent,sdk,coding-agent
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# dgc-sdk
|
|
23
|
+
|
|
24
|
+
Python facade over a managed `dgc serve` process. Protocol v14 / CLI 0.41.5. Frozen 0.5.2.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 -m pip install dgc-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That installs **this** package (`import dgc_sdk`). It is not PyPI `dgc` (an unrelated clustering library). Until the index has this release, use the GitHub wheel:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python3 -m pip install \
|
|
34
|
+
"https://github.com/OpenPeach-ai/dgc/releases/download/sdk-v0.5.2/dgc_sdk-0.5.2-py3-none-any.whl"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from dgc_sdk import DGC, QuestionAnswer, define_tool
|
|
40
|
+
|
|
41
|
+
with DGC(state_dir=Path("/tmp/dgc-sdk-state"), model="demo-model",
|
|
42
|
+
base_url="http://127.0.0.1:11434/v1") as dgc:
|
|
43
|
+
session = dgc.session(
|
|
44
|
+
cwd=".",
|
|
45
|
+
permissions={"mode": "default", "unhandled": "deny"},
|
|
46
|
+
on_permission=lambda req: "deny",
|
|
47
|
+
on_question=lambda req: {req.questions[0].id: QuestionAnswer(selected=(0,))}
|
|
48
|
+
if req.questions else "dismiss",
|
|
49
|
+
)
|
|
50
|
+
result = session.run("Summarize this repository. Do not edit files.")
|
|
51
|
+
print(result.status, result.final_text)
|
|
52
|
+
session.close()
|
|
53
|
+
restored = dgc.resume(latest=True, cwd=".", permissions={"mode": "default", "unhandled": "deny"})
|
|
54
|
+
print(restored.session_id, restored.history().get("items") and "history ok")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
From a clone: `pip install -e sdk/python`. The wheel still needs a DGC runtime (`python -m dgc serve`, CLI 0.41.5). Set `inherit_user_state=False` in production.
|
dgc_sdk-0.5.2/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# dgc-sdk
|
|
2
|
+
|
|
3
|
+
Python facade over a managed `dgc serve` process. Protocol v14 / CLI 0.41.5. Frozen 0.5.2.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python3 -m pip install dgc-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That installs **this** package (`import dgc_sdk`). It is not PyPI `dgc` (an unrelated clustering library). Until the index has this release, use the GitHub wheel:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python3 -m pip install \
|
|
13
|
+
"https://github.com/OpenPeach-ai/dgc/releases/download/sdk-v0.5.2/dgc_sdk-0.5.2-py3-none-any.whl"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from dgc_sdk import DGC, QuestionAnswer, define_tool
|
|
19
|
+
|
|
20
|
+
with DGC(state_dir=Path("/tmp/dgc-sdk-state"), model="demo-model",
|
|
21
|
+
base_url="http://127.0.0.1:11434/v1") as dgc:
|
|
22
|
+
session = dgc.session(
|
|
23
|
+
cwd=".",
|
|
24
|
+
permissions={"mode": "default", "unhandled": "deny"},
|
|
25
|
+
on_permission=lambda req: "deny",
|
|
26
|
+
on_question=lambda req: {req.questions[0].id: QuestionAnswer(selected=(0,))}
|
|
27
|
+
if req.questions else "dismiss",
|
|
28
|
+
)
|
|
29
|
+
result = session.run("Summarize this repository. Do not edit files.")
|
|
30
|
+
print(result.status, result.final_text)
|
|
31
|
+
session.close()
|
|
32
|
+
restored = dgc.resume(latest=True, cwd=".", permissions={"mode": "default", "unhandled": "deny"})
|
|
33
|
+
print(restored.session_id, restored.history().get("items") and "history ok")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
From a clone: `pip install -e sdk/python`. The wheel still needs a DGC runtime (`python -m dgc serve`, CLI 0.41.5). Set `inherit_user_state=False` in production.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""DGC SDK for embedding the harness in applications and CI.
|
|
2
|
+
|
|
3
|
+
Local package. Not published. Version with the TypeScript SDK when the contract changes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ._mcp_bridge import define_tool
|
|
7
|
+
from ._version import PROTOCOL, REQUIRES_CLI, __version__
|
|
8
|
+
from .audit import redact, redact_text
|
|
9
|
+
from .client import AsyncDGC, AsyncRunHandle, AsyncSession, DGC
|
|
10
|
+
from .policy import RuntimePolicy
|
|
11
|
+
from .retry import RetryPolicy
|
|
12
|
+
from .usage import Pricing, cost_usd
|
|
13
|
+
from .errors import (
|
|
14
|
+
DGCConfigError, DGCError, DGCProtocolError, DGCRuntimeError, DGCTimeoutError,
|
|
15
|
+
DGCUnsupportedError,
|
|
16
|
+
)
|
|
17
|
+
from .session import RunHandle, Session
|
|
18
|
+
from .types import (
|
|
19
|
+
AgentInfo, Artifact, Checkpoint, FileChange, Goal, HookInfo, McpInputRequest, McpInputResponse,
|
|
20
|
+
McpServerInfo, Monitor, OnMcpInput, OnPermission, OnPlan, OnQuestion, PermissionAction,
|
|
21
|
+
PermissionMode, PermissionPolicy, PermissionRequest, PermissionRule, PlanAction, PlanRequest,
|
|
22
|
+
Question, QuestionAnswer, QuestionOption, QuestionRequest, RunEvent, RunResult, RunStatus,
|
|
23
|
+
SandboxPolicy, SandboxRequirement, SessionInfo, SkillInfo, TaskItem, TaskStatus, ToolRecord,
|
|
24
|
+
ToolSpec, UnhandledPolicy, VerificationResult,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"PROTOCOL", "REQUIRES_CLI", "__version__",
|
|
29
|
+
"AgentInfo", "Artifact", "AsyncDGC", "AsyncRunHandle", "AsyncSession", "Checkpoint", "DGC",
|
|
30
|
+
"Pricing", "RetryPolicy", "RuntimePolicy", "cost_usd", "redact", "redact_text",
|
|
31
|
+
"DGCConfigError", "DGCError", "DGCProtocolError", "DGCRuntimeError", "DGCTimeoutError",
|
|
32
|
+
"DGCUnsupportedError", "FileChange", "Goal", "HookInfo", "McpInputRequest", "McpInputResponse",
|
|
33
|
+
"McpServerInfo", "Monitor", "OnMcpInput", "OnPermission", "OnPlan", "OnQuestion",
|
|
34
|
+
"PermissionAction", "PermissionMode", "PermissionPolicy", "PermissionRequest",
|
|
35
|
+
"PermissionRule", "PlanAction", "PlanRequest",
|
|
36
|
+
"Question", "QuestionAnswer", "QuestionOption", "QuestionRequest", "RunEvent", "RunHandle",
|
|
37
|
+
"RunResult", "RunStatus", "SandboxPolicy", "SandboxRequirement", "Session", "SessionInfo",
|
|
38
|
+
"SkillInfo", "TaskItem", "TaskStatus", "ToolRecord", "ToolSpec", "UnhandledPolicy",
|
|
39
|
+
"VerificationResult", "define_tool",
|
|
40
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""stdio ↔ Unix-socket relay so DGC can spawn an MCP server that lives in the SDK process.
|
|
2
|
+
|
|
3
|
+
``python -m dgc_sdk._mcp_bridge SOCKET`` copies NDJSON in both directions. The SDK host speaks
|
|
4
|
+
MCP on the accepted socket (initialize, tools/list, tools/call).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import socket
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable, Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sdk_version() -> str:
|
|
20
|
+
try:
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
return __version__
|
|
23
|
+
except ImportError:
|
|
24
|
+
text = Path(__file__).with_name("_version.py").read_text(encoding="utf-8")
|
|
25
|
+
for line in text.splitlines():
|
|
26
|
+
if line.startswith("__version__"):
|
|
27
|
+
return line.split('"', 2)[1]
|
|
28
|
+
return "0.0.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def relay(socket_path: str) -> int:
|
|
32
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
33
|
+
sock.connect(socket_path)
|
|
34
|
+
|
|
35
|
+
def stdin_to_sock() -> None:
|
|
36
|
+
try:
|
|
37
|
+
while True:
|
|
38
|
+
line = sys.stdin.buffer.readline()
|
|
39
|
+
if not line:
|
|
40
|
+
break
|
|
41
|
+
sock.sendall(line)
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
try:
|
|
45
|
+
sock.shutdown(socket.SHUT_WR)
|
|
46
|
+
except OSError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
threading.Thread(target=stdin_to_sock, daemon=True).start()
|
|
50
|
+
try:
|
|
51
|
+
while True:
|
|
52
|
+
chunk = sock.recv(65536)
|
|
53
|
+
if not chunk:
|
|
54
|
+
break
|
|
55
|
+
sys.stdout.buffer.write(chunk)
|
|
56
|
+
sys.stdout.buffer.flush()
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ToolHub:
|
|
63
|
+
"""Accept one MCP stdio proxy and dispatch ``tools/call`` to host handlers."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, socket_path: str, tools: SequenceToolMap):
|
|
66
|
+
self.socket_path = socket_path
|
|
67
|
+
self.tools = {item.name: item for item in tools}
|
|
68
|
+
self._server: socket.socket | None = None
|
|
69
|
+
self._thread: threading.Thread | None = None
|
|
70
|
+
|
|
71
|
+
def start(self) -> None:
|
|
72
|
+
if os.path.exists(self.socket_path):
|
|
73
|
+
os.unlink(self.socket_path)
|
|
74
|
+
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
75
|
+
server.bind(self.socket_path)
|
|
76
|
+
server.listen(4)
|
|
77
|
+
server.settimeout(1.0)
|
|
78
|
+
self._server = server
|
|
79
|
+
self._thread = threading.Thread(target=self._accept, name="dgc-sdk-mcp", daemon=True)
|
|
80
|
+
self._thread.start()
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
if self._server is not None:
|
|
84
|
+
try:
|
|
85
|
+
self._server.close()
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|
|
88
|
+
if os.path.exists(self.socket_path):
|
|
89
|
+
try:
|
|
90
|
+
os.unlink(self.socket_path)
|
|
91
|
+
except OSError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def _accept(self) -> None:
|
|
95
|
+
assert self._server is not None
|
|
96
|
+
while True:
|
|
97
|
+
try:
|
|
98
|
+
conn, _addr = self._server.accept()
|
|
99
|
+
except TimeoutError:
|
|
100
|
+
continue
|
|
101
|
+
except OSError:
|
|
102
|
+
return
|
|
103
|
+
threading.Thread(target=self._serve, args=(conn,), daemon=True).start()
|
|
104
|
+
|
|
105
|
+
def _serve(self, conn: socket.socket) -> None:
|
|
106
|
+
buf = b""
|
|
107
|
+
try:
|
|
108
|
+
while True:
|
|
109
|
+
chunk = conn.recv(65536)
|
|
110
|
+
if not chunk:
|
|
111
|
+
return
|
|
112
|
+
buf += chunk
|
|
113
|
+
while b"\n" in buf:
|
|
114
|
+
line, buf = buf.split(b"\n", 1)
|
|
115
|
+
line = line.strip()
|
|
116
|
+
if not line:
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
message = json.loads(line.decode("utf-8"))
|
|
120
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
121
|
+
continue
|
|
122
|
+
reply = self._handle(message)
|
|
123
|
+
if reply is not None:
|
|
124
|
+
conn.sendall((json.dumps(reply, separators=(",", ":")) + "\n").encode())
|
|
125
|
+
except OSError:
|
|
126
|
+
return
|
|
127
|
+
finally:
|
|
128
|
+
try:
|
|
129
|
+
conn.close()
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def _handle(self, message: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
134
|
+
method = str(message.get("method") or "")
|
|
135
|
+
mid = message.get("id")
|
|
136
|
+
if method == "initialize":
|
|
137
|
+
return {
|
|
138
|
+
"jsonrpc": "2.0", "id": mid,
|
|
139
|
+
"result": {
|
|
140
|
+
"protocolVersion": "2025-11-25",
|
|
141
|
+
"capabilities": {"tools": {}},
|
|
142
|
+
"serverInfo": {"name": "dgc-sdk", "version": _sdk_version()},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
if method in ("notifications/initialized", "notifications/cancelled"):
|
|
146
|
+
return None
|
|
147
|
+
if method == "ping":
|
|
148
|
+
return {"jsonrpc": "2.0", "id": mid, "result": {}}
|
|
149
|
+
if method == "tools/list":
|
|
150
|
+
tools = []
|
|
151
|
+
for spec in self.tools.values():
|
|
152
|
+
tools.append({
|
|
153
|
+
"name": spec.name,
|
|
154
|
+
"description": spec.description,
|
|
155
|
+
"inputSchema": dict(spec.input_schema) or {"type": "object", "properties": {}},
|
|
156
|
+
})
|
|
157
|
+
return {"jsonrpc": "2.0", "id": mid, "result": {"tools": tools}}
|
|
158
|
+
if method == "tools/call":
|
|
159
|
+
params = message.get("params") if isinstance(message.get("params"), dict) else {}
|
|
160
|
+
name = str(params.get("name") or "")
|
|
161
|
+
arguments = params.get("arguments") if isinstance(params.get("arguments"), dict) else {}
|
|
162
|
+
spec = self.tools.get(name)
|
|
163
|
+
if spec is None:
|
|
164
|
+
return {"jsonrpc": "2.0", "id": mid, "error": {
|
|
165
|
+
"code": -32601, "message": f"unknown tool {name}"}}
|
|
166
|
+
try:
|
|
167
|
+
result = self._call_handler(spec, arguments)
|
|
168
|
+
except TimeoutError:
|
|
169
|
+
return {"jsonrpc": "2.0", "id": mid, "result": {
|
|
170
|
+
"content": [{"type": "text", "text": "tool error: TimeoutError: handler exceeded timeout"}],
|
|
171
|
+
"isError": True,
|
|
172
|
+
}}
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
return {"jsonrpc": "2.0", "id": mid, "result": {
|
|
175
|
+
"content": [{"type": "text", "text": f"tool error: {type(exc).__name__}: {exc}"}],
|
|
176
|
+
"isError": True,
|
|
177
|
+
}}
|
|
178
|
+
text = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False)
|
|
179
|
+
return {"jsonrpc": "2.0", "id": mid, "result": {
|
|
180
|
+
"content": [{"type": "text", "text": text[:120_000]}],
|
|
181
|
+
"isError": False,
|
|
182
|
+
}}
|
|
183
|
+
if mid is not None:
|
|
184
|
+
return {"jsonrpc": "2.0", "id": mid, "error": {
|
|
185
|
+
"code": -32601, "message": f"unsupported method {method}"}}
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def _call_handler(self, spec, arguments):
|
|
189
|
+
timeout = getattr(spec, "timeout", None)
|
|
190
|
+
if timeout is None:
|
|
191
|
+
return spec.handler(arguments)
|
|
192
|
+
box: list = []
|
|
193
|
+
error: list = []
|
|
194
|
+
|
|
195
|
+
def worker() -> None:
|
|
196
|
+
try:
|
|
197
|
+
box.append(spec.handler(arguments))
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
error.append(exc)
|
|
200
|
+
|
|
201
|
+
thread = threading.Thread(target=worker, daemon=True)
|
|
202
|
+
thread.start()
|
|
203
|
+
thread.join(float(timeout))
|
|
204
|
+
if thread.is_alive():
|
|
205
|
+
raise TimeoutError("handler exceeded timeout")
|
|
206
|
+
if error:
|
|
207
|
+
raise error[0]
|
|
208
|
+
return box[0] if box else None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Imported after types to keep this module usable as ``python -m``.
|
|
212
|
+
try:
|
|
213
|
+
from .types import ToolSpec
|
|
214
|
+
SequenceToolMap = list[ToolSpec]
|
|
215
|
+
except Exception: # pragma: no cover - running as a frozen proxy
|
|
216
|
+
ToolSpec = Any # type: ignore[misc,assignment]
|
|
217
|
+
SequenceToolMap = list
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def define_tool(name: str, description: str, input_schema: Mapping[str, Any],
|
|
221
|
+
handler: Callable[[Mapping[str, Any]], Any],
|
|
222
|
+
timeout: float | None = 30.0) -> "ToolSpec":
|
|
223
|
+
from .errors import DGCConfigError
|
|
224
|
+
from .types import ToolSpec as Spec
|
|
225
|
+
if not name or not isinstance(name, str) or not name.replace("_", "").replace("-", "").isalnum():
|
|
226
|
+
raise DGCConfigError("tool name must be a non-empty identifier")
|
|
227
|
+
if not description or not isinstance(description, str):
|
|
228
|
+
raise DGCConfigError("tool description is required")
|
|
229
|
+
if not isinstance(input_schema, Mapping):
|
|
230
|
+
raise DGCConfigError("tool input_schema must be an object")
|
|
231
|
+
if not callable(handler):
|
|
232
|
+
raise DGCConfigError("tool handler must be callable")
|
|
233
|
+
if timeout is not None and (not isinstance(timeout, (int, float)) or timeout <= 0):
|
|
234
|
+
raise DGCConfigError("tool timeout must be a positive number of seconds")
|
|
235
|
+
return Spec(name=name, description=description, input_schema=dict(input_schema),
|
|
236
|
+
handler=handler, timeout=None if timeout is None else float(timeout))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main(argv: list[str] | None = None) -> int:
|
|
240
|
+
args = sys.argv[1:] if argv is None else argv
|
|
241
|
+
if not args:
|
|
242
|
+
sys.stderr.write("usage: python -m dgc_sdk._mcp_bridge SOCKET\n")
|
|
243
|
+
return 2
|
|
244
|
+
return relay(args[0])
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Run audit log with secret redaction. Stored in the isolated state_dir, not host ~/.dgc."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
_SECRET_RE = re.compile(
|
|
12
|
+
r"(?i)(sk-[A-Za-z0-9]{8,}|bearer\s+[A-Za-z0-9._\-]+|api[_-]?key\s*[:=]\s*\S+|"
|
|
13
|
+
r"authorization\s*[:=]\s*\S+|x-api-key\s*[:=]\s*\S+)"
|
|
14
|
+
)
|
|
15
|
+
_KEYISH = re.compile(r"(?i)(api_key|token|secret|password|authorization|passwd)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def redact_text(value: str) -> str:
|
|
19
|
+
return _SECRET_RE.sub("[redacted]", value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def redact(value: Any) -> Any:
|
|
23
|
+
if isinstance(value, str):
|
|
24
|
+
return redact_text(value)
|
|
25
|
+
if isinstance(value, Mapping):
|
|
26
|
+
out = {}
|
|
27
|
+
for key, item in value.items():
|
|
28
|
+
if _KEYISH.search(str(key)):
|
|
29
|
+
out[str(key)] = "[redacted]"
|
|
30
|
+
else:
|
|
31
|
+
out[str(key)] = redact(item)
|
|
32
|
+
return out
|
|
33
|
+
if isinstance(value, list):
|
|
34
|
+
return [redact(item) for item in value[:80]]
|
|
35
|
+
if isinstance(value, tuple):
|
|
36
|
+
return [redact(item) for item in value[:80]]
|
|
37
|
+
if isinstance(value, (int, float, bool)) or value is None:
|
|
38
|
+
return value
|
|
39
|
+
return redact_text(str(value)[:2000])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AuditLog:
|
|
43
|
+
def __init__(self, directory: Path):
|
|
44
|
+
self.directory = Path(directory)
|
|
45
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
self._lock = threading.Lock()
|
|
47
|
+
|
|
48
|
+
def path_for(self, session_id: str) -> Path:
|
|
49
|
+
stem = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in (session_id or "unknown"))[:80]
|
|
50
|
+
return self.directory / f"{stem or 'unknown'}.jsonl"
|
|
51
|
+
|
|
52
|
+
def append(self, session_id: str, run_id: str, kind: str, payload: Mapping[str, Any],
|
|
53
|
+
*, do_redact: bool = True) -> None:
|
|
54
|
+
body = redact(dict(payload)) if do_redact else dict(payload)
|
|
55
|
+
row = {
|
|
56
|
+
"ts": time.time(),
|
|
57
|
+
"session_id": session_id,
|
|
58
|
+
"run_id": run_id,
|
|
59
|
+
"type": kind,
|
|
60
|
+
"payload": body,
|
|
61
|
+
}
|
|
62
|
+
line = json.dumps(row, ensure_ascii=False, default=str) + "\n"
|
|
63
|
+
path = self.path_for(session_id)
|
|
64
|
+
with self._lock:
|
|
65
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
66
|
+
handle.write(line)
|
|
67
|
+
|
|
68
|
+
def export(self, session_id: str | None = None, *, redact_output: bool = True) -> list[dict[str, Any]]:
|
|
69
|
+
paths = [self.path_for(session_id)] if session_id else sorted(self.directory.glob("*.jsonl"))
|
|
70
|
+
rows: list[dict[str, Any]] = []
|
|
71
|
+
for path in paths:
|
|
72
|
+
if not path.is_file():
|
|
73
|
+
continue
|
|
74
|
+
with path.open(encoding="utf-8") as handle:
|
|
75
|
+
for line in handle:
|
|
76
|
+
line = line.strip()
|
|
77
|
+
if not line:
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
row = json.loads(line)
|
|
81
|
+
except json.JSONDecodeError:
|
|
82
|
+
continue
|
|
83
|
+
if redact_output:
|
|
84
|
+
row = redact(row)
|
|
85
|
+
rows.append(row)
|
|
86
|
+
return rows
|