@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.
- package/NOTICE +7 -0
- package/package.json +1 -1
- package/server/NOTICE +7 -0
- package/server/VERSION +1 -1
- package/server/agent_server.py +1605 -493
- package/server/agentsdock_jobs.py +2 -2
- package/server/claude_history_repair.py +46 -61
- package/server/codex_auth.py +76 -7
- package/server/codex_history_repair.py +203 -38
- package/server/cursor_process_guard.py +67 -4
- package/server/cursor_provider_mcp.py +2 -0
- package/server/execution_durability.py +87 -11
- package/server/execution_install.py +5 -1
- package/server/gemini_acp_client.py +432 -0
- package/server/gemini_api.py +128 -0
- package/server/gemini_content.py +102 -0
- package/server/gemini_history.py +293 -0
- package/server/gemini_import.py +126 -0
- package/server/gemini_permissions.py +47 -0
- package/server/gemini_runner.py +493 -0
- package/server/gemini_runtime.py +322 -0
- package/server/install.sh +11 -2
- package/server/interactive_chat_controls.py +9 -3
- package/server/interactive_chat_native.py +1 -1
- package/server/interactive_chat_share_routes.py +1 -1
- package/server/interactive_chat_share_web.py +2 -2
- package/server/local_session_ownership.py +1 -0
- package/server/provider_commands.py +65 -0
- package/server/provider_connections.py +39 -10
- package/server/server_instances.py +217 -15
- package/server/title_generation.py +60 -1
- package/server/transcript_reader.py +334 -0
- package/server/update_runner.py +4 -2
|
@@ -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
|
package/server/update_runner.py
CHANGED
|
@@ -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
|
|
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
|
|
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"
|