@agentsdock/server 1.0.10 → 1.0.11-beta.2

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,334 @@
1
+ """Bounded-memory projection of native JSONL, with exact raw-byte receipts.
2
+
3
+ Opaque media/tool fields stay in the provider-owned file. This reader validates
4
+ all JSON, including skipped strings, but only materializes history metadata and
5
+ text. Bounds apply to retained JSON/work, never to opaque payload/file bytes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import codecs
10
+ import json
11
+ import re
12
+ import time
13
+ from dataclasses import dataclass
14
+ from typing import BinaryIO, Callable
15
+
16
+ CHUNK_BYTES = 64 * 1024
17
+ MAX_RETAINED_BYTES = 32 * 1024 * 1024
18
+ MAX_DEPTH = 96
19
+ MAX_NODES = 250_000
20
+ MAX_KEY_BYTES = 4096
21
+ MAX_SCALAR_BYTES = 1024
22
+ SCAN_SECONDS = 60.0
23
+ _SPECIAL = re.compile(rb'["\\\x00-\x1f]')
24
+ _NUMBER = re.compile(rb'-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\Z')
25
+ _HEX = frozenset(b'0123456789abcdefABCDEF')
26
+
27
+
28
+ class TranscriptBudgetExceeded(ValueError):
29
+ """Never treat exhausted work/memory as an ignorable malformed record."""
30
+
31
+ def at_record(self, number: int, start: int):
32
+ # A delta scan starts inside the file; do not call this a file line or
33
+ # a user-visible chat message number. Never include transcript content.
34
+ return TranscriptBudgetExceeded(
35
+ f'{self} Record {number:,} in this scan (source byte offset {start:,}).')
36
+
37
+
38
+ class InvalidRecord(ValueError):
39
+ pass
40
+
41
+
42
+ def scan_check(deadline: float, cancelled: Callable[[], bool] | None = None,
43
+ *, budget_seconds: float | None = None):
44
+ if cancelled is not None and cancelled():
45
+ raise TranscriptBudgetExceeded('transcript scan cancelled')
46
+ if time.monotonic() >= deadline:
47
+ limit = f' ({budget_seconds:g} seconds)' if budget_seconds is not None else ''
48
+ raise TranscriptBudgetExceeded(
49
+ f'Transcript scan exceeded its processing time budget{limit}. '
50
+ 'Reading stopped before completion; this is a processing timeout, not a file-size limit.')
51
+
52
+
53
+ def byte_amount(size: int) -> str:
54
+ mib = f' ({size / (1024 * 1024):.2f} MiB)' if size >= 1024 * 1024 else ''
55
+ return f'{size:,} bytes{mib}'
56
+
57
+
58
+ def opaque_path(path: tuple) -> bool:
59
+ # These fields never feed text_from_content, native user/assistant matching,
60
+ # ownership/turn metadata or goal/wake classification. Do not skip arbitrary
61
+ # `content`, `text`, `message`, thinking or unknown fields.
62
+ if path == ('payload', 'output'):
63
+ return True # Codex function/custom tool output, not assistant text.
64
+ for prefix in (('message', 'content'), ('payload', 'content')):
65
+ if len(path) > 3 and path[:2] == prefix and isinstance(path[2], int):
66
+ tail = path[3:]
67
+ if prefix == ('message', 'content') and tail in (('content',), ('input',)):
68
+ return True # Claude tool result/body and tool invocation arguments.
69
+ if tail in (('source', 'data'), ('image_url',), ('image_url', 'url')):
70
+ return True
71
+ return False
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class RawRecord:
76
+ stream: BinaryIO
77
+ start: int
78
+ end: int
79
+ complete: bool
80
+ blank: bool
81
+ malformed: bool
82
+ deadline: float
83
+ cancelled: Callable[[], bool] | None = None
84
+ number: int = 1
85
+ budget_seconds: float | None = None
86
+
87
+ def __len__(self):
88
+ return self.end - self.start
89
+
90
+ def update_hash(self, digest):
91
+ # Restore the parser's stream position; this also works on Windows,
92
+ # where pread is unavailable. Each iterator owns this file handle.
93
+ saved = self.stream.tell()
94
+ try:
95
+ self.stream.seek(self.start)
96
+ remaining = len(self)
97
+ while remaining:
98
+ scan_check(self.deadline, self.cancelled, budget_seconds=self.budget_seconds)
99
+ chunk = self.stream.read(min(CHUNK_BYTES, remaining))
100
+ if not chunk:
101
+ raise ValueError('transcript was truncated during record hashing')
102
+ digest.update(chunk)
103
+ remaining -= len(chunk)
104
+ except TranscriptBudgetExceeded as error:
105
+ raise error.at_record(self.number, self.start) from error
106
+ finally:
107
+ self.stream.seek(saved)
108
+
109
+
110
+
111
+ class _RecordParser:
112
+ def __init__(self, stream, end, deadline, cancelled, project, maximum, budget_seconds, retained_fields=None, max_nodes=-1, max_depth=None):
113
+ self.stream, self.end = stream, end
114
+ self.deadline, self.cancelled, self.project = deadline, cancelled, project
115
+ self.buffer, self.index, self.final, self.complete = b'', 0, False, False
116
+ self.retained, self.nodes, self.maximum = 0, 0, maximum
117
+ self.budget_seconds = budget_seconds
118
+ self.retained_fields = retained_fields
119
+ self.max_nodes = MAX_NODES if max_nodes == -1 else max_nodes
120
+ self.max_depth = MAX_DEPTH if max_depth is None else max_depth
121
+
122
+ def fill(self):
123
+ if self.index < len(self.buffer):
124
+ return True
125
+ if self.final:
126
+ return False
127
+ scan_check(self.deadline, self.cancelled, budget_seconds=self.budget_seconds)
128
+ remaining = self.end - self.stream.tell()
129
+ self.buffer = self.stream.readline(min(CHUNK_BYTES, remaining)) if remaining > 0 else b''
130
+ self.index = 0
131
+ self.complete = self.buffer.endswith(b'\n')
132
+ self.final = self.complete or not self.buffer or self.stream.tell() >= self.end
133
+ return bool(self.buffer)
134
+
135
+ def peek(self):
136
+ return self.buffer[self.index] if self.fill() else None
137
+
138
+ def take(self):
139
+ value = self.peek()
140
+ if value is None:
141
+ raise InvalidRecord('incomplete JSON record')
142
+ self.index += 1
143
+ return value
144
+
145
+ def space(self):
146
+ while self.peek() in (9, 10, 13, 32):
147
+ self.index += 1
148
+
149
+ def reserve(self, size):
150
+ self.retained += size
151
+ if self.retained > self.maximum:
152
+ raise TranscriptBudgetExceeded(
153
+ 'Single transcript record retained text/metadata exceeds its import budget: '
154
+ f'observed at least {byte_amount(self.retained)}; limit {byte_amount(self.maximum)}. '
155
+ 'This includes accounting overhead, not the total conversation file size.')
156
+
157
+ def check_key_size(self, size):
158
+ if size > MAX_KEY_BYTES:
159
+ raise TranscriptBudgetExceeded(
160
+ 'Single transcript record contains an oversized JSON field name: '
161
+ f'observed at least {byte_amount(size)}; limit {byte_amount(MAX_KEY_BYTES)}.')
162
+
163
+ def string(self, retain, key=False):
164
+ if self.take() != 34:
165
+ raise InvalidRecord('JSON string expected')
166
+ parts = bytearray(b'"') if retain else None
167
+ decoder = codecs.getincrementaldecoder('utf-8')()
168
+ size = 0
169
+ while self.fill():
170
+ match = _SPECIAL.search(self.buffer, self.index)
171
+ stop = match.start() if match else len(self.buffer)
172
+ piece = self.buffer[self.index:stop]
173
+ decoder.decode(piece, final=False)
174
+ if retain:
175
+ self.reserve(len(piece)); parts.extend(piece)
176
+ size += len(piece)
177
+ if key:
178
+ self.check_key_size(size)
179
+ self.index = stop
180
+ if match is None:
181
+ continue
182
+ special = self.take()
183
+ if special == 34:
184
+ decoder.decode(b'', final=True)
185
+ if retain:
186
+ parts.append(34)
187
+ return json.loads(parts)
188
+ return None
189
+ if special != 92:
190
+ raise InvalidRecord('control character in JSON string')
191
+ # Escapes are ASCII boundaries: a partial UTF-8 codepoint here is
192
+ # invalid, including in a discarded field.
193
+ decoder.decode(b'', final=True)
194
+ decoder = codecs.getincrementaldecoder('utf-8')()
195
+ escape = self.take()
196
+ raw = bytes((92, escape))
197
+ if escape == 117:
198
+ suffix = bytes(self.take() for _ in range(4))
199
+ if any(value not in _HEX for value in suffix):
200
+ raise InvalidRecord('invalid JSON Unicode escape')
201
+ raw += suffix
202
+ elif escape not in b'"\\/bfnrt':
203
+ raise InvalidRecord('invalid JSON escape')
204
+ if retain:
205
+ self.reserve(len(raw)); parts.extend(raw)
206
+ size += len(raw)
207
+ if key:
208
+ self.check_key_size(size)
209
+ raise InvalidRecord('unterminated JSON string')
210
+
211
+ def value(self, path=(), retain=True, depth=0):
212
+ self.nodes += 1
213
+ if depth > self.max_depth:
214
+ raise TranscriptBudgetExceeded(
215
+ 'Single transcript record exceeds the JSON nesting limit: '
216
+ f'observed depth {depth:,}; limit {self.max_depth:,}.')
217
+ if self.max_nodes is not None and self.nodes > self.max_nodes:
218
+ raise TranscriptBudgetExceeded(
219
+ 'Single transcript record exceeds the JSON value-count limit: '
220
+ f'observed at least {self.nodes:,} values; limit {self.max_nodes:,}.')
221
+ self.space()
222
+ if path and self.retained_fields is not None and path[0] not in self.retained_fields:
223
+ retain = False
224
+ if self.project and opaque_path(path):
225
+ retain = False
226
+ if retain:
227
+ self.reserve(64) # Bound object overhead as well as string bytes.
228
+ first = self.peek()
229
+ if first == 34:
230
+ return self.string(retain)
231
+ if first in (123, 91):
232
+ self.take()
233
+ obj = first == 123
234
+ result = ({} if obj else []) if retain else None
235
+ close = 125 if obj else 93
236
+ self.space()
237
+ if self.peek() == close:
238
+ self.take(); return result
239
+ index = 0
240
+ while True:
241
+ if obj:
242
+ self.space()
243
+ # Explicit metadata projection does not need keys inside
244
+ # discarded subtrees. Do not charge transient key bytes as
245
+ # retained metadata (native import behavior is unchanged).
246
+ keep_key = retain or self.retained_fields is None or self.project
247
+ key = self.string(keep_key, key=True)
248
+ self.space()
249
+ if self.take() != 58:
250
+ raise InvalidRecord('JSON colon expected')
251
+ else:
252
+ key = index
253
+ value = self.value(path+(key,), retain, depth+1)
254
+ if retain:
255
+ if obj: result[key] = value
256
+ else: result.append(value)
257
+ self.space()
258
+ delimiter = self.take()
259
+ if delimiter == close:
260
+ return result
261
+ if delimiter != 44:
262
+ raise InvalidRecord('JSON comma expected')
263
+ index += 1
264
+ token = bytearray()
265
+ while self.peek() is not None and self.peek() not in b' \r\n\t,]}':
266
+ token.append(self.take())
267
+ if len(token) > MAX_SCALAR_BYTES:
268
+ raise TranscriptBudgetExceeded(
269
+ 'Single transcript record contains an oversized JSON scalar (number or literal): '
270
+ f'observed at least {byte_amount(len(token))}; limit {byte_amount(MAX_SCALAR_BYTES)}.')
271
+ raw = bytes(token)
272
+ if raw not in (b'true', b'false', b'null', b'NaN', b'Infinity', b'-Infinity') and not _NUMBER.fullmatch(raw):
273
+ raise InvalidRecord('invalid JSON scalar')
274
+ return json.loads(raw) if retain else None
275
+
276
+ def drain(self):
277
+ while self.fill():
278
+ self.index = len(self.buffer)
279
+
280
+
281
+ def jsonl_records(stream: BinaryIO, *, end: int, project=True, strict=False,
282
+ require_newline=True, max_records=250_000, deadline=None,
283
+ cancelled=None, max_retained_bytes=MAX_RETAINED_BYTES, retained_fields=None,
284
+ max_nodes=-1, max_depth=None):
285
+ """Yield (projected object or None, ending offset, RawRecord).
286
+
287
+ Malformed provider lines may be represented by None, as before. Resource
288
+ exhaustion always raises, so callers never commit a partial-success cursor.
289
+ A fixed end excludes concurrent appends; callers own file identity checks.
290
+ Native-import work budgets remain the default. Durable metadata-only scans
291
+ can supply separate structural limits without imposing import caps on logs.
292
+ """
293
+ # Externally supplied deadlines can belong to a larger operation. Do not
294
+ # invent a duration for them; the caller can provide its own timeout detail.
295
+ budget_seconds = SCAN_SECONDS if deadline is None else None
296
+ deadline = time.monotonic() + SCAN_SECONDS if deadline is None else deadline
297
+ count = 0
298
+ while stream.tell() < end:
299
+ count += 1
300
+ start = stream.tell()
301
+ if count > max_records:
302
+ raise TranscriptBudgetExceeded(
303
+ 'Transcript scan exceeds the record-count limit: '
304
+ f'observed at least {count:,} records; limit {max_records:,}. '
305
+ 'This counts JSONL records, not chat messages.').at_record(count, start)
306
+ parser = _RecordParser(stream, end, deadline, cancelled, project, max_retained_bytes, budget_seconds, retained_fields, max_nodes, max_depth)
307
+ blank = False
308
+ malformed = False
309
+ try:
310
+ try:
311
+ parser.space()
312
+ blank = parser.peek() is None
313
+ value = None if blank else parser.value()
314
+ parser.space()
315
+ if parser.peek() is not None:
316
+ raise InvalidRecord('trailing JSON content')
317
+ except (InvalidRecord, UnicodeDecodeError, json.JSONDecodeError) as error:
318
+ if strict:
319
+ raise ValueError('malformed transcript record') from error
320
+ parser.drain()
321
+ malformed = True
322
+ value = None
323
+ except TranscriptBudgetExceeded as error:
324
+ raise error.at_record(count, start) from error
325
+ stop = stream.tell()
326
+ if stop <= start:
327
+ raise ValueError('transcript was truncated during reading')
328
+ if require_newline and not parser.complete:
329
+ raise ValueError('transcript cursor does not end on a complete line')
330
+ raw = RawRecord(stream, start, stop, parser.complete, blank, malformed or blank,
331
+ deadline, cancelled, count, budget_seconds)
332
+ if strict and not isinstance(value, dict):
333
+ raise ValueError('transcript object expected')
334
+ yield value if isinstance(value, dict) else None, stop, raw
@@ -555,18 +555,20 @@ def server_work_snapshot(
555
555
  "server health response is missing its service-cgroup admission proof"
556
556
  )
557
557
  raw_count = cgroup.get("unknown_descendant_count")
558
+ inspection = cgroup.get("inspection")
558
559
  if (
559
560
  cgroup.get("safe") is not True
560
561
  or isinstance(raw_count, bool)
561
562
  or not isinstance(raw_count, int)
562
- or raw_count != 0
563
+ or raw_count < 0
564
+ or (raw_count != 0 and inspection != "verified-preserved")
563
565
  ):
564
566
  raise RuntimeError(
565
567
  "AgentsServer has an unverified or nonempty service cgroup"
566
568
  )
567
569
  if (
568
570
  require_verified_service_cgroup
569
- and cgroup.get("inspection") != "verified"
571
+ and inspection not in {"verified", "verified-preserved"}
570
572
  ):
571
573
  raise RuntimeError(
572
574
  "AgentsServer did not return a verified systemd service-cgroup proof"