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 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.
@@ -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,5 @@
1
+ """Local SDK version. Bump with the TypeScript package when the contract changes."""
2
+
3
+ __version__ = "0.5.2"
4
+ PROTOCOL = 14
5
+ REQUIRES_CLI = "0.41.3"
@@ -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