@agentsdock/server 1.0.10 → 1.0.11-beta.12
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 +1658 -498
- 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 +26 -2
- package/server/interactive_chat_controls.py +9 -3
- package/server/interactive_chat_native.py +1 -1
- package/server/interactive_chat_share_routes.py +2 -2
- package/server/interactive_chat_share_web.py +2 -2
- package/server/local_session_ownership.py +1 -0
- package/server/npm/agentsdock/QR-NOTICES.txt +56 -0
- package/server/npm/agentsdock/pair.cjs +123 -0
- package/server/npm/agentsdock/qr-renderer.cjs +15 -0
- package/server/provider_commands.py +65 -0
- package/server/provider_connections.py +39 -10
- package/server/public_chat_share_routes.py +8 -6
- package/server/public_chat_shares.py +17 -7
- package/server/server_instances.py +217 -15
- package/server/shared_chat_analytics_web.py +5 -0
- package/server/title_generation.py +60 -1
- package/server/transcript_reader.py +334 -0
- package/server/update_runner.py +4 -2
|
@@ -507,7 +507,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
507
507
|
create_parser.add_argument("--loop", action="store_true", help="repeat at the interval")
|
|
508
508
|
create_parser.add_argument("--max-runs", type=int)
|
|
509
509
|
create_parser.add_argument("--disabled", action="store_true")
|
|
510
|
-
create_parser.add_argument("--backend", choices=("codex", "claude", "cursor"))
|
|
510
|
+
create_parser.add_argument("--backend", choices=("codex", "claude", "cursor", "opencode", "gemini"))
|
|
511
511
|
create_parser.add_argument(
|
|
512
512
|
"--context-mode",
|
|
513
513
|
choices=("chat", "standalone"),
|
|
@@ -541,7 +541,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
541
541
|
run_limit_group = update_parser.add_mutually_exclusive_group()
|
|
542
542
|
run_limit_group.add_argument("--max-runs", type=int)
|
|
543
543
|
run_limit_group.add_argument("--unlimited", action="store_true", help="clear a finite run limit")
|
|
544
|
-
update_parser.add_argument("--backend", choices=("codex", "claude", "cursor"))
|
|
544
|
+
update_parser.add_argument("--backend", choices=("codex", "claude", "cursor", "opencode", "gemini"))
|
|
545
545
|
update_parser.add_argument(
|
|
546
546
|
"--context-mode",
|
|
547
547
|
choices=("chat", "standalone"),
|
|
@@ -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
|
|
@@ -60,6 +61,54 @@ def handoff_detail(reason: str) -> dict:
|
|
|
60
61
|
"message": f"Codex sign-in handoff is waiting: {reasons[reason]}. "
|
|
61
62
|
f"Your message has not been sent. {action}"}
|
|
62
63
|
|
|
64
|
+
# Native ChatGPT sign-in only. API-key and custom-provider availability is
|
|
65
|
+
# provider-owned. Keep old IDs in saved conversations; this is picker/admission
|
|
66
|
+
# policy, not transcript cleanup. Source: https://learn.chatgpt.com/docs/models
|
|
67
|
+
CHATGPT_RETIRED_MODELS = frozenset({
|
|
68
|
+
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark",
|
|
69
|
+
"gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def native_model_auth_mode(env: dict[str, str], *, cwd: str, observed_mode: str = "") -> str:
|
|
74
|
+
"""Read only the auth kind for model policy, never authenticate or refresh.
|
|
75
|
+
|
|
76
|
+
An opaque store may use the existing native login-status probe. A file
|
|
77
|
+
store is checked directly so a switch to an API key cannot inherit a cached
|
|
78
|
+
ChatGPT restriction. Unknown/managed providers are never globally banned.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
root, config = _native_login_config(env, cwd)
|
|
82
|
+
profile = config.get("profile")
|
|
83
|
+
profiles = config.get("profiles")
|
|
84
|
+
selected_profile = profiles.get(profile, {}) if isinstance(profiles, dict) and isinstance(profile, str) else {}
|
|
85
|
+
if not isinstance(selected_profile, dict):
|
|
86
|
+
return "unknown"
|
|
87
|
+
provider = selected_profile.get("model_provider", config.get("model_provider", "openai"))
|
|
88
|
+
if provider != "openai":
|
|
89
|
+
return "custom"
|
|
90
|
+
providers = config.get("model_providers") or {}
|
|
91
|
+
override = providers.get("openai") if isinstance(providers, dict) else None
|
|
92
|
+
if env.get("OPENAI_BASE_URL") or isinstance(override, dict) and override.get("base_url"):
|
|
93
|
+
return "custom"
|
|
94
|
+
if any(env.get(key) for key in ("CODEX_API_KEY", "OPENAI_API_KEY", "OPENAI_IDENTITY_TOKEN_FILE")):
|
|
95
|
+
return "apiKey"
|
|
96
|
+
if env.get("CODEX_ACCESS_TOKEN"):
|
|
97
|
+
return "unknown"
|
|
98
|
+
store = config.get("cli_auth_credentials_store", "file")
|
|
99
|
+
if store != "file":
|
|
100
|
+
return observed_mode if store in {"keyring", "auto"} and observed_mode in {"chatgpt", "apiKey"} else "unknown"
|
|
101
|
+
data = json.loads(_bounded_file(root / "auth.json"))
|
|
102
|
+
if not isinstance(data, dict):
|
|
103
|
+
return "unknown"
|
|
104
|
+
if isinstance(data.get("OPENAI_API_KEY"), str) and data["OPENAI_API_KEY"]:
|
|
105
|
+
return "apiKey"
|
|
106
|
+
if data.get("auth_mode") in (None, "chatgpt") and isinstance(data.get("tokens"), dict):
|
|
107
|
+
return "chatgpt"
|
|
108
|
+
except (OSError, ValueError, TypeError, RecursionError):
|
|
109
|
+
pass
|
|
110
|
+
return "unknown"
|
|
111
|
+
|
|
63
112
|
|
|
64
113
|
@dataclass(frozen=True)
|
|
65
114
|
class LoginRevision:
|
|
@@ -115,8 +164,9 @@ def native_login_revision(env: dict[str, str], *, cwd: str) -> LoginRevision | N
|
|
|
115
164
|
|
|
116
165
|
Ignore rotating access/refresh tokens and last_refresh. OIDC auth_time is
|
|
117
166
|
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
|
-
|
|
167
|
+
it we can detect an account switch, not prove a same-account re-login.
|
|
168
|
+
Explicit native-admin reload can hand off opaque/keyring logins; an
|
|
169
|
+
unchanged known revision remains observational even on explicit reload.
|
|
120
170
|
Missing/malformed files are not evidence of a new login. No secret survives
|
|
121
171
|
this bounded read except a process-keyed comparison digest.
|
|
122
172
|
"""
|
|
@@ -209,7 +259,7 @@ async def read_account(manager) -> dict:
|
|
|
209
259
|
return account_summary(result)
|
|
210
260
|
|
|
211
261
|
|
|
212
|
-
def create_router(*, authorize, operation, available) -> APIRouter:
|
|
262
|
+
def create_router(*, authorize, operation, available, reload_login=None) -> APIRouter:
|
|
213
263
|
router = APIRouter()
|
|
214
264
|
|
|
215
265
|
@router.get("/api/admin/codex/auth")
|
|
@@ -227,7 +277,26 @@ def create_router(*, authorize, operation, available) -> APIRouter:
|
|
|
227
277
|
raise
|
|
228
278
|
except Exception:
|
|
229
279
|
raise HTTPException(503, "Codex authentication status is unavailable.") from None
|
|
230
|
-
return JSONResponse(result,
|
|
280
|
+
return JSONResponse({**result, "login_reload_available": reload_login is not None},
|
|
281
|
+
headers={"Cache-Control": "no-store"})
|
|
282
|
+
|
|
283
|
+
@router.post("/api/admin/codex/auth/reload")
|
|
284
|
+
async def reload(request: Request):
|
|
285
|
+
authorize(request)
|
|
286
|
+
if not available() or reload_login is None:
|
|
287
|
+
raise HTTPException(501, "Native Codex sign-in reload is unavailable.")
|
|
288
|
+
# No credential input; no handoff from GET, mounting a page or polling.
|
|
289
|
+
try:
|
|
290
|
+
body = await request.json()
|
|
291
|
+
except ValueError:
|
|
292
|
+
raise HTTPException(400, "Send an empty JSON object to reload Codex sign-in.") from None
|
|
293
|
+
if body != {}:
|
|
294
|
+
raise HTTPException(400, "Send an empty JSON object to reload Codex sign-in.")
|
|
295
|
+
pending = await reload_login()
|
|
296
|
+
response = await status(request)
|
|
297
|
+
result = json.loads(response.body)
|
|
298
|
+
return JSONResponse({**result, "login_reload_pending": pending},
|
|
299
|
+
headers={"Cache-Control": "no-store"})
|
|
231
300
|
|
|
232
301
|
@router.post("/api/admin/codex/auth/api-key")
|
|
233
302
|
async def login(request: Request):
|