@agentsdock/server 1.0.10 → 1.0.11-beta.1
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/package.json +1 -1
- package/server/VERSION +1 -1
- package/server/agent_server.py +189 -185
- package/server/claude_history_repair.py +46 -61
- package/server/codex_auth.py +28 -7
- package/server/codex_history_repair.py +23 -23
- package/server/install.sh +3 -2
- package/server/server_instances.py +22 -0
- package/server/transcript_reader.py +322 -0
|
@@ -14,6 +14,7 @@ import hashlib
|
|
|
14
14
|
import hmac
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
|
+
import time
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
import re
|
|
19
20
|
import stat
|
|
@@ -26,9 +27,13 @@ from claude_goals import ClaudeGoalHistoryNormalizer, is_claude_synthetic_no_res
|
|
|
26
27
|
from claude_sdk_client import CLAUDE_SDK_LITERAL_MESSAGE_PREFIX
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
from transcript_reader import jsonl_records, scan_check, CHUNK_BYTES, SCAN_SECONDS
|
|
31
|
+
|
|
29
32
|
MAX_BYTES = 96 * 1024 * 1024
|
|
30
33
|
MAX_EVENTS_BYTES = 32 * 1024 * 1024
|
|
31
|
-
|
|
34
|
+
# Bound retained JSON; opaque provider payload bytes are streamed.
|
|
35
|
+
MAX_LINE_BYTES = 32 * 1024 * 1024
|
|
36
|
+
MAX_TEXT_CHARS = 4 * 1024 * 1024
|
|
32
37
|
MAX_RECORDS = 100_000
|
|
33
38
|
MAX_KEYS = 20_000
|
|
34
39
|
MAX_TARGETS = 4_000
|
|
@@ -73,24 +78,15 @@ def _records(path: Path, expected: tuple[int, int, int, int]):
|
|
|
73
78
|
info = os.fstat(stream.fileno())
|
|
74
79
|
if (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns) != expected:
|
|
75
80
|
raise _Unproven()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
raise _Unproven()
|
|
82
|
-
count += 1
|
|
83
|
-
if count > MAX_RECORDS:
|
|
84
|
-
raise _Unproven()
|
|
85
|
-
offset += len(line)
|
|
86
|
-
event = json.loads(line)
|
|
87
|
-
if not isinstance(event, dict):
|
|
88
|
-
raise _Unproven()
|
|
89
|
-
yield event, offset, line
|
|
81
|
+
try:
|
|
82
|
+
yield from jsonl_records(stream, end=expected[2], strict=True,
|
|
83
|
+
max_records=MAX_RECORDS, max_retained_bytes=MAX_LINE_BYTES)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise _Unproven() from exc
|
|
90
86
|
final = os.fstat(stream.fileno())
|
|
91
87
|
if (final.st_dev, final.st_ino, final.st_size, final.st_mtime_ns) != expected:
|
|
92
88
|
raise _Unproven()
|
|
93
|
-
if
|
|
89
|
+
if _regular_stamp(path) != expected:
|
|
94
90
|
raise _Unproven()
|
|
95
91
|
|
|
96
92
|
|
|
@@ -106,7 +102,7 @@ def _target(event: dict) -> tuple[int, str, str] | None:
|
|
|
106
102
|
event.get("type") != "turn_started" or event.get("imported") is not True
|
|
107
103
|
or event.get("backend") != "claude" or type(seq) is not int or seq <= 0
|
|
108
104
|
or not isinstance(run, str) or not run.startswith("import_")
|
|
109
|
-
or not isinstance(prompt, str) or len(prompt) >
|
|
105
|
+
or not isinstance(prompt, str) or len(prompt) > MAX_TEXT_CHARS
|
|
110
106
|
):
|
|
111
107
|
return None
|
|
112
108
|
return seq, run, _text_key(prompt)
|
|
@@ -118,7 +114,7 @@ def _assistant_target(event: dict) -> tuple | None:
|
|
|
118
114
|
if (event.get("type") not in ("assistant_text", "reasoning_summary")
|
|
119
115
|
or event.get("imported") is not True or event.get("backend") != "claude"
|
|
120
116
|
or type(seq) is not int or seq <= 0 or not isinstance(run, str) or not run.startswith("import_")
|
|
121
|
-
or not isinstance(text, str) or not text.strip() or len(text) >
|
|
117
|
+
or not isinstance(text, str) or not text.strip() or len(text) > MAX_TEXT_CHARS
|
|
122
118
|
or not isinstance(origin, dict) or origin.get("provider") != "claude"
|
|
123
119
|
or origin.get("kind") not in (None, "assistant")
|
|
124
120
|
or event.get("provider_user_authored") is True
|
|
@@ -173,7 +169,7 @@ class _AssistantReplays:
|
|
|
173
169
|
return None
|
|
174
170
|
display = self.normalize_assistant(text)
|
|
175
171
|
return (_text_key(display) if isinstance(display, str) and display.strip()
|
|
176
|
-
and len(display) <=
|
|
172
|
+
and len(display) <= MAX_TEXT_CHARS else None)
|
|
177
173
|
|
|
178
174
|
def event(self, event: dict) -> None:
|
|
179
175
|
target = _assistant_target(event)
|
|
@@ -193,7 +189,7 @@ class _AssistantReplays:
|
|
|
193
189
|
and (event.get("phase") in ("commentary", "final_answer")
|
|
194
190
|
or (kind == "assistant_text" and event.get("phase") is None))
|
|
195
191
|
and isinstance(event.get("text"), str) and event["text"].strip()
|
|
196
|
-
and len(event["text"]) <=
|
|
192
|
+
and len(event["text"]) <= MAX_TEXT_CHARS):
|
|
197
193
|
self.native.append((event, _text_key(event["text"].strip())))
|
|
198
194
|
if sum(map(len, (self.starts, self.ends, self.owners, self.native, self.candidates))) > MAX_TARGETS:
|
|
199
195
|
raise _Unproven()
|
|
@@ -260,7 +256,7 @@ class _AssistantReplays:
|
|
|
260
256
|
and block.get("type") == "text" and isinstance(block.get("text"), str))
|
|
261
257
|
else:
|
|
262
258
|
return
|
|
263
|
-
if text.strip() and len(text) <=
|
|
259
|
+
if text.strip() and len(text) <= MAX_TEXT_CHARS:
|
|
264
260
|
digest = _text_key(text.strip())
|
|
265
261
|
self.sources.setdefault((identity, digest), []).append(offset)
|
|
266
262
|
self.source_display[(identity, digest)] = self.display_key(text)
|
|
@@ -440,7 +436,7 @@ class _AsyncDeliveryInputs:
|
|
|
440
436
|
or any(event.get(key) for key in ("clientUserMessageId", "clientId", "client_user_message_id", "client_id"))):
|
|
441
437
|
return
|
|
442
438
|
full, display = self.normalize_full_user(event), self.normalize_user(event)
|
|
443
|
-
match = _ASYNC_WRAPPER.fullmatch(full.strip()) if isinstance(full, str) and len(full) <=
|
|
439
|
+
match = _ASYNC_WRAPPER.fullmatch(full.strip()) if isinstance(full, str) and len(full) <= MAX_TEXT_CHARS else None
|
|
444
440
|
timestamp = _timestamp(identity[2])
|
|
445
441
|
if match and isinstance(display, str) and display and timestamp is not None:
|
|
446
442
|
self.sources.setdefault((match[1], _text_key(match[2]), len(match[2]), int(timestamp)), []).append(
|
|
@@ -519,7 +515,7 @@ class _MailboxWakeInputs(_AsyncDeliveryInputs):
|
|
|
519
515
|
return
|
|
520
516
|
full, display = self.normalize_full_user(event), self.normalize_user(event)
|
|
521
517
|
timestamp = _timestamp(identity[2])
|
|
522
|
-
if (isinstance(full, str) and full and len(full) <=
|
|
518
|
+
if (isinstance(full, str) and full and len(full) <= MAX_TEXT_CHARS
|
|
523
519
|
and _text_key(full) in self.wake_hashes
|
|
524
520
|
and isinstance(display, str) and display and timestamp is not None):
|
|
525
521
|
self.sources.setdefault(_text_key(full), []).append(
|
|
@@ -631,29 +627,18 @@ def filter_native_claude_mailbox_wake_items(
|
|
|
631
627
|
"backend": "claude", "imported": True}
|
|
632
628
|
native.event(candidate)
|
|
633
629
|
assistant_targets[index] = _assistant_target(candidate)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if offset <= end:
|
|
643
|
-
proof.source(event, offset)
|
|
644
|
-
native.source(event, offset)
|
|
645
|
-
if end not in verified or start and start not in verified:
|
|
646
|
-
return items
|
|
647
|
-
else:
|
|
648
|
-
# Same bounded proof as historical semantic windows: the caller has
|
|
649
|
-
# just verified this checkpoint. Recheck file identity and every
|
|
650
|
-
# candidate's exact source UUID/full hash, without a prefix rescan.
|
|
651
|
-
window_start = max(0, end - MAX_EVENTS_BYTES)
|
|
652
|
-
if start < window_start:
|
|
653
|
-
return items
|
|
654
|
-
for event, offset in _bounded_records(source, source_stamp, window_start, end):
|
|
630
|
+
digest, verified = hashlib.sha256(), set()
|
|
631
|
+
for event, offset, line in _records(source, source_stamp):
|
|
632
|
+
line.update_hash(digest)
|
|
633
|
+
if offset == start and hmac.compare_digest(digest.hexdigest(), previous):
|
|
634
|
+
verified.add(start)
|
|
635
|
+
if offset == end and hmac.compare_digest(digest.hexdigest(), expected):
|
|
636
|
+
verified.add(end)
|
|
637
|
+
if offset <= end:
|
|
655
638
|
proof.source(event, offset)
|
|
656
639
|
native.source(event, offset)
|
|
640
|
+
if end not in verified or start and start not in verified:
|
|
641
|
+
return items
|
|
657
642
|
eligible = {batch: (0, len(items) + 1, start, end)}
|
|
658
643
|
targets, assistant_replays = proof.prove(eligible), native.prove(eligible)
|
|
659
644
|
_assistants, owned_inputs = native.exact_owned_sources()
|
|
@@ -923,7 +908,7 @@ def _prove(session_id: str, provider_id: str, events: Path, root: Path,
|
|
|
923
908
|
raise _Unproven()
|
|
924
909
|
source = source.resolve(strict=True)
|
|
925
910
|
source.relative_to(root.resolve(strict=True))
|
|
926
|
-
source_stamp =
|
|
911
|
+
source_stamp = _regular_stamp(source)
|
|
927
912
|
prefix_digests = {}
|
|
928
913
|
eligible_batches = {}
|
|
929
914
|
for run, (seq, checkpoint) in batches.items():
|
|
@@ -972,7 +957,7 @@ def _prove(session_id: str, provider_id: str, events: Path, root: Path,
|
|
|
972
957
|
scheduled_ranges.setdefault(key, []).append((run, start_time, end_time))
|
|
973
958
|
scheduled_sources, scheduled_counts = {}, {}
|
|
974
959
|
for event, offset, line in _records(source, source_stamp):
|
|
975
|
-
|
|
960
|
+
line.update_hash(digest)
|
|
976
961
|
for wanted in prefix_digests.get(offset, ()):
|
|
977
962
|
if hmac.compare_digest(digest.hexdigest(), wanted):
|
|
978
963
|
verified.add((offset, wanted))
|
|
@@ -1144,25 +1129,25 @@ def _bounded_records(path: Path, expected, start: int, end: int):
|
|
|
1144
1129
|
if (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns) != expected:
|
|
1145
1130
|
raise _Unproven()
|
|
1146
1131
|
stream.seek(start)
|
|
1132
|
+
deadline = time.monotonic() + SCAN_SECONDS
|
|
1147
1133
|
if start:
|
|
1148
1134
|
stream.seek(start - 1)
|
|
1149
1135
|
if stream.read(1) != b"\n":
|
|
1150
|
-
|
|
1151
|
-
|
|
1136
|
+
while stream.tell() < end:
|
|
1137
|
+
scan_check(deadline)
|
|
1138
|
+
skipped = stream.readline(min(CHUNK_BYTES, end - stream.tell()))
|
|
1139
|
+
if not skipped:
|
|
1140
|
+
raise _Unproven()
|
|
1141
|
+
if skipped.endswith(b"\n"):
|
|
1142
|
+
break
|
|
1143
|
+
else:
|
|
1152
1144
|
raise _Unproven()
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
count += 1
|
|
1160
|
-
if count > MAX_RECORDS:
|
|
1161
|
-
raise _Unproven()
|
|
1162
|
-
event = json.loads(line)
|
|
1163
|
-
if not isinstance(event, dict):
|
|
1164
|
-
raise _Unproven()
|
|
1165
|
-
yield event, stream.tell()
|
|
1145
|
+
try:
|
|
1146
|
+
for event, offset, _raw in jsonl_records(stream, end=end, strict=True,
|
|
1147
|
+
deadline=deadline, max_records=MAX_RECORDS, max_retained_bytes=MAX_LINE_BYTES):
|
|
1148
|
+
yield event, offset
|
|
1149
|
+
except ValueError as exc:
|
|
1150
|
+
raise _Unproven() from exc
|
|
1166
1151
|
final = os.fstat(stream.fileno())
|
|
1167
1152
|
if (final.st_dev, final.st_ino, final.st_size, final.st_mtime_ns) != expected:
|
|
1168
1153
|
raise _Unproven()
|
package/server/codex_auth.py
CHANGED
|
@@ -22,9 +22,10 @@ MAX_API_KEY_CHARS = 4096
|
|
|
22
22
|
AUTH_TIMEOUT_SECONDS = 30.0
|
|
23
23
|
BUSY_MESSAGE = "Wait for Codex chats, goals, queued turns and Side chat requests to finish before changing authentication."
|
|
24
24
|
HANDOFF_MESSAGE = (
|
|
25
|
-
"
|
|
26
|
-
"goals
|
|
27
|
-
"not been
|
|
25
|
+
"Codex sign-in changed or was explicitly reloaded. This chat is waiting for "
|
|
26
|
+
"its running work, approvals, goals or background terminals before switching. "
|
|
27
|
+
"Your running work has not been stopped and this message has not been sent. "
|
|
28
|
+
"Retry after that work finishes."
|
|
28
29
|
)
|
|
29
30
|
_REVISION_KEY = secrets.token_bytes(32)
|
|
30
31
|
_MAX_AUTH_BYTES = 128 * 1024
|
|
@@ -115,8 +116,9 @@ def native_login_revision(env: dict[str, str], *, cwd: str) -> LoginRevision | N
|
|
|
115
116
|
|
|
116
117
|
Ignore rotating access/refresh tokens and last_refresh. OIDC auth_time is
|
|
117
118
|
stable across refresh but changes on an interactive authentication. Without
|
|
118
|
-
it we can detect an account switch, not prove a same-account re-login
|
|
119
|
-
|
|
119
|
+
it we can detect an account switch, not prove a same-account re-login.
|
|
120
|
+
Explicit native-admin reload can hand off opaque/keyring logins; an
|
|
121
|
+
unchanged known revision remains observational even on explicit reload.
|
|
120
122
|
Missing/malformed files are not evidence of a new login. No secret survives
|
|
121
123
|
this bounded read except a process-keyed comparison digest.
|
|
122
124
|
"""
|
|
@@ -209,7 +211,7 @@ async def read_account(manager) -> dict:
|
|
|
209
211
|
return account_summary(result)
|
|
210
212
|
|
|
211
213
|
|
|
212
|
-
def create_router(*, authorize, operation, available) -> APIRouter:
|
|
214
|
+
def create_router(*, authorize, operation, available, reload_login=None) -> APIRouter:
|
|
213
215
|
router = APIRouter()
|
|
214
216
|
|
|
215
217
|
@router.get("/api/admin/codex/auth")
|
|
@@ -227,7 +229,26 @@ def create_router(*, authorize, operation, available) -> APIRouter:
|
|
|
227
229
|
raise
|
|
228
230
|
except Exception:
|
|
229
231
|
raise HTTPException(503, "Codex authentication status is unavailable.") from None
|
|
230
|
-
return JSONResponse(result,
|
|
232
|
+
return JSONResponse({**result, "login_reload_available": reload_login is not None},
|
|
233
|
+
headers={"Cache-Control": "no-store"})
|
|
234
|
+
|
|
235
|
+
@router.post("/api/admin/codex/auth/reload")
|
|
236
|
+
async def reload(request: Request):
|
|
237
|
+
authorize(request)
|
|
238
|
+
if not available() or reload_login is None:
|
|
239
|
+
raise HTTPException(501, "Native Codex sign-in reload is unavailable.")
|
|
240
|
+
# No credential input; no handoff from GET, mounting a page or polling.
|
|
241
|
+
try:
|
|
242
|
+
body = await request.json()
|
|
243
|
+
except ValueError:
|
|
244
|
+
raise HTTPException(400, "Send an empty JSON object to reload Codex sign-in.") from None
|
|
245
|
+
if body != {}:
|
|
246
|
+
raise HTTPException(400, "Send an empty JSON object to reload Codex sign-in.")
|
|
247
|
+
pending = await reload_login()
|
|
248
|
+
response = await status(request)
|
|
249
|
+
result = json.loads(response.body)
|
|
250
|
+
return JSONResponse({**result, "login_reload_pending": pending},
|
|
251
|
+
headers={"Cache-Control": "no-store"})
|
|
231
252
|
|
|
232
253
|
@router.post("/api/admin/codex/auth/api-key")
|
|
233
254
|
async def login(request: Request):
|
|
@@ -26,6 +26,8 @@ from claude_history_repair import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
from transcript_reader import jsonl_records
|
|
30
|
+
|
|
29
31
|
MAX_PRIOR_SOURCE_PATHS = 2
|
|
30
32
|
MAX_AGGREGATE_SOURCE_BYTES = 96 * 1024 * 1024
|
|
31
33
|
MAX_AGGREGATE_SOURCE_RECORDS = 100_000
|
|
@@ -34,7 +36,9 @@ _PROVIDER_ID = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
|
|
|
34
36
|
_NATIVE_LEADING_DECORATION_RE = re.compile(
|
|
35
37
|
r"(?m)^[ \t]*(?:(?::[A-Za-z0-9_+\-]+:|[\U0001F300-\U0001FAFF\u2600-\u27BF]\ufe0f?)[ \t]*)+"
|
|
36
38
|
)
|
|
37
|
-
|
|
39
|
+
# Bound retained JSON; opaque provider payload bytes are streamed.
|
|
40
|
+
NATIVE_PROOF_LINE_BYTES = 32 * 1024 * 1024
|
|
41
|
+
NATIVE_PROOF_TEXT_CHARS = 4 * 1024 * 1024
|
|
38
42
|
NATIVE_PROOF_SECONDS = 30.0
|
|
39
43
|
_COMPACTION_PREFIX = (
|
|
40
44
|
"Another language model started to solve this problem and produced a summary of its thinking process. "
|
|
@@ -70,7 +74,7 @@ class CodexCompactionSummaryTracker:
|
|
|
70
74
|
and isinstance(metadata, dict) and metadata.get("content_item_kinds") == ["unknown"]
|
|
71
75
|
and isinstance(content, list) and len(content) == 1 and isinstance(content[0], dict)
|
|
72
76
|
and content[0].get("type") == "output_text" and isinstance(content[0].get("text"), str)
|
|
73
|
-
and 0 < len(content[0]["text"]) <=
|
|
77
|
+
and 0 < len(content[0]["text"]) <= NATIVE_PROOF_TEXT_CHARS):
|
|
74
78
|
self.pending = {"origin": origin, "text": content[0]["text"], "response_id": None}
|
|
75
79
|
return None
|
|
76
80
|
pending = self.pending
|
|
@@ -174,24 +178,20 @@ def _native_records(path: Path, expected: tuple[int, int, int, int], budget: _Na
|
|
|
174
178
|
info = os.fstat(stream.fileno())
|
|
175
179
|
if not stat.S_ISREG(info.st_mode) or (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns) != expected:
|
|
176
180
|
raise _Unproven()
|
|
177
|
-
|
|
178
|
-
while offset < boundary:
|
|
181
|
+
def cancelled():
|
|
179
182
|
budget.check()
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
record
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if not isinstance(record, dict):
|
|
193
|
-
raise _Unproven()
|
|
194
|
-
yield record, offset, line
|
|
183
|
+
return False
|
|
184
|
+
try:
|
|
185
|
+
for record, offset, raw in jsonl_records(stream, end=boundary,
|
|
186
|
+
strict=not provider_source, project=provider_source,
|
|
187
|
+
max_retained_bytes=NATIVE_PROOF_LINE_BYTES, cancelled=cancelled):
|
|
188
|
+
if record is None and not raw.malformed:
|
|
189
|
+
raise _Unproven() # Valid non-object JSON cannot substantiate native proof.
|
|
190
|
+
yield record or {}, offset, raw
|
|
191
|
+
except CodexNativeHistoryProofUnavailable:
|
|
192
|
+
raise
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
raise _Unproven() from exc
|
|
195
195
|
info = os.fstat(stream.fileno())
|
|
196
196
|
if (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns) != expected:
|
|
197
197
|
raise _Unproven()
|
|
@@ -436,7 +436,7 @@ def _prove_source(provider_id: str, source: Path, root: Path,
|
|
|
436
436
|
elif event.get("type") == "session_meta":
|
|
437
437
|
if not isinstance(payload, dict) or payload.get("id") != provider_id:
|
|
438
438
|
raise _Unproven()
|
|
439
|
-
|
|
439
|
+
line.update_hash(digest)
|
|
440
440
|
for expected in wanted.get(offset, ()):
|
|
441
441
|
if hmac.compare_digest(digest.hexdigest(), expected):
|
|
442
442
|
verified.add((offset, expected))
|
|
@@ -654,7 +654,7 @@ _ASYNC_DELIVERY_WRAPPER = re.compile(
|
|
|
654
654
|
|
|
655
655
|
def _async_delivery_body(text: str) -> tuple[str, str, int] | None:
|
|
656
656
|
"""Parse one complete legacy wire shape; text alone never proves delivery."""
|
|
657
|
-
if len(text) >
|
|
657
|
+
if len(text) > NATIVE_PROOF_TEXT_CHARS:
|
|
658
658
|
return None
|
|
659
659
|
match = _ASYNC_DELIVERY_WRAPPER.fullmatch(text)
|
|
660
660
|
if not match:
|
|
@@ -735,7 +735,7 @@ class _AsyncDeliveryIndex:
|
|
|
735
735
|
if isinstance(run, str) and 0 < len(run) <= 256 and not run.startswith("import_"):
|
|
736
736
|
if event_type == "turn_started":
|
|
737
737
|
body = event.get("prompt")
|
|
738
|
-
if isinstance(body, str) and 0 < len(body) <=
|
|
738
|
+
if isinstance(body, str) and 0 < len(body) <= NATIVE_PROOF_TEXT_CHARS and not _runtime_human_provenance(event):
|
|
739
739
|
record.update(body_key=_text_key(body), body_chars=len(body))
|
|
740
740
|
self.starts.setdefault(run, []).append(record)
|
|
741
741
|
self.count += 1
|
|
@@ -987,7 +987,7 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
987
987
|
raise _Unproven()
|
|
988
988
|
if parent is not None:
|
|
989
989
|
allowed_header_owners.add(parent)
|
|
990
|
-
|
|
990
|
+
line.update_hash(digest)
|
|
991
991
|
for expected in wanted.get(offset, ()):
|
|
992
992
|
if hmac.compare_digest(digest.hexdigest(), expected):
|
|
993
993
|
verified.add((offset, expected))
|
package/server/install.sh
CHANGED
|
@@ -1839,7 +1839,7 @@ if [[ "$TEAM_HUB_MODE" == "host" && "$TEAM_HUB_OPERATION_PENDING" != "true" \
|
|
|
1839
1839
|
fi
|
|
1840
1840
|
fi
|
|
1841
1841
|
|
|
1842
|
-
RELEASE_FILES=(activation_transaction.py execution_activation.py execution_legacy_runner.py execution_preparation.py update_preparation.py update_handoff.py update_recovery.py execution_update_status.py execution_uninstall.py execution_recovery.py execution_recovery_status.py execution_http.py execution_durability.py execution_control.py execution_install.py execution_maintenance.py execution_manage.py execution_ownership.py execution_service.py execution_transport.py agent_server.py local_session_ownership.py cursor_history.py workspace_git.py team_hub_host.py secure_peer_runtime.py team_mail_runtime.py team_mail_websocket.py team_mail_grants.py secure_peer_delivery.py agentsdock_jobs.py agentsdock_chats.py chat_mailbox.py agentsdock_emergency.py agentsdock_publish.py agentsdock_mail.py agentsdock_team.py provider_commands.py provider_usage.py claude_sdk_client.py claude_model_catalog.py claude_goals.py claude_background_reconciliation.py codex_app_server.py codex_auth.py codex_provider.py codex_response_stream.py provider_connections.py cursor_api_key.py side_questions.py title_generation.py codex_side_question.py codex_side_chat_progress.py claude_side_question.py cursor_agent_client.py cursor_provider_mcp.py opencode_agent_client.py cursor_process_guard.py claude_history_repair.py claude_history_provenance.py codex_history_repair.py public_chat_shares.py public_chat_transcript.py public_chat_share_routes.py interactive_chat_shares.py interactive_chat_share_routes.py interactive_chat_share_web.py interactive_chat_projection.py interactive_chat_runtime.py interactive_chat_native.py shared_chat_videos.py shared_chat_video_stream.py interactive_chat_controls.py install.sh uninstall.sh instances.sh server_instances.py update_runner.py pyproject.toml uv.lock VERSION release-public-key.pem LICENSE NOTICE)
|
|
1842
|
+
RELEASE_FILES=(activation_transaction.py execution_activation.py execution_legacy_runner.py execution_preparation.py update_preparation.py update_handoff.py update_recovery.py execution_update_status.py execution_uninstall.py execution_recovery.py execution_recovery_status.py execution_http.py execution_durability.py execution_control.py execution_install.py execution_maintenance.py execution_manage.py execution_ownership.py execution_service.py execution_transport.py agent_server.py local_session_ownership.py cursor_history.py workspace_git.py team_hub_host.py secure_peer_runtime.py team_mail_runtime.py team_mail_websocket.py team_mail_grants.py secure_peer_delivery.py agentsdock_jobs.py agentsdock_chats.py chat_mailbox.py agentsdock_emergency.py agentsdock_publish.py agentsdock_mail.py agentsdock_team.py provider_commands.py provider_usage.py claude_sdk_client.py claude_model_catalog.py claude_goals.py claude_background_reconciliation.py codex_app_server.py codex_auth.py codex_provider.py codex_response_stream.py provider_connections.py cursor_api_key.py side_questions.py title_generation.py codex_side_question.py codex_side_chat_progress.py claude_side_question.py cursor_agent_client.py cursor_provider_mcp.py opencode_agent_client.py cursor_process_guard.py transcript_reader.py claude_history_repair.py claude_history_provenance.py codex_history_repair.py public_chat_shares.py public_chat_transcript.py public_chat_share_routes.py interactive_chat_shares.py interactive_chat_share_routes.py interactive_chat_share_web.py interactive_chat_projection.py interactive_chat_runtime.py interactive_chat_native.py shared_chat_videos.py shared_chat_video_stream.py interactive_chat_controls.py install.sh uninstall.sh instances.sh server_instances.py update_runner.py pyproject.toml uv.lock VERSION release-public-key.pem LICENSE NOTICE)
|
|
1843
1843
|
RELEASE_DIRECTORIES=(agentsdock_team_hub)
|
|
1844
1844
|
TEAM_HUB_RELEASE_FILES=(
|
|
1845
1845
|
__init__.py
|
|
@@ -3763,6 +3763,7 @@ validate_staged_release_runtime() (
|
|
|
3763
3763
|
"$STAGE_DIR/cursor_provider_mcp.py" \
|
|
3764
3764
|
"$STAGE_DIR/opencode_agent_client.py" \
|
|
3765
3765
|
"$STAGE_DIR/cursor_process_guard.py" \
|
|
3766
|
+
"$STAGE_DIR/transcript_reader.py" \
|
|
3766
3767
|
"$STAGE_DIR/claude_history_repair.py" \
|
|
3767
3768
|
"$STAGE_DIR/claude_history_provenance.py" \
|
|
3768
3769
|
"$STAGE_DIR/codex_history_repair.py" \
|
|
@@ -3781,7 +3782,7 @@ validate_staged_release_runtime() (
|
|
|
3781
3782
|
"$STAGE_DIR/update_runner.py"
|
|
3782
3783
|
"$STAGE_DIR/.venv/bin/python" -m py_compile "$STAGE_DIR/server_instances.py"
|
|
3783
3784
|
"$STAGE_DIR/.venv/bin/python" -m compileall -q "$STAGE_DIR/agentsdock_team_hub"
|
|
3784
|
-
PYTHONPATH="$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c 'import execution_activation, execution_preparation, update_preparation, update_handoff, update_recovery, execution_update_status, execution_uninstall, execution_recovery, execution_recovery_status, execution_http, execution_durability, execution_control, execution_install, execution_maintenance, execution_manage, execution_ownership, execution_service, execution_transport; import workspace_git, claude_model_catalog, server_instances, local_session_ownership, cursor_history; import codex_auth, codex_provider, codex_response_stream, provider_connections, cursor_api_key, side_questions, title_generation, codex_side_question, codex_side_chat_progress, claude_side_question, agentsdock_team_hub, cursor_agent_client, cursor_provider_mcp, opencode_agent_client, cursor_process_guard, secure_peer_delivery, secure_peer_runtime, team_mail_runtime, team_mail_websocket, team_mail_grants, team_hub_host, agentsdock_mail, agentsdock_team, claude_history_repair, claude_history_provenance, claude_goals, claude_background_reconciliation, codex_history_repair, public_chat_shares, public_chat_transcript, public_chat_share_routes, interactive_chat_shares, interactive_chat_share_routes, interactive_chat_share_web, interactive_chat_projection, interactive_chat_runtime, interactive_chat_native, shared_chat_videos, shared_chat_video_stream, interactive_chat_controls, chat_mailbox, provider_commands, provider_usage; from agentsdock_team_hub import secure_peer, secure_peer_hub' >/dev/null
|
|
3785
|
+
PYTHONPATH="$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c 'import execution_activation, execution_preparation, update_preparation, update_handoff, update_recovery, execution_update_status, execution_uninstall, execution_recovery, execution_recovery_status, execution_http, execution_durability, execution_control, execution_install, execution_maintenance, execution_manage, execution_ownership, execution_service, execution_transport; import workspace_git, claude_model_catalog, server_instances, local_session_ownership, cursor_history; import codex_auth, codex_provider, transcript_reader, codex_response_stream, provider_connections, cursor_api_key, side_questions, title_generation, codex_side_question, codex_side_chat_progress, claude_side_question, agentsdock_team_hub, cursor_agent_client, cursor_provider_mcp, opencode_agent_client, cursor_process_guard, secure_peer_delivery, secure_peer_runtime, team_mail_runtime, team_mail_websocket, team_mail_grants, team_hub_host, agentsdock_mail, agentsdock_team, claude_history_repair, claude_history_provenance, claude_goals, claude_background_reconciliation, codex_history_repair, public_chat_shares, public_chat_transcript, public_chat_share_routes, interactive_chat_shares, interactive_chat_share_routes, interactive_chat_share_web, interactive_chat_projection, interactive_chat_runtime, interactive_chat_native, shared_chat_videos, shared_chat_video_stream, interactive_chat_controls, chat_mailbox, provider_commands, provider_usage; from agentsdock_team_hub import secure_peer, secure_peer_hub' >/dev/null
|
|
3785
3786
|
)
|
|
3786
3787
|
|
|
3787
3788
|
abort_unclaimed_team_hub_reactivation() {
|
|
@@ -587,6 +587,20 @@ def describe(instance: Instance) -> dict:
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
|
|
590
|
+
def pairing_info(instance: Instance) -> dict:
|
|
591
|
+
"""Private pipe to the CLI QR renderer; never write these details to logs."""
|
|
592
|
+
for target in (instance.config, instance.config / "env"):
|
|
593
|
+
check_path(target, instance.home)
|
|
594
|
+
env = read_config(instance)
|
|
595
|
+
token = env.get("AGENTSDOCK_AGENT_TOKEN", "")
|
|
596
|
+
if not token or any(ord(char) < 32 or ord(char) == 127 for char in token):
|
|
597
|
+
raise ValueError("This instance has no usable saved access token.")
|
|
598
|
+
port = int(env.get("AGENTSDOCK_AGENT_PORT", "7850"))
|
|
599
|
+
bind = env.get("AGENTSDOCK_AGENT_BIND", "0.0.0.0")
|
|
600
|
+
choices = connection_choices(bind, port, candidate_addresses(bind, port), tailscale_status())
|
|
601
|
+
return {"name": instance.name, "accessToken": token, "choices": choices}
|
|
602
|
+
|
|
603
|
+
|
|
590
604
|
def connection_choices(bind: str, port: int, addresses: list[str], network: dict[str, str]) -> dict:
|
|
591
605
|
"""Label candidates, never infer Tailscale connectivity from an IP range."""
|
|
592
606
|
choices = {"local": [], "lan": [], "other": [], "tailscale": "", "tailscale_note": ""}
|
|
@@ -941,6 +955,8 @@ def parser() -> argparse.ArgumentParser:
|
|
|
941
955
|
commands.add_parser("token", help="Choose an existing server and display only its access token")
|
|
942
956
|
info = commands.add_parser("info")
|
|
943
957
|
info.add_argument("name", type=instance_name)
|
|
958
|
+
pairing = commands.add_parser("_pair-info", help=argparse.SUPPRESS)
|
|
959
|
+
pairing.add_argument("name", type=instance_name)
|
|
944
960
|
status = commands.add_parser("status", help="Show readable details for all instances or one name")
|
|
945
961
|
status.add_argument("name", nargs="?", type=instance_name)
|
|
946
962
|
new = commands.add_parser("new")
|
|
@@ -979,6 +995,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
979
995
|
validate_binding(instance)
|
|
980
996
|
print(instance.shell_bindings())
|
|
981
997
|
return 0
|
|
998
|
+
if args.command == "_pair-info":
|
|
999
|
+
selected = next((item for item in registry.instances() if item.name == args.name), None)
|
|
1000
|
+
if selected is None:
|
|
1001
|
+
raise ValueError("Unknown instance. Use agentsdock list.")
|
|
1002
|
+
print(json.dumps(pairing_info(selected)))
|
|
1003
|
+
return 0
|
|
982
1004
|
if args.command == "token":
|
|
983
1005
|
selected = choose_token_instance(registry)
|
|
984
1006
|
if selected is None:
|