prime-agent-dsh 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: dsh-context
3
+ description: Inspect the current Prime session through lazy DeepSeek Harness context objects. Use to search prior turns, read bounded transcript slices, inspect provider-reported cache usage, create immutable snapshots or private artifacts, deliberately admit selected context, and grant bounded parent context to an RLM descendant.
4
+ ---
5
+
6
+ # DSH Context Objects
7
+
8
+ The extension mirrors Prime's canonical active branch into a rebuildable DSH projection before each model request. Prime remains the only model/tool loop and canonical session log. The Python module reads immutable private snapshots; it never edits either history.
9
+
10
+ ```python
11
+ ctx = dsh_context.current()
12
+ ctx
13
+
14
+ ctx.entries(last=10)
15
+ ctx.search("authentication", limit=20)
16
+ ctx.metrics
17
+
18
+ selection = ctx.search("migration decision", limit=8)
19
+ print(ctx.inject(selection, label="Relevant migration decisions"))
20
+ ```
21
+
22
+ ## API
23
+
24
+ - `dsh_context.current()` returns the current session's `ContextHandle`.
25
+ - `ctx.snapshot(digest=None)` pins the current or named immutable snapshot.
26
+ - `ctx.entries(start=0, limit=20, last=None, role=None)` returns bounded Prime branch entries.
27
+ - `ctx.messages(start=0, limit=20, last=None, role=None)` returns bounded DSH-projected messages.
28
+ - `ctx.search(query, limit=20, regex=False, case_sensitive=False)` searches the complete retained entry range.
29
+ - `ctx.metrics` reports provider-supplied input, output, cache-read, cache-write, and total token aggregates. It does not infer cache hits.
30
+ - `ctx.artifact(value, label="context")` writes selected material to a private content-addressed Markdown file.
31
+ - `ctx.inject(value, label="Selected DSH context", max_bytes=65536)` returns bounded model-facing text. **Print or return this value from the IPython cell** so Prime records it as a durable tool result. `ctx.admit` is an alias.
32
+ - `grant = ctx.grant(value, label="Shared parent context")` creates a bounded, expiring read-only capability. Include `grant.instruction` in an RLM child's task.
33
+ - `dsh_context.open_grant(grant.uri)` opens a grant reachable from the current session directory or one of its ancestors.
34
+
35
+ ## Parent-to-child context
36
+
37
+ ```python
38
+ selection = ctx.search("authentication design", limit=12)
39
+ grant = ctx.grant(selection, label="Authentication evidence")
40
+ child = await rlm.spawn(
41
+ "Review the authentication design. " + grant.instruction,
42
+ name="auth-review",
43
+ )
44
+ ```
45
+
46
+ The child can then call:
47
+
48
+ ```python
49
+ evidence = dsh_context.open_grant("dsh-context-grant:...")
50
+ evidence.value
51
+ ```
52
+
53
+ A grant contains only the selected bounded value, its source snapshot identity, and expiry. It does not grant parent tool authority or a live parent transcript.
54
+
55
+ ## Rules
56
+
57
+ - Search and inspect before admitting context; do not copy the complete transcript into active context.
58
+ - Treat snapshots as read-only evidence at their recorded revision and branch leaf.
59
+ - A child sees its own session snapshot. A small untrusted evidence capsule is inherited automatically through stock Prime lifecycle hooks; use an explicit capability grant for larger selected parent context.
60
+ - Reference-only snapshot metadata and indexes are derived and rebuildable. Prime JSONL is the only full-content authority; new DSH roots do not persist duplicate message bodies or compatibility text.
61
+ - Printing `ctx.inject(...)` is the supported durable-admission path on Prime 0.9.5. Arbitrary extension-defined Python host requests are not public in that release.
62
+ - Provider cache metrics are authoritative; context-object access itself does not imply a cache hit.
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "dsh-context"
3
+ version = "0.1.0"
4
+ description = "Lazy DeepSeek Harness context objects for Prime Agent kernels"
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [build-system]
9
+ requires = ["hatchling"]
10
+ build-backend = "hatchling.build"
11
+
12
+ [tool.hatch.build.targets.wheel]
13
+ packages = ["src/dsh_context"]
@@ -0,0 +1,573 @@
1
+ """Lazy DeepSeek Harness context objects for Prime Agent kernels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Iterator, Sequence
6
+ from dataclasses import dataclass
7
+ from hashlib import sha256
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import re
12
+ import secrets
13
+ import time
14
+ from typing import Any
15
+
16
+ _VERSION = "prime-agent-dsh/context-object-v1"
17
+ _GRANT_VERSION = "prime-agent-dsh/context-grant-v1"
18
+ _DEFAULT_LIMIT = 20
19
+ _MAX_LIMIT = 200
20
+ _MAX_INJECT_BYTES = 65_536
21
+ _MAX_ARTIFACT_BYTES = 4 * 1024 * 1024
22
+ _MAX_GRANT_BYTES = 1024 * 1024
23
+ _MAX_GRANT_TTL_SECONDS = 7 * 24 * 60 * 60
24
+ _GRANT_TOKEN = re.compile(r"^[A-Za-z0-9_-]{24,128}$")
25
+
26
+
27
+ def _session_dir() -> Path:
28
+ raw = os.environ.get("RLM_SESSION_DIR")
29
+ if not raw:
30
+ raise RuntimeError("RLM_SESSION_DIR is unavailable; dsh_context requires a Prime Agent session kernel")
31
+ return Path(raw).expanduser().resolve()
32
+
33
+
34
+ def _private_root() -> Path:
35
+ root = _session_dir() / "dsh-context"
36
+ manifest = root / "manifest.json"
37
+ if not manifest.is_file():
38
+ raise RuntimeError(
39
+ f"DSH context snapshot is not ready at {manifest}; run one model turn after loading the extension"
40
+ )
41
+ return root
42
+
43
+
44
+ def _read_json(path: Path) -> dict[str, Any]:
45
+ value = json.loads(path.read_text(encoding="utf-8"))
46
+ if not isinstance(value, dict):
47
+ raise RuntimeError(f"invalid DSH context object at {path}")
48
+ return value
49
+
50
+
51
+ def _canonical_json(value: Any) -> bytes:
52
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
53
+
54
+
55
+ def _bounded_limit(limit: int) -> int:
56
+ if not isinstance(limit, int) or isinstance(limit, bool) or limit < 0:
57
+ raise TypeError("limit must be a non-negative integer")
58
+ return min(limit, _MAX_LIMIT)
59
+
60
+
61
+ def _message_text(message: dict[str, Any]) -> str:
62
+ out: list[str] = []
63
+ content = message.get("content")
64
+ if not isinstance(content, list):
65
+ return ""
66
+ for block in content:
67
+ if not isinstance(block, dict):
68
+ continue
69
+ kind = block.get("type")
70
+ if kind in ("text", "reasoning") and isinstance(block.get("text"), str):
71
+ out.append(block["text"])
72
+ elif kind == "tool-call":
73
+ out.append(f"[tool {block.get('name', 'unknown')}] {block.get('arguments', '')}")
74
+ elif kind == "tool-result":
75
+ out.append(_message_text({"content": block.get("content", [])}))
76
+ elif kind == "image":
77
+ out.append("[image attachment]")
78
+ return "\n".join(part for part in out if part)
79
+
80
+
81
+ def _jsonable(value: Any) -> Any:
82
+ if isinstance(value, ContextSelection):
83
+ return [item.data for item in value]
84
+ if isinstance(value, ContextItem):
85
+ return value.data
86
+ if isinstance(value, ContextSnapshot):
87
+ return value.data
88
+ if isinstance(value, GrantedContext):
89
+ return value.value
90
+ return value
91
+
92
+
93
+ def _render(value: Any) -> str:
94
+ plain = _jsonable(value)
95
+ if isinstance(plain, str):
96
+ return plain
97
+ return json.dumps(plain, ensure_ascii=False, indent=2, sort_keys=True)
98
+
99
+
100
+ def _crop_utf8(value: str, max_bytes: int) -> str:
101
+ encoded = value.encode("utf-8")
102
+ if len(encoded) <= max_bytes:
103
+ return value
104
+ marker = f"\n[… {len(encoded) - max_bytes} UTF-8 bytes omitted …]"
105
+ budget = max(0, max_bytes - len(marker.encode("utf-8")))
106
+ return encoded[:budget].decode("utf-8", errors="ignore") + marker
107
+
108
+
109
+ def _private_directory(path: Path) -> None:
110
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
111
+ if path.is_symlink() or not path.is_dir():
112
+ raise RuntimeError(f"unsafe DSH context directory: {path}")
113
+ os.chmod(path, 0o700)
114
+
115
+
116
+ def _exclusive_write(path: Path, raw: bytes) -> None:
117
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
118
+ with os.fdopen(fd, "wb") as stream:
119
+ stream.write(raw)
120
+ stream.flush()
121
+ os.fsync(stream.fileno())
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class ContextItem:
126
+ """One immutable entry or model message selected from a snapshot."""
127
+
128
+ kind: str
129
+ index: int
130
+ data: dict[str, Any]
131
+ text: str
132
+
133
+ @property
134
+ def role(self) -> str | None:
135
+ value = self.data.get("role")
136
+ return value if isinstance(value, str) else None
137
+
138
+
139
+ class ContextSelection(Sequence[ContextItem]):
140
+ """A bounded immutable collection tied to one snapshot digest."""
141
+
142
+ def __init__(self, digest: str, items: Iterable[ContextItem]):
143
+ self.digest = digest
144
+ self._items = tuple(items)
145
+
146
+ def __getitem__(self, index: int | slice) -> ContextItem | tuple[ContextItem, ...]:
147
+ return self._items[index]
148
+
149
+ def __len__(self) -> int:
150
+ return len(self._items)
151
+
152
+ def __iter__(self) -> Iterator[ContextItem]:
153
+ return iter(self._items)
154
+
155
+ def __repr__(self) -> str:
156
+ return f"ContextSelection(digest={self.digest[:12]!r}, items={len(self)})"
157
+
158
+
159
+ class ContextSnapshot:
160
+ """An immutable content-addressed view of one exact observed Prime branch."""
161
+
162
+ def __init__(self, root: Path, manifest: dict[str, Any], data: dict[str, Any]):
163
+ self.root = root
164
+ self.manifest = manifest
165
+ self.data = data
166
+ self.digest = str(manifest["digest"])
167
+ self.session_id = str(data["sessionId"])
168
+ self.branch_id = str(data["branchId"])
169
+ self.revision = int(data["revision"])
170
+ self.cropped = bool(data.get("cropped", False))
171
+ self.unavailable_message_count = int(data.get("unavailableEffectiveEntries", 0))
172
+ metrics = data.get("metrics")
173
+ self.metrics = dict(metrics) if isinstance(metrics, dict) else {}
174
+
175
+ def _select(
176
+ self,
177
+ kind: str,
178
+ values: list[Any],
179
+ *,
180
+ start: int,
181
+ limit: int,
182
+ last: int | None,
183
+ role: str | None,
184
+ ) -> ContextSelection:
185
+ if not isinstance(start, int) or isinstance(start, bool) or start < 0:
186
+ raise TypeError("start must be a non-negative integer")
187
+ limit = _bounded_limit(limit)
188
+ items: list[ContextItem] = []
189
+ for index, raw in enumerate(values):
190
+ if not isinstance(raw, dict):
191
+ continue
192
+ item_role = raw.get("role")
193
+ if role is not None and item_role != role:
194
+ continue
195
+ text = str(raw.get("text", "")) if kind == "entry" else _message_text(raw)
196
+ items.append(ContextItem(kind=kind, index=index, data=raw, text=text))
197
+ if last is not None:
198
+ if not isinstance(last, int) or isinstance(last, bool) or last < 0:
199
+ raise TypeError("last must be a non-negative integer or None")
200
+ count = min(last, _MAX_LIMIT)
201
+ return ContextSelection(self.digest, items[-count:] if count else [])
202
+ return ContextSelection(self.digest, items[start : start + limit])
203
+
204
+ def entries(
205
+ self,
206
+ start: int = 0,
207
+ limit: int = _DEFAULT_LIMIT,
208
+ *,
209
+ last: int | None = None,
210
+ role: str | None = None,
211
+ ) -> ContextSelection:
212
+ values = self.data.get("entries", [])
213
+ return self._select("entry", values if isinstance(values, list) else [], start=start, limit=limit, last=last, role=role)
214
+
215
+ def messages(
216
+ self,
217
+ start: int = 0,
218
+ limit: int = _DEFAULT_LIMIT,
219
+ *,
220
+ last: int | None = None,
221
+ role: str | None = None,
222
+ ) -> ContextSelection:
223
+ values = self.data.get("messages", [])
224
+ return self._select("message", values if isinstance(values, list) else [], start=start, limit=limit, last=last, role=role)
225
+
226
+ def search(
227
+ self,
228
+ query: str,
229
+ limit: int = _DEFAULT_LIMIT,
230
+ *,
231
+ regex: bool = False,
232
+ case_sensitive: bool = False,
233
+ ) -> ContextSelection:
234
+ if not isinstance(query, str) or not query:
235
+ raise ValueError("query must be a non-empty string")
236
+ limit = _bounded_limit(limit)
237
+ flags = 0 if case_sensitive else re.IGNORECASE
238
+ pattern = re.compile(query if regex else re.escape(query), flags)
239
+ values = self.data.get("entries", [])
240
+ items: list[ContextItem] = []
241
+ for index, raw in enumerate(values if isinstance(values, list) else []):
242
+ if not isinstance(raw, dict):
243
+ continue
244
+ text = str(raw.get("text", ""))
245
+ if pattern.search(text):
246
+ items.append(ContextItem(kind="entry", index=index, data=raw, text=text))
247
+ if len(items) == limit:
248
+ break
249
+ return ContextSelection(self.digest, items)
250
+
251
+ def __repr__(self) -> str:
252
+ return (
253
+ f"ContextSnapshot(session_id={self.session_id!r}, branch_id={self.branch_id!r}, "
254
+ f"revision={self.revision}, digest={self.digest[:12]!r}, cropped={self.cropped})"
255
+ )
256
+
257
+
258
+ @dataclass(frozen=True)
259
+ class ContextArtifact:
260
+ path: Path
261
+ digest: str
262
+ bytes: int
263
+
264
+
265
+ @dataclass(frozen=True)
266
+ class ContextGrant:
267
+ """Capability token for a bounded immutable parent-context selection."""
268
+
269
+ token: str
270
+ digest: str
271
+ expires_at: int
272
+ label: str
273
+
274
+ @property
275
+ def uri(self) -> str:
276
+ return f"dsh-context-grant:{self.token}"
277
+
278
+ @property
279
+ def instruction(self) -> str:
280
+ return (
281
+ f"A read-only parent context grant is available as {self.uri}. "
282
+ "In the Python REPL call dsh_context.open_grant(" + repr(self.uri) + ") to inspect it."
283
+ )
284
+
285
+
286
+ @dataclass(frozen=True)
287
+ class GrantedContext:
288
+ token: str
289
+ label: str
290
+ source_session_id: str
291
+ source_branch_id: str
292
+ source_snapshot_digest: str
293
+ created_at: int
294
+ expires_at: int
295
+ value: Any
296
+
297
+
298
+ class ContextHandle:
299
+ """Live pointer whose operations pin the current immutable snapshot first."""
300
+
301
+ def __init__(self, root: Path):
302
+ self.root = root
303
+
304
+ def snapshot(self, digest: str | None = None, *, expected_branch_id: str | None = None) -> ContextSnapshot:
305
+ manifest = _read_json(self.root / "manifest.json")
306
+ if manifest.get("version") != _VERSION:
307
+ raise RuntimeError(f"unsupported DSH context manifest version: {manifest.get('version')!r}")
308
+ expected = manifest.get("digest") if digest is None else digest
309
+ if not isinstance(expected, str) or not re.fullmatch(r"[a-f0-9]{64}", expected):
310
+ raise ValueError("snapshot digest must be 64 lowercase hexadecimal characters")
311
+ if digest is None or digest == manifest.get("digest"):
312
+ relative = manifest.get("snapshot")
313
+ if relative != f"objects/{expected}.json":
314
+ raise RuntimeError("invalid DSH context object path")
315
+ else:
316
+ manifest = {**manifest, "digest": expected, "snapshot": f"objects/{expected}.json"}
317
+ path = (self.root / "objects" / f"{expected}.json").resolve()
318
+ if self.root not in path.parents or path.is_symlink() or not path.is_file():
319
+ raise RuntimeError(f"invalid DSH context object file: {path}")
320
+ raw = path.read_bytes()
321
+ if sha256(raw.rstrip(b"\n")).hexdigest() != expected:
322
+ raise RuntimeError("DSH context object digest mismatch")
323
+ stored = json.loads(raw)
324
+ if not isinstance(stored, dict) or stored.get("version") != "prime-agent-dsh/derived-object-v3-reference":
325
+ raise RuntimeError("unsupported DSH derived object schema")
326
+ compatibility = stored.get("compatibility")
327
+ if not isinstance(compatibility, dict) or compatibility.get("version") != "prime-agent-dsh/durable-store-v3-reference":
328
+ raise RuntimeError("invalid DSH durable context object")
329
+ binding = _read_json(self.root / "BINDING")
330
+ if binding.get("version") != "prime-agent-dsh/durable-store-v3-reference":
331
+ raise RuntimeError("unsupported DSH durable store schema")
332
+ prime_raw = binding.get("primeSessionFile")
333
+ if not isinstance(prime_raw, str) or not os.path.isabs(prime_raw):
334
+ raise RuntimeError("invalid DSH Prime binding")
335
+ prime_path = Path(prime_raw)
336
+ if prime_path.is_symlink() or not prime_path.is_file():
337
+ raise RuntimeError("missing or unsafe bound Prime JSONL")
338
+ prime = prime_path.read_bytes()
339
+ digests = stored.get("sourceEntryDigests")
340
+ locators = stored.get("sourceLocators")
341
+ if not isinstance(digests, list) or not isinstance(locators, list) or len(digests) != len(locators):
342
+ raise RuntimeError("invalid DSH source locators")
343
+ source: list[Any] = []
344
+ for index, (entry_digest, locator) in enumerate(zip(digests, locators)):
345
+ if (not isinstance(entry_digest, str) or not re.fullmatch(r"[a-f0-9]{64}", entry_digest)
346
+ or not isinstance(locator, dict) or locator.get("index") != index
347
+ or locator.get("entryDigest") != entry_digest):
348
+ raise RuntimeError("invalid DSH source locator")
349
+ offset, length = locator.get("byteOffset"), locator.get("byteLength")
350
+ if (not isinstance(offset, int) or isinstance(offset, bool) or offset < 0
351
+ or not isinstance(length, int) or isinstance(length, bool) or length <= 0
352
+ or offset + length > len(prime)):
353
+ raise RuntimeError("invalid DSH source locator bounds")
354
+ end = offset + length
355
+ if ((offset > 0 and prime[offset - 1] != 0x0A)
356
+ or (end < len(prime) and prime[end] != 0x0A
357
+ and not (prime[end] == 0x0D and end + 1 < len(prime) and prime[end + 1] == 0x0A))):
358
+ raise RuntimeError("DSH source locator is not a complete JSONL line")
359
+ try:
360
+ value = json.loads(prime[offset:end])
361
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
362
+ raise RuntimeError("invalid located Prime JSONL entry") from error
363
+ if sha256(_canonical_json(value)).hexdigest() != entry_digest:
364
+ raise RuntimeError("bound Prime JSONL entry digest mismatch")
365
+ entry_id = locator.get("entryId")
366
+ if entry_id is not None and (not isinstance(value, dict) or value.get("id") != entry_id):
367
+ raise RuntimeError("bound Prime JSONL entry id mismatch")
368
+ source.append(value)
369
+ if sha256(_canonical_json(source)).hexdigest() != stored.get("sourceDigest"):
370
+ raise RuntimeError("DSH source aggregate digest mismatch")
371
+ refs = stored.get("effectiveReferences")
372
+ effective_digests = stored.get("effectiveEntryDigests")
373
+ if not isinstance(refs, list) or not isinstance(effective_digests, list) or len(refs) != len(effective_digests):
374
+ raise RuntimeError("invalid DSH effective references")
375
+ def context_message(entry: Any) -> dict[str, Any] | None:
376
+ if not isinstance(entry, dict): return None
377
+ kind = entry.get("type")
378
+ if kind == "message" and isinstance(entry.get("message"), dict): return dict(entry["message"])
379
+ if kind == "custom_message":
380
+ return {"role": "custom", "customType": entry.get("customType", ""), "content": entry.get("content", []),
381
+ "display": entry.get("display", False), "details": entry.get("details"), "timestamp": entry.get("timestamp")}
382
+ if kind == "branch_summary" and isinstance(entry.get("summary"), str):
383
+ return {"role": "branchSummary", "summary": entry["summary"], "fromId": entry.get("fromId"), "timestamp": entry.get("timestamp")}
384
+ if kind == "compaction" and isinstance(entry.get("summary"), str):
385
+ return {"role": "compactionSummary", "summary": entry["summary"], "tokensBefore": entry.get("tokensBefore", 0), "timestamp": entry.get("timestamp")}
386
+ return None
387
+ messages: list[Any] = []
388
+ unavailable = 0
389
+ for reference, expected_digest in zip(refs, effective_digests):
390
+ if (not isinstance(expected_digest, str) or not re.fullmatch(r"[a-f0-9]{64}", expected_digest)
391
+ or not isinstance(reference, dict) or reference.get("entryDigest") != expected_digest):
392
+ raise RuntimeError("invalid DSH effective reference")
393
+ source_index = reference.get("sourceIndex")
394
+ if source_index is None:
395
+ unavailable += 1
396
+ continue
397
+ if not isinstance(source_index, int) or isinstance(source_index, bool) or not 0 <= source_index < len(source):
398
+ raise RuntimeError("invalid DSH effective source index")
399
+ message = context_message(source[source_index])
400
+ if message is None:
401
+ unavailable += 1
402
+ continue
403
+ rendered = dict(message)
404
+ if isinstance(rendered.get("content"), str):
405
+ rendered["content"] = [{"type": "text", "text": rendered["content"]}]
406
+ messages.append(rendered)
407
+ def entry_text(value: Any) -> str:
408
+ if isinstance(value, dict):
409
+ message = value.get("message")
410
+ candidate = message if isinstance(message, dict) else value
411
+ content = candidate.get("content")
412
+ if isinstance(content, str): return content
413
+ if isinstance(content, list): return _message_text({"content": content})
414
+ if isinstance(candidate.get("summary"), str): return candidate["summary"]
415
+ return json.dumps(value, ensure_ascii=False, sort_keys=True)
416
+ entries = [{**value, "index": index, "text": entry_text(value)} if isinstance(value, dict)
417
+ else {"index": index, "text": entry_text(value), "value": value}
418
+ for index, value in enumerate(source)]
419
+ if compatibility.get("messageCount") != len(refs) or compatibility.get("entries") != [] or "messages" in compatibility:
420
+ raise RuntimeError("invalid DSH reference-only view")
421
+ data = {**compatibility, "version": _VERSION, "entries": entries, "messages": messages,
422
+ "unavailableEffectiveEntries": unavailable,
423
+ "metrics": compatibility.get("metrics", manifest.get("metrics", {}))}
424
+ snapshot = ContextSnapshot(self.root, manifest, data)
425
+ if expected_branch_id is not None and snapshot.branch_id != expected_branch_id:
426
+ raise RuntimeError("DSH context snapshot branch mismatch")
427
+ return snapshot
428
+
429
+ def entries(self, *args: Any, **kwargs: Any) -> ContextSelection:
430
+ return self.snapshot().entries(*args, **kwargs)
431
+
432
+ def messages(self, *args: Any, **kwargs: Any) -> ContextSelection:
433
+ return self.snapshot().messages(*args, **kwargs)
434
+
435
+ def search(self, *args: Any, **kwargs: Any) -> ContextSelection:
436
+ return self.snapshot().search(*args, **kwargs)
437
+
438
+ @property
439
+ def metrics(self) -> dict[str, Any]:
440
+ return dict(self.snapshot().metrics)
441
+
442
+ def artifact(self, value: Any, label: str = "context") -> ContextArtifact:
443
+ if not isinstance(label, str) or not label.strip():
444
+ raise ValueError("label must be a non-empty string")
445
+ body = f"# {label.strip()}\n\n{_render(value)}\n"
446
+ raw = body.encode("utf-8")
447
+ if len(raw) > _MAX_ARTIFACT_BYTES:
448
+ raise ValueError(f"artifact exceeds {_MAX_ARTIFACT_BYTES} bytes")
449
+ digest = sha256(raw).hexdigest()
450
+ directory = self.root / "artifacts"
451
+ _private_directory(directory)
452
+ path = directory / f"{digest}.md"
453
+ if path.exists() or path.is_symlink():
454
+ if path.is_symlink() or not path.is_file() or sha256(path.read_bytes()).hexdigest() != digest:
455
+ raise RuntimeError(f"unsafe existing context artifact: {path}")
456
+ else:
457
+ _exclusive_write(path, raw)
458
+ return ContextArtifact(path=path, digest=digest, bytes=len(raw))
459
+
460
+ def inject(self, value: Any, label: str = "Selected DSH context", max_bytes: int = _MAX_INJECT_BYTES) -> str:
461
+ """Return bounded context text; print/return it so Prime logs the IPython result."""
462
+ if not isinstance(label, str) or not label.strip():
463
+ raise ValueError("label must be a non-empty string")
464
+ if not isinstance(max_bytes, int) or isinstance(max_bytes, bool) or max_bytes <= 0 or max_bytes > _MAX_INJECT_BYTES:
465
+ raise ValueError(f"max_bytes must be between 1 and {_MAX_INJECT_BYTES}")
466
+ rendered = _crop_utf8(_render(value), max_bytes)
467
+ return f"<dsh-context label={json.dumps(label.strip(), ensure_ascii=False)}>\n{rendered}\n</dsh-context>"
468
+
469
+ admit = inject
470
+
471
+ def grant(
472
+ self,
473
+ value: Any,
474
+ label: str = "Shared parent context",
475
+ *,
476
+ ttl_seconds: int = 24 * 60 * 60,
477
+ ) -> ContextGrant:
478
+ """Create a bounded read-only grant that a descendant can open by token."""
479
+ if not isinstance(label, str) or not label.strip():
480
+ raise ValueError("label must be a non-empty string")
481
+ if not isinstance(ttl_seconds, int) or isinstance(ttl_seconds, bool) or ttl_seconds <= 0 or ttl_seconds > _MAX_GRANT_TTL_SECONDS:
482
+ raise ValueError(f"ttl_seconds must be between 1 and {_MAX_GRANT_TTL_SECONDS}")
483
+ selection_digest = value.digest if isinstance(value, ContextSelection) else None
484
+ snapshot = self.snapshot(selection_digest)
485
+ now = int(time.time())
486
+ token = secrets.token_urlsafe(32)
487
+ record = {
488
+ "version": _GRANT_VERSION,
489
+ "token": token,
490
+ "label": label.strip(),
491
+ "sourceSessionId": snapshot.session_id,
492
+ "sourceBranchId": snapshot.branch_id,
493
+ "sourceSnapshotDigest": snapshot.digest,
494
+ "createdAt": now,
495
+ "expiresAt": now + ttl_seconds,
496
+ "value": _jsonable(value),
497
+ }
498
+ payload = _canonical_json(record)
499
+ if len(payload) > _MAX_GRANT_BYTES:
500
+ raise ValueError(f"context grant exceeds {_MAX_GRANT_BYTES} bytes")
501
+ digest = sha256(payload).hexdigest()
502
+ encoded = _canonical_json({**record, "digest": digest}) + b"\n"
503
+ directory = self.root / "grants"
504
+ _private_directory(directory)
505
+ _exclusive_write(directory / f"{token}.json", encoded)
506
+ return ContextGrant(token=token, digest=digest, expires_at=record["expiresAt"], label=label.strip())
507
+
508
+ def __repr__(self) -> str:
509
+ try:
510
+ return f"ContextHandle({self.snapshot()!r})"
511
+ except Exception as error:
512
+ return f"ContextHandle(unavailable={error!r})"
513
+
514
+
515
+ def _grant_token(value: str) -> str:
516
+ prefix = "dsh-context-grant:"
517
+ token = value[len(prefix) :] if value.startswith(prefix) else value
518
+ if not _GRANT_TOKEN.fullmatch(token):
519
+ raise ValueError("invalid DSH context grant token")
520
+ return token
521
+
522
+
523
+ def open_grant(value: str) -> GrantedContext:
524
+ """Open a capability grant from this session or one of its ancestors."""
525
+ token = _grant_token(value)
526
+ for ancestor in (_session_dir(), *_session_dir().parents):
527
+ candidate = ancestor / "dsh-context" / "grants" / f"{token}.json"
528
+ if not candidate.exists() and not candidate.is_symlink():
529
+ continue
530
+ if candidate.is_symlink() or not candidate.is_file():
531
+ raise RuntimeError(f"unsafe DSH context grant path: {candidate}")
532
+ record = _read_json(candidate)
533
+ if record.get("version") != _GRANT_VERSION or record.get("token") != token:
534
+ raise RuntimeError("invalid DSH context grant")
535
+ supplied = record.get("digest")
536
+ unsigned = {key: item for key, item in record.items() if key != "digest"}
537
+ if not isinstance(supplied, str) or sha256(_canonical_json(unsigned)).hexdigest() != supplied:
538
+ raise RuntimeError("DSH context grant digest mismatch")
539
+ expires_at = record.get("expiresAt")
540
+ if not isinstance(expires_at, int) or expires_at < int(time.time()):
541
+ raise RuntimeError("DSH context grant has expired")
542
+ required = ("label", "sourceSessionId", "sourceBranchId", "sourceSnapshotDigest", "createdAt")
543
+ if any(not isinstance(record.get(key), (str if key != "createdAt" else int)) for key in required):
544
+ raise RuntimeError("invalid DSH context grant fields")
545
+ return GrantedContext(
546
+ token=token,
547
+ label=record["label"],
548
+ source_session_id=record["sourceSessionId"],
549
+ source_branch_id=record["sourceBranchId"],
550
+ source_snapshot_digest=record["sourceSnapshotDigest"],
551
+ created_at=record["createdAt"],
552
+ expires_at=expires_at,
553
+ value=record.get("value"),
554
+ )
555
+ raise FileNotFoundError(f"DSH context grant {token!r} is not reachable from this session")
556
+
557
+
558
+ def current() -> ContextHandle:
559
+ """Return the current Prime session's lazy DSH context handle."""
560
+ return ContextHandle(_private_root())
561
+
562
+
563
+ __all__ = [
564
+ "ContextArtifact",
565
+ "ContextGrant",
566
+ "ContextHandle",
567
+ "ContextItem",
568
+ "ContextSelection",
569
+ "ContextSnapshot",
570
+ "GrantedContext",
571
+ "current",
572
+ "open_grant",
573
+ ]