dent8 0.6.0__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.
Potentially problematic release.
This version of dent8 might be problematic. Click here for more details.
- dent8/__init__.py +331 -0
- dent8-0.6.0.dist-info/METADATA +78 -0
- dent8-0.6.0.dist-info/RECORD +4 -0
- dent8-0.6.0.dist-info/WHEEL +4 -0
dent8/__init__.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""dent8 — a memory firewall for coding agents, from Python.
|
|
2
|
+
|
|
3
|
+
A deliberately thin SDK: every call runs the ``dent8`` binary with ``--output json``
|
|
4
|
+
and returns the parsed payload as a ``dict``. The wire contract is the product —
|
|
5
|
+
each payload carries ``schema_version``; every error carries a stable ``status``
|
|
6
|
+
(``rejected`` / ``invalid``) and a machine-readable ``code``
|
|
7
|
+
(``insufficient-authority``, ``authority-ceiling``, ``content-rejected``, …), raised
|
|
8
|
+
here as :class:`Dent8Rejected` / :class:`Dent8Invalid` so callers branch on
|
|
9
|
+
``error.code`` instead of parsing prose.
|
|
10
|
+
|
|
11
|
+
Because the CLI itself resolves the store (repo-confined ``.dent8/`` discovery,
|
|
12
|
+
``DENT8_LOG`` / ``DENT8_STORE_URL`` env), routes writes through a local daemon when
|
|
13
|
+
``DENT8_DAEMON_SOCKET`` is set, and defaults ``--source`` / ``--authority`` from the
|
|
14
|
+
active signed grant, this SDK inherits all of that for free.
|
|
15
|
+
|
|
16
|
+
from dent8 import Dent8
|
|
17
|
+
|
|
18
|
+
d8 = Dent8()
|
|
19
|
+
d8.assert_fact("repo:myproj", "database", "postgres",
|
|
20
|
+
authority="high", source="user:alice")
|
|
21
|
+
fact = d8.explain("repo:myproj", "database")
|
|
22
|
+
assert fact["status"] == "ok"
|
|
23
|
+
|
|
24
|
+
Requires the ``dent8`` binary on ``PATH`` (``cargo install dent8-cli --locked``) or an
|
|
25
|
+
explicit ``Dent8(binary=...)``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import subprocess
|
|
33
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Dent8",
|
|
37
|
+
"Dent8Error",
|
|
38
|
+
"Dent8Invalid",
|
|
39
|
+
"Dent8Rejected",
|
|
40
|
+
"SCHEMA_VERSION",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
#: The JSON output shape this SDK was written against (the CLI stamps it on every payload).
|
|
44
|
+
SCHEMA_VERSION = 1
|
|
45
|
+
|
|
46
|
+
#: A wall-clock instant, in any grammar the CLI accepts: unix millis (int), ``"now"``,
|
|
47
|
+
#: a ±duration offset (``"-7d"``), RFC 3339, or a bare UTC date/datetime.
|
|
48
|
+
Time = Union[int, str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Dent8Error(Exception):
|
|
52
|
+
"""A failed dent8 operation. ``status`` says what happened (``rejected`` /
|
|
53
|
+
``invalid``), ``code`` says why (stable kebab-case, e.g.
|
|
54
|
+
``insufficient-authority``), ``payload`` is the full JSON error object."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, payload: Dict[str, Any]) -> None:
|
|
57
|
+
self.payload = payload
|
|
58
|
+
self.status: str = payload.get("status", "failed")
|
|
59
|
+
self.code: str = payload.get("code", "operation-failed")
|
|
60
|
+
self.message: str = payload.get("message", payload.get("error_reason", ""))
|
|
61
|
+
super().__init__(f"[{self.code}] {self.message}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Dent8Rejected(Dent8Error):
|
|
65
|
+
"""The firewall (or a write-boundary gate) refused a well-formed request."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Dent8Invalid(Dent8Error):
|
|
69
|
+
"""The request was malformed or the configuration unusable."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Dent8:
|
|
73
|
+
"""A handle on the dent8 belief surface, one subprocess per call.
|
|
74
|
+
|
|
75
|
+
:param binary: path to the ``dent8`` binary (default: ``dent8`` on ``PATH``, or
|
|
76
|
+
the ``DENT8_BIN`` environment variable when set).
|
|
77
|
+
:param cwd: working directory for store discovery (default: the process cwd).
|
|
78
|
+
:param env: extra environment entries merged over ``os.environ`` — e.g.
|
|
79
|
+
``{"DENT8_LOG": "/tmp/memory.jsonl"}`` to pin a store instead of discovery.
|
|
80
|
+
:param timeout: per-call subprocess timeout in seconds.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
binary: Optional[str] = None,
|
|
86
|
+
*,
|
|
87
|
+
cwd: Optional[str] = None,
|
|
88
|
+
env: Optional[Dict[str, str]] = None,
|
|
89
|
+
timeout: float = 60.0,
|
|
90
|
+
) -> None:
|
|
91
|
+
self.binary = binary or os.environ.get("DENT8_BIN", "dent8")
|
|
92
|
+
self.cwd = cwd
|
|
93
|
+
self.env = env or {}
|
|
94
|
+
self.timeout = timeout
|
|
95
|
+
|
|
96
|
+
# ---- writes ---------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def assert_fact(
|
|
99
|
+
self,
|
|
100
|
+
subject: str,
|
|
101
|
+
predicate: str,
|
|
102
|
+
value: str,
|
|
103
|
+
*,
|
|
104
|
+
authority: Optional[str] = None,
|
|
105
|
+
source: Optional[str] = None,
|
|
106
|
+
valid_from: Optional[Time] = None,
|
|
107
|
+
valid_to: Optional[Time] = None,
|
|
108
|
+
ttl: Optional[str] = None,
|
|
109
|
+
) -> Dict[str, Any]:
|
|
110
|
+
"""Assert a fact through the firewall. Named ``assert_fact`` because
|
|
111
|
+
``assert`` is a Python keyword; every other verb matches the CLI 1:1."""
|
|
112
|
+
return self._run(
|
|
113
|
+
"assert",
|
|
114
|
+
subject,
|
|
115
|
+
predicate,
|
|
116
|
+
value,
|
|
117
|
+
authority=authority,
|
|
118
|
+
source=source,
|
|
119
|
+
valid_from=valid_from,
|
|
120
|
+
valid_to=valid_to,
|
|
121
|
+
ttl=ttl,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def supersede(
|
|
125
|
+
self,
|
|
126
|
+
subject: str,
|
|
127
|
+
predicate: str,
|
|
128
|
+
value: str,
|
|
129
|
+
*,
|
|
130
|
+
authority: Optional[str] = None,
|
|
131
|
+
source: Optional[str] = None,
|
|
132
|
+
valid_from: Optional[Time] = None,
|
|
133
|
+
valid_to: Optional[Time] = None,
|
|
134
|
+
ttl: Optional[str] = None,
|
|
135
|
+
) -> Dict[str, Any]:
|
|
136
|
+
"""Revise the believed fact via the sanctioned supersession path."""
|
|
137
|
+
return self._run(
|
|
138
|
+
"supersede",
|
|
139
|
+
subject,
|
|
140
|
+
predicate,
|
|
141
|
+
value,
|
|
142
|
+
authority=authority,
|
|
143
|
+
source=source,
|
|
144
|
+
valid_from=valid_from,
|
|
145
|
+
valid_to=valid_to,
|
|
146
|
+
ttl=ttl,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def contradict(
|
|
150
|
+
self,
|
|
151
|
+
subject: str,
|
|
152
|
+
predicate: str,
|
|
153
|
+
value: str,
|
|
154
|
+
*,
|
|
155
|
+
authority: Optional[str] = None,
|
|
156
|
+
source: Optional[str] = None,
|
|
157
|
+
valid_from: Optional[Time] = None,
|
|
158
|
+
valid_to: Optional[Time] = None,
|
|
159
|
+
ttl: Optional[str] = None,
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""Record dissent: keep both facts, mark the pair contested."""
|
|
162
|
+
return self._run(
|
|
163
|
+
"contradict",
|
|
164
|
+
subject,
|
|
165
|
+
predicate,
|
|
166
|
+
value,
|
|
167
|
+
authority=authority,
|
|
168
|
+
source=source,
|
|
169
|
+
valid_from=valid_from,
|
|
170
|
+
valid_to=valid_to,
|
|
171
|
+
ttl=ttl,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def retract(
|
|
175
|
+
self,
|
|
176
|
+
subject: str,
|
|
177
|
+
predicate: str,
|
|
178
|
+
*,
|
|
179
|
+
authority: Optional[str] = None,
|
|
180
|
+
source: Optional[str] = None,
|
|
181
|
+
) -> Dict[str, Any]:
|
|
182
|
+
"""Retract the believed fact (terminal; taints derivatives)."""
|
|
183
|
+
return self._run("retract", subject, predicate, authority=authority, source=source)
|
|
184
|
+
|
|
185
|
+
def reinforce(
|
|
186
|
+
self,
|
|
187
|
+
subject: str,
|
|
188
|
+
predicate: str,
|
|
189
|
+
*,
|
|
190
|
+
authority: Optional[str] = None,
|
|
191
|
+
source: Optional[str] = None,
|
|
192
|
+
) -> Dict[str, Any]:
|
|
193
|
+
"""Corroborate the believed fact from another source (earned entrenchment)."""
|
|
194
|
+
return self._run("reinforce", subject, predicate, authority=authority, source=source)
|
|
195
|
+
|
|
196
|
+
def expire(
|
|
197
|
+
self,
|
|
198
|
+
subject: str,
|
|
199
|
+
predicate: str,
|
|
200
|
+
*,
|
|
201
|
+
authority: Optional[str] = None,
|
|
202
|
+
source: Optional[str] = None,
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
"""Expire the believed fact (terminal)."""
|
|
205
|
+
return self._run("expire", subject, predicate, authority=authority, source=source)
|
|
206
|
+
|
|
207
|
+
def derive(
|
|
208
|
+
self,
|
|
209
|
+
subject: str,
|
|
210
|
+
predicate: str,
|
|
211
|
+
value: str,
|
|
212
|
+
*,
|
|
213
|
+
basis: Tuple[str, str],
|
|
214
|
+
authority: Optional[str] = None,
|
|
215
|
+
source: Optional[str] = None,
|
|
216
|
+
valid_from: Optional[Time] = None,
|
|
217
|
+
valid_to: Optional[Time] = None,
|
|
218
|
+
ttl: Optional[str] = None,
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
"""Assert a fact derived from ``basis = (subject, predicate)``, recording the
|
|
221
|
+
dependency edge — if the basis is later retracted, this derivative is flagged
|
|
222
|
+
tainted by ``verify``."""
|
|
223
|
+
basis_subject, basis_predicate = basis
|
|
224
|
+
return self._run(
|
|
225
|
+
"derive",
|
|
226
|
+
subject,
|
|
227
|
+
predicate,
|
|
228
|
+
value,
|
|
229
|
+
"--basis",
|
|
230
|
+
basis_subject,
|
|
231
|
+
basis_predicate,
|
|
232
|
+
authority=authority,
|
|
233
|
+
source=source,
|
|
234
|
+
valid_from=valid_from,
|
|
235
|
+
valid_to=valid_to,
|
|
236
|
+
ttl=ttl,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# ---- reads / audit ----------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def explain(
|
|
242
|
+
self,
|
|
243
|
+
subject: str,
|
|
244
|
+
predicate: str,
|
|
245
|
+
*,
|
|
246
|
+
as_of: Optional[Time] = None,
|
|
247
|
+
valid_at: Optional[Time] = None,
|
|
248
|
+
) -> Dict[str, Any]:
|
|
249
|
+
"""The believed (or terminal) fact with its integrity receipt; time-travel
|
|
250
|
+
with ``as_of`` / ``valid_at``."""
|
|
251
|
+
return self._run("explain", subject, predicate, as_of=as_of, valid_at=valid_at)
|
|
252
|
+
|
|
253
|
+
def replay(
|
|
254
|
+
self,
|
|
255
|
+
subject: str,
|
|
256
|
+
predicate: str,
|
|
257
|
+
*,
|
|
258
|
+
as_of: Optional[Time] = None,
|
|
259
|
+
valid_at: Optional[Time] = None,
|
|
260
|
+
) -> Dict[str, Any]:
|
|
261
|
+
"""The full event history behind a fact — why it is believed."""
|
|
262
|
+
return self._run("replay", subject, predicate, as_of=as_of, valid_at=valid_at)
|
|
263
|
+
|
|
264
|
+
def facts(self, *, include_diagnostics: bool = False) -> Dict[str, Any]:
|
|
265
|
+
"""Every known fact stream, with freshness flags."""
|
|
266
|
+
args: List[str] = ["facts", "list"]
|
|
267
|
+
if include_diagnostics:
|
|
268
|
+
args.append("--include-diagnostics")
|
|
269
|
+
return self._run(*args)
|
|
270
|
+
|
|
271
|
+
def verify(self) -> Dict[str, Any]:
|
|
272
|
+
"""Integrity checks: hash chain, lineage, taint, attestations. Findings are a
|
|
273
|
+
*result*, not an exception: the payload's ``status`` is ``ok`` or
|
|
274
|
+
``integrity_issues`` (mirroring the MCP tool). Only a malformed invocation
|
|
275
|
+
raises."""
|
|
276
|
+
try:
|
|
277
|
+
return self._run("verify")
|
|
278
|
+
except Dent8Rejected as error:
|
|
279
|
+
return error.payload
|
|
280
|
+
|
|
281
|
+
def conflicts(self) -> Dict[str, Any]:
|
|
282
|
+
"""Contested facts. ``status`` is ``contested`` when disputes exist, ``ok``
|
|
283
|
+
when none do."""
|
|
284
|
+
return self._run("conflicts")
|
|
285
|
+
|
|
286
|
+
# ---- plumbing ---------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def _run(self, *args: str, **flags: Optional[Union[Time, str]]) -> Dict[str, Any]:
|
|
289
|
+
command: List[str] = [self.binary, "--output", "json", *args]
|
|
290
|
+
for name, value in flags.items():
|
|
291
|
+
if value is None:
|
|
292
|
+
continue
|
|
293
|
+
command.append("--" + name.replace("_", "-"))
|
|
294
|
+
command.append(str(value))
|
|
295
|
+
completed = subprocess.run(
|
|
296
|
+
command,
|
|
297
|
+
capture_output=True,
|
|
298
|
+
text=True,
|
|
299
|
+
timeout=self.timeout,
|
|
300
|
+
cwd=self.cwd,
|
|
301
|
+
env={**os.environ, **self.env},
|
|
302
|
+
check=False,
|
|
303
|
+
)
|
|
304
|
+
payload = self._parse(completed, command)
|
|
305
|
+
if completed.returncode == 0:
|
|
306
|
+
return payload
|
|
307
|
+
raise (Dent8Invalid if completed.returncode == 2 else Dent8Rejected)(payload)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _parse(
|
|
311
|
+
completed: "subprocess.CompletedProcess[str]", command: Sequence[str]
|
|
312
|
+
) -> Dict[str, Any]:
|
|
313
|
+
# The machine contract: every --output json result — success and error — is one
|
|
314
|
+
# JSON object on stdout. Anything else (e.g. a clap usage error on stderr) is
|
|
315
|
+
# surfaced as invalid with the raw text preserved.
|
|
316
|
+
text = completed.stdout.strip()
|
|
317
|
+
if text:
|
|
318
|
+
try:
|
|
319
|
+
payload = json.loads(text)
|
|
320
|
+
if isinstance(payload, dict):
|
|
321
|
+
return payload
|
|
322
|
+
except json.JSONDecodeError:
|
|
323
|
+
pass
|
|
324
|
+
raise Dent8Invalid(
|
|
325
|
+
{
|
|
326
|
+
"status": "invalid",
|
|
327
|
+
"code": "invalid-argument",
|
|
328
|
+
"message": (completed.stderr or completed.stdout or "").strip()
|
|
329
|
+
or f"no JSON output from: {' '.join(command)}",
|
|
330
|
+
}
|
|
331
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dent8
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: A memory firewall for coding agents — thin Python SDK over the dent8 CLI's JSON contract.
|
|
5
|
+
Project-URL: Homepage, https://github.com/xyzzylabs/dent8
|
|
6
|
+
Project-URL: Changelog, https://github.com/xyzzylabs/dent8/blob/main/CHANGELOG.md
|
|
7
|
+
Author: xyzzylabs
|
|
8
|
+
License-Expression: Apache-2.0 OR MIT
|
|
9
|
+
Keywords: agent,firewall,mcp,memory,provenance
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# dent8 (Python)
|
|
18
|
+
|
|
19
|
+
A memory firewall for coding agents — the thin Python SDK.
|
|
20
|
+
|
|
21
|
+
Every call runs the [`dent8`](https://github.com/xyzzylabs/dent8) binary with
|
|
22
|
+
`--output json` and returns the parsed payload as a `dict`. The wire contract is
|
|
23
|
+
the product: each payload carries `schema_version`; every error carries a stable
|
|
24
|
+
`status` plus a machine-readable `code` (`insufficient-authority`,
|
|
25
|
+
`authority-ceiling`, `content-rejected`, …), raised as `Dent8Rejected` /
|
|
26
|
+
`Dent8Invalid` so you branch on `error.code`, never on prose.
|
|
27
|
+
|
|
28
|
+
Because the CLI resolves the store itself (repo-confined `.dent8/` discovery,
|
|
29
|
+
`DENT8_LOG` / `DENT8_STORE_URL`), routes writes through a local daemon when
|
|
30
|
+
`DENT8_DAEMON_SOCKET` is set, and defaults `--source`/`--authority` from the
|
|
31
|
+
active signed grant, the SDK inherits all of it with zero configuration.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install dent8
|
|
37
|
+
cargo install dent8-cli --locked # the binary the SDK drives
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dent8 import Dent8, Dent8Rejected
|
|
44
|
+
|
|
45
|
+
d8 = Dent8() # finds `dent8` on PATH; Dent8(binary=..., env={"DENT8_LOG": ...}) to pin
|
|
46
|
+
|
|
47
|
+
d8.assert_fact("repo:myproj", "database", "postgres",
|
|
48
|
+
authority="high", source="user:alice")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
d8.supersede("repo:myproj", "database", "mysql",
|
|
52
|
+
authority="low", source="web:scrape")
|
|
53
|
+
except Dent8Rejected as rejection:
|
|
54
|
+
assert rejection.code == "insufficient-authority" # branch on the code
|
|
55
|
+
|
|
56
|
+
fact = d8.explain("repo:myproj", "database")
|
|
57
|
+
assert fact["value"]["text"] == "postgres" # the firewall held
|
|
58
|
+
|
|
59
|
+
report = d8.verify() # findings are a result: status ok | integrity_issues
|
|
60
|
+
disputes = d8.conflicts() # status contested when live disputes exist
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The belief surface maps 1:1 onto the CLI: `assert_fact` (Python keyword), `supersede`,
|
|
64
|
+
`contradict`, `retract`, `reinforce`, `expire`, `derive(basis=(subject, predicate))`,
|
|
65
|
+
`explain`, `replay`, `facts`, `verify`, `conflicts`. Temporal keywords accept the CLI's
|
|
66
|
+
whole grammar — unix millis, `"now"`, `"-7d"`, RFC 3339, or a bare UTC date.
|
|
67
|
+
|
|
68
|
+
For LLM tool-calling agents, prefer the MCP server (`dent8 mcp serve`) — see
|
|
69
|
+
[examples/langchain](https://github.com/xyzzylabs/dent8/tree/main/examples/langchain) and
|
|
70
|
+
[examples/vercel-ai-sdk](https://github.com/xyzzylabs/dent8/tree/main/examples/vercel-ai-sdk). This SDK is for
|
|
71
|
+
*programmatic* access from Python code.
|
|
72
|
+
|
|
73
|
+
## Test
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
cargo build -p dent8-cli
|
|
77
|
+
DENT8_BIN=../../target/debug/dent8 python -m pytest
|
|
78
|
+
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
dent8/__init__.py,sha256=EOjQ_wkhMLb0To4i2S79-BJa9WrywvWqsmvx3GKwJJQ,11348
|
|
2
|
+
dent8-0.6.0.dist-info/METADATA,sha256=Aim3OxHFaHnvWYnUfekypbrTt8Motjny8OBCIpi130A,3106
|
|
3
|
+
dent8-0.6.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
+
dent8-0.6.0.dist-info/RECORD,,
|