@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.
@@ -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
- MAX_LINE_BYTES = 4 * 1024 * 1024
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
- count = 0
77
- offset = 0
78
- while offset < expected[2]:
79
- line = stream.readline(min(MAX_LINE_BYTES + 1, expected[2] - offset))
80
- if len(line) > MAX_LINE_BYTES or not line.endswith(b"\n"):
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 _stamp(path) != expected:
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) > MAX_LINE_BYTES
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) > MAX_LINE_BYTES
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) <= MAX_LINE_BYTES else None)
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"]) <= MAX_LINE_BYTES):
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) <= MAX_LINE_BYTES:
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) <= MAX_LINE_BYTES else None
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) <= MAX_LINE_BYTES
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
- if source_stamp[2] <= MAX_EVENTS_BYTES:
635
- digest, verified = hashlib.sha256(), set()
636
- for event, offset, line in _records(source, source_stamp):
637
- digest.update(line)
638
- if offset == start and hmac.compare_digest(digest.hexdigest(), previous):
639
- verified.add(start)
640
- if offset == end and hmac.compare_digest(digest.hexdigest(), expected):
641
- verified.add(end)
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 = _stamp(source)
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
- digest.update(line)
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
- skipped = stream.readline(min(MAX_LINE_BYTES + 1, end - start))
1151
- if len(skipped) > MAX_LINE_BYTES or not skipped.endswith(b"\n"):
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
- count = 0
1154
- while stream.tell() < end:
1155
- offset = stream.tell()
1156
- line = stream.readline(min(MAX_LINE_BYTES + 1, end - offset))
1157
- if len(line) > MAX_LINE_BYTES or not line.endswith(b"\n"):
1158
- raise _Unproven()
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()
@@ -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
- "Refreshing Codex sign-in. Wait for this chat's running work, approvals, "
26
- "goals and background terminals to finish, then retry. Your message has "
27
- "not been sent to Codex."
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
- Recheck CLIs provides an explicit handoff for that case and keyring stores.
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, headers={"Cache-Control": "no-store"})
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
- NATIVE_PROOF_LINE_BYTES = 4 * 1024 * 1024
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"]) <= NATIVE_PROOF_LINE_BYTES):
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
- offset = 0
178
- while offset < boundary:
181
+ def cancelled():
179
182
  budget.check()
180
- line = stream.readline(min(NATIVE_PROOF_LINE_BYTES + 1, boundary - offset))
181
- if not line or len(line) > NATIVE_PROOF_LINE_BYTES or not line.endswith(b"\n"):
182
- raise _Unproven()
183
- offset += len(line)
184
- try:
185
- record = json.loads(line)
186
- except (ValueError, UnicodeDecodeError):
187
- if not provider_source:
188
- raise
189
- # The provider importer skips malformed records. Their exact
190
- # bytes still participate in the checkpoint digest below.
191
- record = {}
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
- digest.update(line)
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) > NATIVE_PROOF_LINE_BYTES:
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) <= NATIVE_PROOF_LINE_BYTES and not _runtime_human_provenance(event):
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
- digest.update(line)
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: