dejima-sdk 0.5.2__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.
- dejima/__init__.py +17 -0
- dejima/client.py +702 -0
- dejima_sdk-0.5.2.dist-info/METADATA +120 -0
- dejima_sdk-0.5.2.dist-info/RECORD +5 -0
- dejima_sdk-0.5.2.dist-info/WHEEL +4 -0
dejima/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Dejima — Python client for the Dejima API.
|
|
2
|
+
|
|
3
|
+
from dejima import Client
|
|
4
|
+
dj = Client() # reads DEJIMA_HOST / DEJIMA_TOKEN
|
|
5
|
+
isl = dj.create_island(repo="git@github.com:you/foo.git", agent="claude-code")
|
|
6
|
+
print(dj.list_islands())
|
|
7
|
+
|
|
8
|
+
See https://aoos.github.io/dejima/api.html for the full API. Dejima is alpha
|
|
9
|
+
(0.x): fields may change until 1.0. This client is hand-written over the REST
|
|
10
|
+
surface; the WebSocket PTY session (``Client.attach`` → ``Session``) needs the
|
|
11
|
+
``ws`` extra: ``pip install 'dejima[ws]'``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .client import Client, DejimaError, Session
|
|
15
|
+
|
|
16
|
+
__all__ = ["Client", "DejimaError", "Session", "__version__"]
|
|
17
|
+
__version__ = "0.5.2"
|
dejima/client.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"""Thin client over the Dejima HTTP/WebSocket API.
|
|
2
|
+
|
|
3
|
+
Design note: this is a small hand-written client for the v0.x API. The request/
|
|
4
|
+
response layer mirrors the route table in ``internal/api/server.go`` (the source
|
|
5
|
+
of truth) and the hand-maintained ``openapi.yaml`` at the repo root. Once the API
|
|
6
|
+
stabilizes (v1.0), the request/response layer is intended to be generated from
|
|
7
|
+
the spec, leaving only the ergonomic helper — the :class:`Session` PTY stream —
|
|
8
|
+
hand-written.
|
|
9
|
+
|
|
10
|
+
The PTY session is *not* a raw byte stream: the daemon speaks a small JSON
|
|
11
|
+
envelope protocol (``{"type": "data", "b64": ...}`` / ``{"type": "resize", ...}``)
|
|
12
|
+
over the WebSocket, base64-encoding the terminal bytes. :class:`Session` hides
|
|
13
|
+
that; see :meth:`Client.attach`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
22
|
+
from urllib.parse import quote
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
|
|
26
|
+
DEFAULT_HOST = "127.0.0.1:7273"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DejimaError(RuntimeError):
|
|
30
|
+
"""Raised when the daemon returns a non-2xx response.
|
|
31
|
+
|
|
32
|
+
The daemon's error body is JSON (``{"error": "..."}``); ``message`` is the
|
|
33
|
+
extracted text when present, else the raw body.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, status: int, message: str):
|
|
37
|
+
super().__init__(f"HTTP {status}: {message}")
|
|
38
|
+
self.status = status
|
|
39
|
+
self.message = message
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _clean(d: Mapping[str, Any]) -> Dict[str, Any]:
|
|
43
|
+
"""Drop None-valued keys so optional fields are simply omitted from the body."""
|
|
44
|
+
return {k: v for k, v in d.items() if v is not None}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Client:
|
|
48
|
+
"""A client for one Dejima daemon.
|
|
49
|
+
|
|
50
|
+
host: ``HOST:PORT`` of the daemon (default: ``$DEJIMA_HOST`` or 127.0.0.1:7273).
|
|
51
|
+
token: per-island bearer token for the autonomy path (default: ``$DEJIMA_TOKEN``).
|
|
52
|
+
Operator access over the unix socket / tailnet needs no token.
|
|
53
|
+
scheme: ``http`` (default) or ``https``.
|
|
54
|
+
timeout: per-request timeout in seconds.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
host: Optional[str] = None,
|
|
60
|
+
token: Optional[str] = None,
|
|
61
|
+
*,
|
|
62
|
+
scheme: str = "http",
|
|
63
|
+
timeout: float = 30.0,
|
|
64
|
+
):
|
|
65
|
+
self.host = host or os.environ.get("DEJIMA_HOST", DEFAULT_HOST)
|
|
66
|
+
self.token = token or os.environ.get("DEJIMA_TOKEN")
|
|
67
|
+
self.scheme = scheme
|
|
68
|
+
self.base = f"{scheme}://{self.host}"
|
|
69
|
+
self.timeout = timeout
|
|
70
|
+
self._s = requests.Session()
|
|
71
|
+
if self.token:
|
|
72
|
+
self._s.headers["Authorization"] = f"Bearer {self.token}"
|
|
73
|
+
|
|
74
|
+
# ----- low-level ------------------------------------------------------
|
|
75
|
+
def _req(self, method: str, path: str, **kw) -> requests.Response:
|
|
76
|
+
kw.setdefault("timeout", self.timeout)
|
|
77
|
+
r = self._s.request(method, self.base + path, **kw)
|
|
78
|
+
if not r.ok:
|
|
79
|
+
raise DejimaError(r.status_code, _error_message(r))
|
|
80
|
+
return r
|
|
81
|
+
|
|
82
|
+
def _json(self, method: str, path: str, **kw) -> Any:
|
|
83
|
+
r = self._req(method, path, **kw)
|
|
84
|
+
return r.json() if r.content else None
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _seg(s: str) -> str:
|
|
88
|
+
"""Percent-encode a single path segment (a name/id), escaping slashes."""
|
|
89
|
+
return quote(s, safe="")
|
|
90
|
+
|
|
91
|
+
def _island(self, name: str) -> str:
|
|
92
|
+
return f"/v1/islands/{self._seg(name)}"
|
|
93
|
+
|
|
94
|
+
# ===== islands ========================================================
|
|
95
|
+
def list_islands(self) -> List[Dict[str, Any]]:
|
|
96
|
+
"""All islands, most-recently-used first."""
|
|
97
|
+
return self._json("GET", "/v1/islands")
|
|
98
|
+
|
|
99
|
+
def create_island(
|
|
100
|
+
self,
|
|
101
|
+
repo: str,
|
|
102
|
+
*,
|
|
103
|
+
agent: Optional[str] = None,
|
|
104
|
+
name: Optional[str] = None,
|
|
105
|
+
image: Optional[str] = None,
|
|
106
|
+
cmd: Optional[str] = None,
|
|
107
|
+
resources: Optional[Dict[str, Any]] = None,
|
|
108
|
+
agents: Optional[List[Dict[str, Any]]] = None,
|
|
109
|
+
role: Optional[str] = None,
|
|
110
|
+
github_identity: Optional[str] = None,
|
|
111
|
+
seed_path: Optional[str] = None,
|
|
112
|
+
owner: Optional[str] = None,
|
|
113
|
+
tags: Optional[Dict[str, str]] = None,
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
|
+
"""Provision an island.
|
|
116
|
+
|
|
117
|
+
``repo`` is a git URL or local path. ``cmd`` is required when
|
|
118
|
+
``agent="headless"``. ``agents`` seeds a multi-agent island (element 0 is
|
|
119
|
+
the primary). On a token-authenticated (Home-Island) create the response
|
|
120
|
+
carries the child island's ``token``.
|
|
121
|
+
"""
|
|
122
|
+
body = _clean(
|
|
123
|
+
dict(
|
|
124
|
+
repo=repo,
|
|
125
|
+
agent=agent,
|
|
126
|
+
name=name,
|
|
127
|
+
image=image,
|
|
128
|
+
cmd=cmd,
|
|
129
|
+
resources=resources,
|
|
130
|
+
agents=agents,
|
|
131
|
+
role=role,
|
|
132
|
+
github_identity=github_identity,
|
|
133
|
+
seed_path=seed_path,
|
|
134
|
+
owner=owner,
|
|
135
|
+
tags=tags,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return self._json("POST", "/v1/islands", json=body)
|
|
139
|
+
|
|
140
|
+
def get_island(self, name: str) -> Dict[str, Any]:
|
|
141
|
+
"""Island detail (agents, presence, cpu/mem, git, health, resources, disk)."""
|
|
142
|
+
return self._json("GET", self._island(name))
|
|
143
|
+
|
|
144
|
+
def update_island(self, name: str, *, title: str) -> Dict[str, Any]:
|
|
145
|
+
"""Update the island's cosmetic display title (``""`` clears it)."""
|
|
146
|
+
return self._json("PATCH", self._island(name), json={"title": title})
|
|
147
|
+
|
|
148
|
+
def delete_island(self, name: str, *, force: bool = False) -> None:
|
|
149
|
+
"""Purge an island (container + volumes). ``force`` bypasses the
|
|
150
|
+
unpushed-work guard (which returns a 409 otherwise)."""
|
|
151
|
+
self._req("DELETE", self._island(name), params={"force": "true"} if force else None)
|
|
152
|
+
|
|
153
|
+
def hibernate(self, name: str) -> Dict[str, Any]:
|
|
154
|
+
"""Stop the container, preserving volumes."""
|
|
155
|
+
return self._json("POST", f"{self._island(name)}/hibernate")
|
|
156
|
+
|
|
157
|
+
def wake(self, name: str) -> Dict[str, Any]:
|
|
158
|
+
"""Start a hibernated island's container against existing volumes."""
|
|
159
|
+
return self._json("POST", f"{self._island(name)}/wake")
|
|
160
|
+
|
|
161
|
+
def reset(self, name: str) -> Dict[str, Any]:
|
|
162
|
+
"""Clear agent on-disk state (preserves the workspace)."""
|
|
163
|
+
return self._json("POST", f"{self._island(name)}/reset")
|
|
164
|
+
|
|
165
|
+
def upgrade(self, name: str) -> Dict[str, Any]:
|
|
166
|
+
"""Recreate the container against the current island image (both volumes
|
|
167
|
+
preserved); also re-assembles bind mounts for older islands."""
|
|
168
|
+
return self._json("POST", f"{self._island(name)}/upgrade")
|
|
169
|
+
|
|
170
|
+
def clone_island(self, name: str, new_name: str) -> Dict[str, Any]:
|
|
171
|
+
"""Copy an island (workspace + home volumes; Port grants are dropped)."""
|
|
172
|
+
return self._json("POST", f"{self._island(name)}/clone", json={"new_name": new_name})
|
|
173
|
+
|
|
174
|
+
def set_resources(
|
|
175
|
+
self, name: str, *, memory: Optional[str] = None, oom_priority: Optional[int] = None
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""Update memory limit (live) and/or OOM priority (needs a recreate; the
|
|
178
|
+
response flags ``restart_required``). ``memory=""`` means unlimited."""
|
|
179
|
+
body: Dict[str, Any] = {}
|
|
180
|
+
if memory is not None:
|
|
181
|
+
body["memory"] = memory
|
|
182
|
+
if oom_priority is not None:
|
|
183
|
+
body["oom_priority"] = oom_priority
|
|
184
|
+
return self._json("PUT", f"{self._island(name)}/resources", json=body)
|
|
185
|
+
|
|
186
|
+
def workspace_ready(self, name: str) -> bool:
|
|
187
|
+
"""Whether the island's repo clone has landed in /workspace yet."""
|
|
188
|
+
r = self._json("GET", f"{self._island(name)}/workspace-ready")
|
|
189
|
+
return bool(r and r.get("ready"))
|
|
190
|
+
|
|
191
|
+
def island_events(self, name: str) -> List[Dict[str, Any]]:
|
|
192
|
+
"""The island's recent event log (newest first)."""
|
|
193
|
+
return self._json("GET", f"{self._island(name)}/events")
|
|
194
|
+
|
|
195
|
+
# ===== agents =========================================================
|
|
196
|
+
def list_agents(self, name: str) -> List[Dict[str, Any]]:
|
|
197
|
+
return self._json("GET", f"{self._island(name)}/agents")
|
|
198
|
+
|
|
199
|
+
def add_agent(
|
|
200
|
+
self,
|
|
201
|
+
name: str,
|
|
202
|
+
*,
|
|
203
|
+
type: Optional[str] = None,
|
|
204
|
+
label: Optional[str] = None,
|
|
205
|
+
cmd: Optional[str] = None,
|
|
206
|
+
provider: Optional[str] = None,
|
|
207
|
+
model: Optional[str] = None,
|
|
208
|
+
) -> Dict[str, Any]:
|
|
209
|
+
"""Add an agent (its own worktree + tmux session). ``cmd`` is required for
|
|
210
|
+
a generic headless type."""
|
|
211
|
+
body = _clean(dict(type=type, label=label, cmd=cmd, provider=provider, model=model))
|
|
212
|
+
return self._json("POST", f"{self._island(name)}/agents", json=body)
|
|
213
|
+
|
|
214
|
+
def get_agent(self, name: str, agent_id: str) -> Dict[str, Any]:
|
|
215
|
+
return self._json("GET", f"{self._island(name)}/agents/{self._seg(agent_id)}")
|
|
216
|
+
|
|
217
|
+
def update_agent(self, name: str, agent_id: str, *, label: str) -> Dict[str, Any]:
|
|
218
|
+
"""Rename an agent's cosmetic label (``""`` clears it)."""
|
|
219
|
+
return self._json(
|
|
220
|
+
"PATCH", f"{self._island(name)}/agents/{self._seg(agent_id)}", json={"label": label}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def remove_agent(self, name: str, agent_id: str) -> None:
|
|
224
|
+
"""Remove a non-primary agent (kills its session, prunes its worktree;
|
|
225
|
+
the branch is kept). The primary and last agent cannot be removed."""
|
|
226
|
+
self._req("DELETE", f"{self._island(name)}/agents/{self._seg(agent_id)}")
|
|
227
|
+
|
|
228
|
+
def configure_agent(
|
|
229
|
+
self,
|
|
230
|
+
name: str,
|
|
231
|
+
agent_id: str,
|
|
232
|
+
*,
|
|
233
|
+
provider: Optional[str] = None,
|
|
234
|
+
model: Optional[str] = None,
|
|
235
|
+
) -> Dict[str, Any]:
|
|
236
|
+
"""Set an agent's LLM provider/model (only for key-requiring frameworks).
|
|
237
|
+
A change flags ``restart_required`` (selection rides a create-time env)."""
|
|
238
|
+
body: Dict[str, Any] = {}
|
|
239
|
+
if provider is not None:
|
|
240
|
+
body["provider"] = provider
|
|
241
|
+
if model is not None:
|
|
242
|
+
body["model"] = model
|
|
243
|
+
return self._json(
|
|
244
|
+
"PATCH", f"{self._island(name)}/agents/{self._seg(agent_id)}/config", json=body
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# ===== exec / files / logs -------------------------------------------
|
|
248
|
+
def exec(self, name: str, cmd: List[str]) -> Dict[str, Any]:
|
|
249
|
+
"""Run a one-shot command. Returns ``{stdout, stderr, exit_code}``."""
|
|
250
|
+
return self._json("POST", f"{self._island(name)}/exec", json={"cmd": cmd})
|
|
251
|
+
|
|
252
|
+
def read_file(self, name: str, path: str) -> bytes:
|
|
253
|
+
"""Read a file from the island (defaults to the primary worktree)."""
|
|
254
|
+
return self._req("GET", f"{self._island(name)}/files/{quote(path)}").content
|
|
255
|
+
|
|
256
|
+
def write_file(self, name: str, path: str, data: bytes) -> None:
|
|
257
|
+
"""Write a file into the island."""
|
|
258
|
+
self._req("PUT", f"{self._island(name)}/files/{quote(path)}", data=data)
|
|
259
|
+
|
|
260
|
+
def logs(self, name: str, *, agent: Optional[str] = None) -> str:
|
|
261
|
+
"""Fetch container/agent logs as text."""
|
|
262
|
+
return self._req("GET", f"{self._island(name)}/logs", params=_clean({"agent": agent})).text
|
|
263
|
+
|
|
264
|
+
# ===== port broker ----------------------------------------------------
|
|
265
|
+
def list_port_scopes(self, name: str) -> Dict[str, Any]:
|
|
266
|
+
"""List the island's brokered host-folder grants."""
|
|
267
|
+
return self._json("GET", f"{self._island(name)}/port/scopes")
|
|
268
|
+
|
|
269
|
+
def grant_port_scope(self, name: str, host_path: str, *, mode: str = "ro") -> Dict[str, Any]:
|
|
270
|
+
"""Grant the island scoped access to a host directory (``ro``/``rw``)."""
|
|
271
|
+
return self._json(
|
|
272
|
+
"POST", f"{self._island(name)}/port/scopes", json={"host_path": host_path, "mode": mode}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def revoke_port_scope(self, name: str, scope: str) -> None:
|
|
276
|
+
"""Drop a Port grant by its scope name."""
|
|
277
|
+
self._req("DELETE", f"{self._island(name)}/port/scopes/{self._seg(scope)}")
|
|
278
|
+
|
|
279
|
+
def port_intake(
|
|
280
|
+
self, name: str, scope: str, src_rel: str, *, dest: Optional[str] = None
|
|
281
|
+
) -> Dict[str, Any]:
|
|
282
|
+
"""Brokered read-only import of a host file (within a scope) into the island."""
|
|
283
|
+
body = _clean(dict(scope=scope, src_rel=src_rel, dest=dest))
|
|
284
|
+
return self._json("POST", f"{self._island(name)}/port/intake", json=body)
|
|
285
|
+
|
|
286
|
+
def port_export(self, name: str, src: str) -> Dict[str, Any]:
|
|
287
|
+
"""Brokered export of an island file out to the host-owned export staging area."""
|
|
288
|
+
return self._json("POST", f"{self._island(name)}/port/export", json={"src": src})
|
|
289
|
+
|
|
290
|
+
def port_write(self, name: str, scope: str, src: str, dest_rel: str) -> Dict[str, Any]:
|
|
291
|
+
"""Brokered write of an island file into a read-write Port scope on the host."""
|
|
292
|
+
return self._json(
|
|
293
|
+
"POST",
|
|
294
|
+
f"{self._island(name)}/port/write",
|
|
295
|
+
json={"scope": scope, "src": src, "dest_rel": dest_rel},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# ===== capability broker ---------------------------------------------
|
|
299
|
+
def list_capability_grants(self, name: str) -> Dict[str, Any]:
|
|
300
|
+
"""List the host capability targets an island may invoke."""
|
|
301
|
+
return self._json("GET", f"{self._island(name)}/capability/grants")
|
|
302
|
+
|
|
303
|
+
def grant_capability(self, name: str, target: str) -> Dict[str, Any]:
|
|
304
|
+
"""Grant the island permission to invoke a named host capability target."""
|
|
305
|
+
return self._json(
|
|
306
|
+
"POST", f"{self._island(name)}/capability/grants", json={"target": target}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def revoke_capability(self, name: str, target: str) -> None:
|
|
310
|
+
"""Drop a capability grant by target name."""
|
|
311
|
+
self._req("DELETE", f"{self._island(name)}/capability/grants/{self._seg(target)}")
|
|
312
|
+
|
|
313
|
+
def execute_capability(
|
|
314
|
+
self,
|
|
315
|
+
target: str,
|
|
316
|
+
*,
|
|
317
|
+
island: Optional[str] = None,
|
|
318
|
+
args: Optional[Dict[str, str]] = None,
|
|
319
|
+
) -> Dict[str, Any]:
|
|
320
|
+
"""Invoke a granted capability. Operators name ``island``; a token-
|
|
321
|
+
authenticated in-island caller is pinned to its own island."""
|
|
322
|
+
body = _clean(dict(target=target, island=island, args=args))
|
|
323
|
+
return self._json("POST", "/v1/capabilities/execute", json=body)
|
|
324
|
+
|
|
325
|
+
# ===== MCP broker -----------------------------------------------------
|
|
326
|
+
def list_mcp_grants(self, name: str) -> Dict[str, Any]:
|
|
327
|
+
"""List the MCP servers an island may call."""
|
|
328
|
+
return self._json("GET", f"{self._island(name)}/mcp/grants")
|
|
329
|
+
|
|
330
|
+
def grant_mcp(self, name: str, server: str) -> Dict[str, Any]:
|
|
331
|
+
"""Grant an island access to a registered MCP server."""
|
|
332
|
+
return self._json("POST", f"{self._island(name)}/mcp/grants", json={"server": server})
|
|
333
|
+
|
|
334
|
+
def revoke_mcp(self, name: str, server: str) -> None:
|
|
335
|
+
"""Revoke an island's MCP-server grant."""
|
|
336
|
+
self._req("DELETE", f"{self._island(name)}/mcp/grants/{self._seg(server)}")
|
|
337
|
+
|
|
338
|
+
def mcp_call(
|
|
339
|
+
self,
|
|
340
|
+
server: str,
|
|
341
|
+
method: str,
|
|
342
|
+
*,
|
|
343
|
+
island: Optional[str] = None,
|
|
344
|
+
params: Optional[Any] = None,
|
|
345
|
+
) -> Dict[str, Any]:
|
|
346
|
+
"""Invoke a JSON-RPC ``method`` on a granted MCP server (deny-all; every
|
|
347
|
+
call is ledgered). Operators name ``island``; a token-authenticated
|
|
348
|
+
in-island caller is pinned to its own island."""
|
|
349
|
+
body = _clean(dict(server=server, method=method, island=island, params=params))
|
|
350
|
+
return self._json("POST", "/v1/mcp/call", json=body)
|
|
351
|
+
|
|
352
|
+
# ===== credentials ----------------------------------------------------
|
|
353
|
+
def push_claude_credentials(self, credentials_json: str) -> None:
|
|
354
|
+
"""Store a Claude Code credentials blob as the seed new islands clone from."""
|
|
355
|
+
self._req("PUT", "/v1/credentials/claude", json={"credentials_json": credentials_json})
|
|
356
|
+
|
|
357
|
+
def claude_credentials_status(self) -> Dict[str, Any]:
|
|
358
|
+
"""Whether new islands get Claude credentials, and from where (no secret)."""
|
|
359
|
+
return self._json("GET", "/v1/credentials/claude")
|
|
360
|
+
|
|
361
|
+
def list_github_identities(self) -> Dict[str, Any]:
|
|
362
|
+
"""List the daemon's GitHub identities (no tokens)."""
|
|
363
|
+
return self._json("GET", "/v1/credentials/github")
|
|
364
|
+
|
|
365
|
+
def put_github_identity(
|
|
366
|
+
self,
|
|
367
|
+
name: str,
|
|
368
|
+
*,
|
|
369
|
+
login: str,
|
|
370
|
+
token: str,
|
|
371
|
+
id: Optional[int] = None,
|
|
372
|
+
host: Optional[str] = None,
|
|
373
|
+
default: bool = False,
|
|
374
|
+
) -> Dict[str, Any]:
|
|
375
|
+
"""Add or update a named GitHub identity."""
|
|
376
|
+
body = _clean(dict(login=login, token=token, id=id, host=host, default=default or None))
|
|
377
|
+
return self._json("PUT", f"/v1/credentials/github/{self._seg(name)}", json=body)
|
|
378
|
+
|
|
379
|
+
def delete_github_identity(self, name: str) -> Dict[str, Any]:
|
|
380
|
+
"""Remove a GitHub identity; reports islands that still referenced it."""
|
|
381
|
+
return self._json("DELETE", f"/v1/credentials/github/{self._seg(name)}")
|
|
382
|
+
|
|
383
|
+
def github_repos(self, name: str) -> Dict[str, Any]:
|
|
384
|
+
"""List the repositories a GitHub identity can access (fetched daemon-side)."""
|
|
385
|
+
return self._json("GET", f"/v1/credentials/github/{self._seg(name)}/repos")
|
|
386
|
+
|
|
387
|
+
def list_providers(self) -> Dict[str, Any]:
|
|
388
|
+
"""List the daemon's LLM provider credentials (masked hint only, no keys)."""
|
|
389
|
+
return self._json("GET", "/v1/credentials/providers")
|
|
390
|
+
|
|
391
|
+
def put_provider(
|
|
392
|
+
self,
|
|
393
|
+
provider: str,
|
|
394
|
+
*,
|
|
395
|
+
api_key: str,
|
|
396
|
+
base_url: Optional[str] = None,
|
|
397
|
+
env_var: Optional[str] = None,
|
|
398
|
+
default: bool = False,
|
|
399
|
+
) -> Dict[str, Any]:
|
|
400
|
+
"""Store or update a provider key (write-only; never echoed back)."""
|
|
401
|
+
body = _clean(
|
|
402
|
+
dict(api_key=api_key, base_url=base_url, env_var=env_var, default=default or None)
|
|
403
|
+
)
|
|
404
|
+
return self._json("PUT", f"/v1/credentials/providers/{self._seg(provider)}", json=body)
|
|
405
|
+
|
|
406
|
+
def delete_provider(self, provider: str) -> Dict[str, Any]:
|
|
407
|
+
"""Remove a provider credential; reports islands that still reference it."""
|
|
408
|
+
return self._json("DELETE", f"/v1/credentials/providers/{self._seg(provider)}")
|
|
409
|
+
|
|
410
|
+
# ===== operator tokens (owner-only) ----------------------------------
|
|
411
|
+
def list_tokens(self) -> Dict[str, Any]:
|
|
412
|
+
"""List issued operator tokens (metadata only; never secrets)."""
|
|
413
|
+
return self._json("GET", "/v1/tokens")
|
|
414
|
+
|
|
415
|
+
def create_token(
|
|
416
|
+
self, role: str, *, label: Optional[str] = None, islands: Optional[List[str]] = None
|
|
417
|
+
) -> Dict[str, Any]:
|
|
418
|
+
"""Mint an operator bearer token. ``role`` is ``owner``/``operator``/
|
|
419
|
+
``viewer``; ``islands`` scopes it (empty = all). The response carries the
|
|
420
|
+
bearer ``secret`` — returned ONCE and never stored in the clear."""
|
|
421
|
+
body = _clean(dict(role=role, label=label, islands=islands))
|
|
422
|
+
return self._json("POST", "/v1/tokens", json=body)
|
|
423
|
+
|
|
424
|
+
def revoke_token(self, token_id: str) -> None:
|
|
425
|
+
"""Revoke an operator token by id."""
|
|
426
|
+
self._req("DELETE", f"/v1/tokens/{self._seg(token_id)}")
|
|
427
|
+
|
|
428
|
+
# ===== events / webhooks ---------------------------------------------
|
|
429
|
+
def list_subscriptions(self) -> List[Dict[str, Any]]:
|
|
430
|
+
"""List webhook subscriptions."""
|
|
431
|
+
return self._json("GET", "/v1/events/subscriptions")
|
|
432
|
+
|
|
433
|
+
def subscribe_webhook(
|
|
434
|
+
self, url: str, *, secret: Optional[str] = None, events: Optional[List[str]] = None
|
|
435
|
+
) -> Dict[str, Any]:
|
|
436
|
+
"""Subscribe a webhook URL to state-change events (empty ``events`` = all)."""
|
|
437
|
+
body = _clean(dict(url=url, secret=secret, events=events))
|
|
438
|
+
return self._json("POST", "/v1/events/subscribe", json=body)
|
|
439
|
+
|
|
440
|
+
def unsubscribe_webhook(self, subscription_id: str) -> None:
|
|
441
|
+
"""Remove a webhook subscription by id."""
|
|
442
|
+
self._req("DELETE", f"/v1/events/subscriptions/{self._seg(subscription_id)}")
|
|
443
|
+
|
|
444
|
+
# ===== daemon ---------------------------------------------------------
|
|
445
|
+
def overview(self) -> Dict[str, Any]:
|
|
446
|
+
"""Daemon health, VM memory, fleet rollup."""
|
|
447
|
+
return self._json("GET", "/v1/overview")
|
|
448
|
+
|
|
449
|
+
def agent_types(self) -> Dict[str, Any]:
|
|
450
|
+
"""The built-in agent-type capability descriptors (provider/model hints)."""
|
|
451
|
+
return self._json("GET", "/v1/agent-types")
|
|
452
|
+
|
|
453
|
+
def healthz(self) -> bool:
|
|
454
|
+
"""Reachability probe; True when the daemon answers."""
|
|
455
|
+
try:
|
|
456
|
+
self._req("GET", "/v1/healthz")
|
|
457
|
+
return True
|
|
458
|
+
except DejimaError:
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
def audit(
|
|
462
|
+
self,
|
|
463
|
+
*,
|
|
464
|
+
limit: Optional[int] = None,
|
|
465
|
+
since: Optional[str] = None,
|
|
466
|
+
until: Optional[str] = None,
|
|
467
|
+
island: Optional[str] = None,
|
|
468
|
+
type_prefix: Optional[str] = None,
|
|
469
|
+
actor: Optional[str] = None,
|
|
470
|
+
decision: Optional[str] = None,
|
|
471
|
+
format: Optional[str] = None,
|
|
472
|
+
) -> Any:
|
|
473
|
+
"""The audit ledger (brokered ops + ``api.request`` + lifecycle) plus a
|
|
474
|
+
hash-chain verification verdict.
|
|
475
|
+
|
|
476
|
+
Filters compose; ``limit`` tails the filtered result. ``since``/``until``
|
|
477
|
+
are RFC3339; ``type_prefix`` matches the entry type prefix (e.g. ``port.``);
|
|
478
|
+
``decision`` is ``allowed``/``denied``. With ``format="jsonl"`` or
|
|
479
|
+
``"csv"`` the daemon streams a plain-text export — this returns that text
|
|
480
|
+
instead of the parsed JSON envelope.
|
|
481
|
+
"""
|
|
482
|
+
params = _clean(
|
|
483
|
+
{
|
|
484
|
+
"limit": limit,
|
|
485
|
+
"since": since,
|
|
486
|
+
"until": until,
|
|
487
|
+
"island": island,
|
|
488
|
+
"type": type_prefix,
|
|
489
|
+
"actor": actor,
|
|
490
|
+
"decision": decision,
|
|
491
|
+
"format": format,
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
if format in ("jsonl", "csv"):
|
|
495
|
+
return self._req("GET", "/v1/audit", params=params).text
|
|
496
|
+
return self._json("GET", "/v1/audit", params=params)
|
|
497
|
+
|
|
498
|
+
def activity(
|
|
499
|
+
self,
|
|
500
|
+
*,
|
|
501
|
+
actor: Optional[str] = None,
|
|
502
|
+
island: Optional[str] = None,
|
|
503
|
+
owner: Optional[str] = None,
|
|
504
|
+
kind: Optional[str] = None,
|
|
505
|
+
decision: Optional[str] = None,
|
|
506
|
+
since: Optional[str] = None,
|
|
507
|
+
until: Optional[str] = None,
|
|
508
|
+
limit: Optional[int] = None,
|
|
509
|
+
) -> Dict[str, Any]:
|
|
510
|
+
"""The team activity feed — a curated, human-rendered timeline projected
|
|
511
|
+
over the audit ledger (who launched what, which agent did what), newest
|
|
512
|
+
first. ``kind`` is ``lifecycle``/``broker``/``system``; ``decision`` is
|
|
513
|
+
``allowed``/``denied``; ``since``/``until`` are RFC3339."""
|
|
514
|
+
params = _clean(
|
|
515
|
+
{
|
|
516
|
+
"actor": actor,
|
|
517
|
+
"island": island,
|
|
518
|
+
"owner": owner,
|
|
519
|
+
"kind": kind,
|
|
520
|
+
"decision": decision,
|
|
521
|
+
"since": since,
|
|
522
|
+
"until": until,
|
|
523
|
+
"limit": limit,
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
return self._json("GET", "/v1/activity", params=params)
|
|
527
|
+
|
|
528
|
+
def client_history(self) -> List[Dict[str, Any]]:
|
|
529
|
+
"""Recent attach/detach events across every island (newest first)."""
|
|
530
|
+
return self._json("GET", "/v1/clients")
|
|
531
|
+
|
|
532
|
+
def revoke_sessions(self) -> Dict[str, Any]:
|
|
533
|
+
"""Drop every active websocket client across every island."""
|
|
534
|
+
return self._json("POST", "/v1/sessions/revoke")
|
|
535
|
+
|
|
536
|
+
def panic_status(self) -> Dict[str, Any]:
|
|
537
|
+
"""Whether the daemon is in PANIC (all islands stopped, no auto-start)."""
|
|
538
|
+
return self._json("GET", "/v1/panic")
|
|
539
|
+
|
|
540
|
+
def panic(self, *, reason: Optional[str] = None) -> Dict[str, Any]:
|
|
541
|
+
"""Engage PANIC: stop every island."""
|
|
542
|
+
return self._json("POST", "/v1/panic", json=_clean({"reason": reason}))
|
|
543
|
+
|
|
544
|
+
def unpanic(self) -> Dict[str, Any]:
|
|
545
|
+
"""Clear PANIC and restart islands that should be running."""
|
|
546
|
+
return self._json("DELETE", "/v1/panic")
|
|
547
|
+
|
|
548
|
+
def admin_update(self, *, execute: bool = False, force: bool = False) -> Dict[str, Any]:
|
|
549
|
+
"""Report (or, with ``execute``, apply) a daemon self-update. With clients
|
|
550
|
+
attached an apply defers unless ``force`` is set."""
|
|
551
|
+
return self._json("POST", "/v1/admin/update", json={"execute": execute, "force": force})
|
|
552
|
+
|
|
553
|
+
def image_build(self) -> str:
|
|
554
|
+
"""Rebuild the island image; returns the combined build output as text. A
|
|
555
|
+
failure is reported as a trailing ``ERROR:`` line in the stream."""
|
|
556
|
+
return self._req("POST", "/v1/image/build").text
|
|
557
|
+
|
|
558
|
+
def authorize_ssh_key(self, public_key: str) -> Dict[str, Any]:
|
|
559
|
+
"""Authorize an OpenSSH public key fleet-wide; returns its fingerprint."""
|
|
560
|
+
return self._json("POST", "/v1/ssh/account-keys", json={"public_key": public_key})
|
|
561
|
+
|
|
562
|
+
def list_ssh_keys(self) -> Dict[str, Any]:
|
|
563
|
+
"""List the fleet-wide authorized SSH keys."""
|
|
564
|
+
return self._json("GET", "/v1/ssh/account-keys")
|
|
565
|
+
|
|
566
|
+
# ===== host terminals (operator-only; requires --host-terminals) -----
|
|
567
|
+
def list_terminals(self) -> Dict[str, Any]:
|
|
568
|
+
"""List host terminals (uncontained operator shells on the daemon host)."""
|
|
569
|
+
return self._json("GET", "/v1/terminals")
|
|
570
|
+
|
|
571
|
+
def create_terminal(self, *, label: Optional[str] = None) -> Dict[str, Any]:
|
|
572
|
+
"""Create a host terminal."""
|
|
573
|
+
return self._json("POST", "/v1/terminals", json=_clean({"label": label}))
|
|
574
|
+
|
|
575
|
+
def delete_terminal(self, terminal_id: str) -> None:
|
|
576
|
+
"""Remove a host terminal."""
|
|
577
|
+
self._req("DELETE", f"/v1/terminals/{self._seg(terminal_id)}")
|
|
578
|
+
|
|
579
|
+
def relabel_terminal(self, terminal_id: str, *, label: str) -> Dict[str, Any]:
|
|
580
|
+
"""Rename a host terminal."""
|
|
581
|
+
return self._json(
|
|
582
|
+
"PATCH", f"/v1/terminals/{self._seg(terminal_id)}", json={"label": label}
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# ===== session (WebSocket PTY) ---------------------------------------
|
|
586
|
+
def session_url(self, name: str, *, agent: Optional[str] = None) -> str:
|
|
587
|
+
"""The WebSocket URL for an interactive PTY session (multi-attach)."""
|
|
588
|
+
base = self._ws_base()
|
|
589
|
+
if agent:
|
|
590
|
+
return f"{base}{self._island(name)}/agents/{self._seg(agent)}/session"
|
|
591
|
+
return f"{base}{self._island(name)}/session"
|
|
592
|
+
|
|
593
|
+
def terminal_session_url(self, terminal_id: str, *, label: Optional[str] = None) -> str:
|
|
594
|
+
"""The WebSocket URL for a host terminal's PTY session."""
|
|
595
|
+
url = f"{self._ws_base()}/v1/terminals/{self._seg(terminal_id)}/session"
|
|
596
|
+
if label:
|
|
597
|
+
url += f"?label={quote(label)}"
|
|
598
|
+
return url
|
|
599
|
+
|
|
600
|
+
def _ws_base(self) -> str:
|
|
601
|
+
ws = "wss" if self.scheme == "https" else "ws"
|
|
602
|
+
return f"{ws}://{self.host}"
|
|
603
|
+
|
|
604
|
+
def attach(self, name: str, *, agent: Optional[str] = None, label: Optional[str] = None) -> "Session":
|
|
605
|
+
"""Open an island's PTY session stream. Requires the ``ws`` extra
|
|
606
|
+
(``pip install 'dejima[ws]'``). Returns a :class:`Session`."""
|
|
607
|
+
return self._open_session(self.session_url(name, agent=agent), label=label)
|
|
608
|
+
|
|
609
|
+
def attach_terminal(self, terminal_id: str, *, label: Optional[str] = None) -> "Session":
|
|
610
|
+
"""Open a host terminal's PTY session stream."""
|
|
611
|
+
return self._open_session(self.terminal_session_url(terminal_id, label=label), label=label)
|
|
612
|
+
|
|
613
|
+
def _open_session(self, url: str, *, label: Optional[str]) -> "Session":
|
|
614
|
+
try:
|
|
615
|
+
import websocket # type: ignore
|
|
616
|
+
except ImportError as e: # pragma: no cover
|
|
617
|
+
raise DejimaError(0, "attach() needs the 'ws' extra: pip install 'dejima[ws]'") from e
|
|
618
|
+
header = [f"Authorization: Bearer {self.token}"] if self.token else None
|
|
619
|
+
ws = websocket.create_connection(url, header=header)
|
|
620
|
+
return Session(ws)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _error_message(r: requests.Response) -> str:
|
|
624
|
+
"""Extract the daemon's ``{"error": ...}`` text, falling back to body/reason."""
|
|
625
|
+
body = (r.text or "").strip()
|
|
626
|
+
try:
|
|
627
|
+
data = r.json()
|
|
628
|
+
if isinstance(data, dict) and isinstance(data.get("error"), str):
|
|
629
|
+
return data["error"]
|
|
630
|
+
except ValueError:
|
|
631
|
+
pass
|
|
632
|
+
return body or r.reason
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class Session:
|
|
636
|
+
"""An attached PTY session over the daemon's JSON-envelope WebSocket protocol.
|
|
637
|
+
|
|
638
|
+
The wire is line-delimited JSON envelopes, terminal bytes base64-encoded:
|
|
639
|
+
|
|
640
|
+
- server → client: ``{"type": "hello", "attached": [...]}`` once on connect,
|
|
641
|
+
then ``{"type": "data", "b64": ...}`` and ``{"type": "error", "b64": ...}``.
|
|
642
|
+
- client → server: ``{"type": "data", "b64": ...}`` and
|
|
643
|
+
``{"type": "resize", "rows": R, "cols": C}``.
|
|
644
|
+
|
|
645
|
+
:meth:`send` and :meth:`recv` deal in raw ``bytes``; the base64/envelope
|
|
646
|
+
framing is handled for you. Usable as a context manager.
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
def __init__(self, ws: Any):
|
|
650
|
+
self._ws = ws
|
|
651
|
+
|
|
652
|
+
# context-manager sugar
|
|
653
|
+
def __enter__(self) -> "Session":
|
|
654
|
+
return self
|
|
655
|
+
|
|
656
|
+
def __exit__(self, *exc: Any) -> None:
|
|
657
|
+
self.close()
|
|
658
|
+
|
|
659
|
+
def send(self, data: bytes) -> None:
|
|
660
|
+
"""Send raw bytes to the PTY (keystrokes)."""
|
|
661
|
+
self._send_envelope({"type": "data", "b64": base64.b64encode(data).decode("ascii")})
|
|
662
|
+
|
|
663
|
+
def resize(self, rows: int, cols: int) -> None:
|
|
664
|
+
"""Resize the PTY."""
|
|
665
|
+
self._send_envelope({"type": "resize", "rows": rows, "cols": cols})
|
|
666
|
+
|
|
667
|
+
def recv(self) -> Optional[bytes]:
|
|
668
|
+
"""Receive the next chunk of PTY output as raw bytes.
|
|
669
|
+
|
|
670
|
+
Skips the initial ``hello`` and any control envelopes, returning the next
|
|
671
|
+
``data`` payload. Raises :class:`DejimaError` on a server ``error``
|
|
672
|
+
envelope. Returns ``None`` when the connection closes.
|
|
673
|
+
"""
|
|
674
|
+
import websocket # type: ignore
|
|
675
|
+
|
|
676
|
+
while True:
|
|
677
|
+
try:
|
|
678
|
+
msg = self._ws.recv()
|
|
679
|
+
except websocket.WebSocketConnectionClosedException:
|
|
680
|
+
return None
|
|
681
|
+
if msg is None or msg == "":
|
|
682
|
+
return None
|
|
683
|
+
if isinstance(msg, bytes):
|
|
684
|
+
msg = msg.decode("utf-8", "replace")
|
|
685
|
+
env = json.loads(msg)
|
|
686
|
+
kind = env.get("type")
|
|
687
|
+
if kind == "data":
|
|
688
|
+
return base64.b64decode(env.get("b64", ""))
|
|
689
|
+
if kind == "error":
|
|
690
|
+
# The daemon puts the plaintext error string in the b64 field for
|
|
691
|
+
# error envelopes (it is not base64-encoded there).
|
|
692
|
+
raise DejimaError(0, str(env.get("b64", "")))
|
|
693
|
+
# "hello" and anything unknown: keep reading.
|
|
694
|
+
|
|
695
|
+
def close(self) -> None:
|
|
696
|
+
try:
|
|
697
|
+
self._ws.close()
|
|
698
|
+
except Exception: # pragma: no cover - best effort
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
def _send_envelope(self, env: Dict[str, Any]) -> None:
|
|
702
|
+
self._ws.send(json.dumps(env))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dejima-sdk
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Python client for the Dejima API — run a fleet of AI coding agents on hardware you own.
|
|
5
|
+
Project-URL: Homepage, https://dejima.tech/
|
|
6
|
+
Project-URL: Source, https://github.com/aoos/dejima
|
|
7
|
+
Project-URL: API, https://dejima.tech/api.html
|
|
8
|
+
Project-URL: Issues, https://github.com/aoos/dejima/issues
|
|
9
|
+
Author: Dejima
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ai-agents,claude-code,codex,dejima,sandbox,self-hosted
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: requests>=2.28
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
23
|
+
Requires-Dist: websocket-client>=1.6; extra == 'dev'
|
|
24
|
+
Provides-Extra: ws
|
|
25
|
+
Requires-Dist: websocket-client>=1.6; extra == 'ws'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# dejima — Python client
|
|
29
|
+
|
|
30
|
+
Thin Python client for the [Dejima](https://dejima.tech/) API: run a
|
|
31
|
+
fleet of AI coding agents on hardware you own.
|
|
32
|
+
|
|
33
|
+
> **Alpha (0.x).** The API is stable in shape (`v1/`-prefixed) but fields may
|
|
34
|
+
> change until `1.0`. This client is hand-written over the REST surface and
|
|
35
|
+
> mirrors [`openapi.yaml`](https://github.com/aoos/dejima/blob/master/openapi.yaml);
|
|
36
|
+
> the only non-generated piece is the PTY `Session` helper.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install dejima-sdk # REST client
|
|
42
|
+
pip install 'dejima[ws]' # + WebSocket PTY attach()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from dejima import Client
|
|
49
|
+
|
|
50
|
+
# host/token from $DEJIMA_HOST and $DEJIMA_TOKEN, or pass explicitly:
|
|
51
|
+
dj = Client(host="100.84.12.7:7273")
|
|
52
|
+
|
|
53
|
+
isl = dj.create_island(repo="git@github.com:you/foo.git", agent="claude-code")
|
|
54
|
+
print(isl["name"], isl["state"])
|
|
55
|
+
|
|
56
|
+
# add a second agent on its own worktree
|
|
57
|
+
dj.add_agent(isl["name"], type="codex")
|
|
58
|
+
|
|
59
|
+
# one-shot command
|
|
60
|
+
out = dj.exec(isl["name"], ["git", "status", "--short"])
|
|
61
|
+
print(out["stdout"], "exit", out["exit_code"])
|
|
62
|
+
|
|
63
|
+
# fleet view
|
|
64
|
+
for i in dj.list_islands():
|
|
65
|
+
print(i["name"], i["state"], len(i.get("agents", [])), "agents")
|
|
66
|
+
|
|
67
|
+
print(dj.overview()) # daemon health, VM memory, rollup
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Interactive sessions
|
|
71
|
+
|
|
72
|
+
`attach()` opens the multi-attach PTY stream (needs the `ws` extra). The daemon
|
|
73
|
+
speaks a small JSON-envelope protocol over the WebSocket — `Session` hides that,
|
|
74
|
+
so you deal in raw `bytes`:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
with dj.attach(isl["name"]) as s: # or agent="p2"
|
|
78
|
+
s.resize(40, 120)
|
|
79
|
+
s.send(b"ls -la\n")
|
|
80
|
+
print(s.recv()) # bytes of PTY output, or None when closed
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`dj.attach_terminal("t1")` does the same for an operator host terminal.
|
|
84
|
+
|
|
85
|
+
## API coverage
|
|
86
|
+
|
|
87
|
+
The client covers the full v1 surface:
|
|
88
|
+
|
|
89
|
+
- **Islands** — list/create/get/update/delete, hibernate/wake/reset/upgrade,
|
|
90
|
+
clone, resources, workspace-ready, events.
|
|
91
|
+
- **Agents** — list/add/get/update/remove, configure (provider/model).
|
|
92
|
+
- **Exec & files** — exec, file read/write, logs.
|
|
93
|
+
- **Port broker** — scopes (list/grant/revoke), intake/export/write.
|
|
94
|
+
- **Capability broker** — grants (list/grant/revoke), execute.
|
|
95
|
+
- **MCP broker** — grants (list/grant/revoke), `mcp_call`.
|
|
96
|
+
- **Credentials** — Claude push/status, GitHub identities + repos, LLM providers.
|
|
97
|
+
- **Operator tokens** — create/list/revoke (owner-only; role + island scope).
|
|
98
|
+
- **Events** — webhook subscribe/list/unsubscribe.
|
|
99
|
+
- **Activity** — team activity feed (`activity`, filterable).
|
|
100
|
+
- **Daemon** — overview, agent-types, healthz, audit (filters + jsonl/csv export),
|
|
101
|
+
clients, sessions-revoke, panic, admin-update, image-build, SSH account keys.
|
|
102
|
+
- **Host terminals** — list/create/delete/relabel + `attach_terminal`.
|
|
103
|
+
- **Sessions** — `attach` / `attach_terminal` (PTY) and `session_url`.
|
|
104
|
+
|
|
105
|
+
Every non-2xx response raises `dejima.DejimaError` (`.status`, `.message`).
|
|
106
|
+
|
|
107
|
+
## Auth
|
|
108
|
+
|
|
109
|
+
- **Operator** (unix socket / tailnet) needs no token.
|
|
110
|
+
- **Autonomy path** (an agent driving its own/child islands) uses a per-island
|
|
111
|
+
bearer token — set `DEJIMA_TOKEN` or pass `token=`.
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install -e '.[dev]'
|
|
117
|
+
pytest
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Tests run against a stdlib `http.server` stub — no running daemon required.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
dejima/__init__.py,sha256=EWDi-WjL5a17IDtkjNo726COY_Vldz4pHKdp80cIDR4,686
|
|
2
|
+
dejima/client.py,sha256=s2iMCfluZCPdqMDBxEfx5kBHohY4ogma46GWvyULa2E,30078
|
|
3
|
+
dejima_sdk-0.5.2.dist-info/METADATA,sha256=ZQkjuBzZxlpYTm9mcGoNbjBbI6WZpMu7VdEGMcBaegQ,4317
|
|
4
|
+
dejima_sdk-0.5.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
dejima_sdk-0.5.2.dist-info/RECORD,,
|