codex-autorunner 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.
- codex_autorunner/bootstrap.py +26 -5
- codex_autorunner/core/config.py +176 -59
- codex_autorunner/core/filesystem.py +24 -0
- codex_autorunner/core/flows/controller.py +50 -12
- codex_autorunner/core/flows/runtime.py +8 -3
- codex_autorunner/core/hub.py +293 -16
- codex_autorunner/core/lifecycle_events.py +44 -5
- codex_autorunner/core/pma_delivery.py +81 -0
- codex_autorunner/core/pma_dispatches.py +224 -0
- codex_autorunner/core/pma_lane_worker.py +122 -0
- codex_autorunner/core/pma_queue.py +167 -18
- codex_autorunner/core/pma_reactive.py +91 -0
- codex_autorunner/core/pma_safety.py +58 -0
- codex_autorunner/core/pma_sink.py +104 -0
- codex_autorunner/core/pma_transcripts.py +183 -0
- codex_autorunner/core/safe_paths.py +117 -0
- codex_autorunner/housekeeping.py +77 -23
- codex_autorunner/integrations/agents/codex_backend.py +18 -12
- codex_autorunner/integrations/agents/wiring.py +2 -0
- codex_autorunner/integrations/app_server/client.py +31 -0
- codex_autorunner/integrations/app_server/supervisor.py +3 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +16 -15
- codex_autorunner/integrations/telegram/handlers/commands/files.py +5 -8
- codex_autorunner/integrations/telegram/handlers/commands/github.py +10 -6
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +9 -8
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +85 -2
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +29 -8
- codex_autorunner/integrations/telegram/helpers.py +30 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +54 -3
- codex_autorunner/static/docChatCore.js +2 -0
- codex_autorunner/static/hub.js +59 -0
- codex_autorunner/static/index.html +70 -54
- codex_autorunner/static/notificationBell.js +173 -0
- codex_autorunner/static/notifications.js +154 -36
- codex_autorunner/static/pma.js +96 -35
- codex_autorunner/static/styles.css +415 -4
- codex_autorunner/static/utils.js +5 -1
- codex_autorunner/surfaces/cli/cli.py +206 -129
- codex_autorunner/surfaces/cli/template_repos.py +157 -0
- codex_autorunner/surfaces/web/app.py +193 -5
- codex_autorunner/surfaces/web/routes/file_chat.py +109 -61
- codex_autorunner/surfaces/web/routes/flows.py +125 -67
- codex_autorunner/surfaces/web/routes/pma.py +638 -57
- codex_autorunner/tickets/agent_pool.py +6 -1
- codex_autorunner/tickets/outbox.py +27 -14
- codex_autorunner/tickets/replies.py +4 -10
- codex_autorunner/tickets/runner.py +1 -0
- codex_autorunner/workspace/paths.py +8 -3
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/METADATA +1 -1
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/RECORD +55 -45
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/top_level.txt +0 -0
codex_autorunner/static/utils.js
CHANGED
|
@@ -542,7 +542,7 @@ export function confirmModal(message, options = {}) {
|
|
|
542
542
|
});
|
|
543
543
|
}
|
|
544
544
|
export function inputModal(message, options = {}) {
|
|
545
|
-
const { placeholder = "", defaultValue = "", confirmText = "OK", cancelText = "Cancel" } = options;
|
|
545
|
+
const { placeholder = "", defaultValue = "", confirmText = "OK", cancelText = "Cancel", allowEmpty = false, } = options;
|
|
546
546
|
return new Promise((resolve) => {
|
|
547
547
|
const overlay = document.getElementById("input-modal");
|
|
548
548
|
const messageEl = document.getElementById("input-modal-message");
|
|
@@ -576,6 +576,10 @@ export function inputModal(message, options = {}) {
|
|
|
576
576
|
};
|
|
577
577
|
const onOk = () => {
|
|
578
578
|
const value = inputEl.value.trim();
|
|
579
|
+
if (allowEmpty) {
|
|
580
|
+
finalize(value);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
579
583
|
finalize(value || null);
|
|
580
584
|
};
|
|
581
585
|
const onCancel = () => {
|
|
@@ -38,7 +38,6 @@ from ...core.flows.worker_process import (
|
|
|
38
38
|
)
|
|
39
39
|
from ...core.git_utils import GitError, run_git
|
|
40
40
|
from ...core.hub import HubSupervisor
|
|
41
|
-
from ...core.locks import file_lock
|
|
42
41
|
from ...core.logging_utils import log_event, setup_rotating_logger
|
|
43
42
|
from ...core.optional_dependencies import require_optional_dependencies
|
|
44
43
|
from ...core.runtime import (
|
|
@@ -106,11 +105,13 @@ from ...tickets.lint import (
|
|
|
106
105
|
from ...voice import VoiceConfig
|
|
107
106
|
from ..web.app import create_hub_app
|
|
108
107
|
from .pma_cli import pma_app as pma_cli_app
|
|
108
|
+
from .template_repos import TemplatesConfigError, load_template_repos_manager
|
|
109
109
|
|
|
110
110
|
logger = logging.getLogger("codex_autorunner.cli")
|
|
111
111
|
|
|
112
112
|
app = typer.Typer(add_completion=False)
|
|
113
113
|
hub_app = typer.Typer(add_completion=False)
|
|
114
|
+
dispatch_app = typer.Typer(add_completion=False)
|
|
114
115
|
telegram_app = typer.Typer(add_completion=False)
|
|
115
116
|
templates_app = typer.Typer(add_completion=False)
|
|
116
117
|
repos_app = typer.Typer(add_completion=False)
|
|
@@ -155,26 +156,6 @@ def _require_hub_config(path: Optional[Path]) -> HubConfig:
|
|
|
155
156
|
_raise_exit(str(exc), cause=exc)
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
def _load_hub_config_yaml(path: Path) -> dict:
|
|
159
|
-
if not path.exists():
|
|
160
|
-
_raise_exit(f"Hub config file not found: {path}")
|
|
161
|
-
try:
|
|
162
|
-
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
163
|
-
if not isinstance(data, dict):
|
|
164
|
-
_raise_exit(f"Hub config must be a YAML mapping: {path}")
|
|
165
|
-
return data
|
|
166
|
-
except yaml.YAMLError as exc:
|
|
167
|
-
_raise_exit(f"Invalid YAML in hub config: {exc}", cause=exc)
|
|
168
|
-
except OSError as exc:
|
|
169
|
-
_raise_exit(f"Failed to read hub config: {exc}", cause=exc)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _write_hub_config_yaml(path: Path, data: dict) -> None:
|
|
173
|
-
lock_path = path.parent / (path.name + ".lock")
|
|
174
|
-
with file_lock(lock_path):
|
|
175
|
-
path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
|
176
|
-
|
|
177
|
-
|
|
178
159
|
def _require_templates_enabled(config: RepoConfig) -> None:
|
|
179
160
|
if not config.templates.enabled:
|
|
180
161
|
_raise_exit(
|
|
@@ -452,6 +433,31 @@ def _request_json(
|
|
|
452
433
|
return data if isinstance(data, dict) else {}
|
|
453
434
|
|
|
454
435
|
|
|
436
|
+
def _request_form_json(
|
|
437
|
+
method: str,
|
|
438
|
+
url: str,
|
|
439
|
+
form: Optional[dict] = None,
|
|
440
|
+
token_env: Optional[str] = None,
|
|
441
|
+
*,
|
|
442
|
+
force_multipart: bool = False,
|
|
443
|
+
) -> dict:
|
|
444
|
+
headers = None
|
|
445
|
+
if token_env:
|
|
446
|
+
token = _require_auth_token(token_env)
|
|
447
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
448
|
+
data = form
|
|
449
|
+
files = None
|
|
450
|
+
if force_multipart:
|
|
451
|
+
data = form or {}
|
|
452
|
+
files = []
|
|
453
|
+
response = httpx.request(
|
|
454
|
+
method, url, data=data, files=files, timeout=5.0, headers=headers
|
|
455
|
+
)
|
|
456
|
+
response.raise_for_status()
|
|
457
|
+
data = response.json()
|
|
458
|
+
return data if isinstance(data, dict) else {}
|
|
459
|
+
|
|
460
|
+
|
|
455
461
|
def _require_optional_feature(
|
|
456
462
|
*, feature: str, deps: list[tuple[str, str]], extra: Optional[str] = None
|
|
457
463
|
) -> None:
|
|
@@ -462,6 +468,7 @@ def _require_optional_feature(
|
|
|
462
468
|
|
|
463
469
|
|
|
464
470
|
app.add_typer(hub_app, name="hub")
|
|
471
|
+
hub_app.add_typer(dispatch_app, name="dispatch")
|
|
465
472
|
hub_app.add_typer(worktree_app, name="worktree")
|
|
466
473
|
app.add_typer(telegram_app, name="telegram")
|
|
467
474
|
app.add_typer(templates_app, name="templates")
|
|
@@ -780,16 +787,8 @@ def repos_list(
|
|
|
780
787
|
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
781
788
|
):
|
|
782
789
|
"""List configured template repos."""
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
data = _load_hub_config_yaml(hub_config_path)
|
|
786
|
-
|
|
787
|
-
templates_config = data.get("templates", {})
|
|
788
|
-
if not isinstance(templates_config, dict):
|
|
789
|
-
templates_config = {}
|
|
790
|
-
repos = templates_config.get("repos", [])
|
|
791
|
-
if not isinstance(repos, list):
|
|
792
|
-
repos = []
|
|
790
|
+
manager = load_template_repos_manager(hub)
|
|
791
|
+
repos = manager.list_repos()
|
|
793
792
|
|
|
794
793
|
if output_json:
|
|
795
794
|
payload = {"repos": repos}
|
|
@@ -823,43 +822,16 @@ def repos_add(
|
|
|
823
822
|
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
824
823
|
):
|
|
825
824
|
"""Add a template repo to the hub config."""
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
enabled = templates_config.get("enabled", True)
|
|
834
|
-
if enabled is False:
|
|
835
|
-
_raise_exit(
|
|
836
|
-
"Templates are disabled. Set templates.enabled=true in the hub config to enable."
|
|
837
|
-
)
|
|
838
|
-
|
|
839
|
-
templates_config = data.setdefault("templates", {})
|
|
840
|
-
if not isinstance(templates_config, dict):
|
|
841
|
-
_raise_exit("Invalid templates config in hub config")
|
|
842
|
-
templates_config.setdefault("enabled", True)
|
|
843
|
-
repos = templates_config.setdefault("repos", [])
|
|
844
|
-
if not isinstance(repos, list):
|
|
845
|
-
_raise_exit("Invalid repos config in hub config")
|
|
846
|
-
|
|
847
|
-
existing_ids = {repo.get("id") for repo in repos if isinstance(repo, dict)}
|
|
848
|
-
if repo_id in existing_ids:
|
|
849
|
-
_raise_exit(f"Repo ID '{repo_id}' already exists. Use a unique ID.")
|
|
850
|
-
|
|
851
|
-
new_repo = {
|
|
852
|
-
"id": repo_id,
|
|
853
|
-
"url": url,
|
|
854
|
-
"default_ref": default_ref,
|
|
855
|
-
}
|
|
856
|
-
if trusted is not None:
|
|
857
|
-
new_repo["trusted"] = trusted
|
|
858
|
-
|
|
859
|
-
repos.append(new_repo)
|
|
825
|
+
manager = load_template_repos_manager(hub)
|
|
826
|
+
try:
|
|
827
|
+
manager.add_repo(repo_id, url, trusted, default_ref)
|
|
828
|
+
except TemplatesConfigError as exc:
|
|
829
|
+
_raise_exit(str(exc), cause=exc)
|
|
830
|
+
except OSError as exc:
|
|
831
|
+
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
860
832
|
|
|
861
833
|
try:
|
|
862
|
-
|
|
834
|
+
manager.save()
|
|
863
835
|
except OSError as exc:
|
|
864
836
|
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
865
837
|
|
|
@@ -872,29 +844,16 @@ def repos_remove(
|
|
|
872
844
|
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
873
845
|
):
|
|
874
846
|
"""Remove a template repo from the hub config."""
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
repos = templates_config.get("repos", [])
|
|
883
|
-
if not isinstance(repos, list):
|
|
884
|
-
repos = []
|
|
885
|
-
|
|
886
|
-
original_count = len(repos)
|
|
887
|
-
filtered_repos = [
|
|
888
|
-
repo for repo in repos if isinstance(repo, dict) and repo.get("id") != repo_id
|
|
889
|
-
]
|
|
890
|
-
|
|
891
|
-
if len(filtered_repos) == original_count:
|
|
892
|
-
_raise_exit(f"Repo ID '{repo_id}' not found in config.")
|
|
893
|
-
|
|
894
|
-
templates_config["repos"] = filtered_repos
|
|
847
|
+
manager = load_template_repos_manager(hub)
|
|
848
|
+
try:
|
|
849
|
+
manager.remove_repo(repo_id)
|
|
850
|
+
except TemplatesConfigError as exc:
|
|
851
|
+
_raise_exit(str(exc), cause=exc)
|
|
852
|
+
except OSError as exc:
|
|
853
|
+
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
895
854
|
|
|
896
855
|
try:
|
|
897
|
-
|
|
856
|
+
manager.save()
|
|
898
857
|
except OSError as exc:
|
|
899
858
|
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
900
859
|
|
|
@@ -907,29 +866,16 @@ def repos_trust(
|
|
|
907
866
|
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
908
867
|
):
|
|
909
868
|
"""Mark a template repo as trusted (skip scanning)."""
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
repos = templates_config.get("repos", [])
|
|
918
|
-
if not isinstance(repos, list):
|
|
919
|
-
repos = []
|
|
920
|
-
|
|
921
|
-
found = False
|
|
922
|
-
for repo in repos:
|
|
923
|
-
if isinstance(repo, dict) and repo.get("id") == repo_id:
|
|
924
|
-
repo["trusted"] = True
|
|
925
|
-
found = True
|
|
926
|
-
break
|
|
927
|
-
|
|
928
|
-
if not found:
|
|
929
|
-
_raise_exit(f"Repo ID '{repo_id}' not found in config.")
|
|
869
|
+
manager = load_template_repos_manager(hub)
|
|
870
|
+
try:
|
|
871
|
+
manager.set_trusted(repo_id, True)
|
|
872
|
+
except TemplatesConfigError as exc:
|
|
873
|
+
_raise_exit(str(exc), cause=exc)
|
|
874
|
+
except OSError as exc:
|
|
875
|
+
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
930
876
|
|
|
931
877
|
try:
|
|
932
|
-
|
|
878
|
+
manager.save()
|
|
933
879
|
except OSError as exc:
|
|
934
880
|
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
935
881
|
|
|
@@ -942,29 +888,16 @@ def repos_untrust(
|
|
|
942
888
|
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
943
889
|
):
|
|
944
890
|
"""Mark a template repo as untrusted (require scanning)."""
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
repos = templates_config.get("repos", [])
|
|
953
|
-
if not isinstance(repos, list):
|
|
954
|
-
repos = []
|
|
955
|
-
|
|
956
|
-
found = False
|
|
957
|
-
for repo in repos:
|
|
958
|
-
if isinstance(repo, dict) and repo.get("id") == repo_id:
|
|
959
|
-
repo["trusted"] = False
|
|
960
|
-
found = True
|
|
961
|
-
break
|
|
962
|
-
|
|
963
|
-
if not found:
|
|
964
|
-
_raise_exit(f"Repo ID '{repo_id}' not found in config.")
|
|
891
|
+
manager = load_template_repos_manager(hub)
|
|
892
|
+
try:
|
|
893
|
+
manager.set_trusted(repo_id, False)
|
|
894
|
+
except TemplatesConfigError as exc:
|
|
895
|
+
_raise_exit(str(exc), cause=exc)
|
|
896
|
+
except OSError as exc:
|
|
897
|
+
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
965
898
|
|
|
966
899
|
try:
|
|
967
|
-
|
|
900
|
+
manager.save()
|
|
968
901
|
except OSError as exc:
|
|
969
902
|
_raise_exit(f"Failed to write hub config: {exc}", cause=exc)
|
|
970
903
|
|
|
@@ -1783,6 +1716,150 @@ def hub_snapshot(
|
|
|
1783
1716
|
typer.echo(json.dumps(snapshot, indent=indent))
|
|
1784
1717
|
|
|
1785
1718
|
|
|
1719
|
+
@dispatch_app.command("reply")
|
|
1720
|
+
def hub_dispatch_reply(
|
|
1721
|
+
repo_id: str = typer.Option(..., "--repo-id", help="Hub repo id"),
|
|
1722
|
+
run_id: str = typer.Option(..., "--run-id", help="Flow run id (UUID)"),
|
|
1723
|
+
message: Optional[str] = typer.Option(None, "--message", help="Reply message body"),
|
|
1724
|
+
message_file: Optional[Path] = typer.Option(
|
|
1725
|
+
None, "--message-file", help="Read reply message body from file"
|
|
1726
|
+
),
|
|
1727
|
+
resume: bool = typer.Option(
|
|
1728
|
+
True, "--resume/--no-resume", help="Resume run after posting reply"
|
|
1729
|
+
),
|
|
1730
|
+
idempotency_key: Optional[str] = typer.Option(
|
|
1731
|
+
None, "--idempotency-key", help="Optional key to avoid duplicate replies"
|
|
1732
|
+
),
|
|
1733
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
1734
|
+
output_json: bool = typer.Option(
|
|
1735
|
+
True, "--json/--no-json", help="Emit JSON output (default: true)"
|
|
1736
|
+
),
|
|
1737
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON output"),
|
|
1738
|
+
):
|
|
1739
|
+
"""Reply to a paused dispatch and optionally resume the run."""
|
|
1740
|
+
config = _require_hub_config(path)
|
|
1741
|
+
|
|
1742
|
+
if bool(message) == bool(message_file):
|
|
1743
|
+
_raise_exit("Provide exactly one of --message or --message-file.")
|
|
1744
|
+
|
|
1745
|
+
raw_message = message
|
|
1746
|
+
if message_file is not None:
|
|
1747
|
+
try:
|
|
1748
|
+
raw_message = message_file.read_text(encoding="utf-8")
|
|
1749
|
+
except OSError as exc:
|
|
1750
|
+
_raise_exit(f"Failed to read message file: {exc}", cause=exc)
|
|
1751
|
+
body = (raw_message or "").strip()
|
|
1752
|
+
if not body:
|
|
1753
|
+
_raise_exit("Reply message cannot be empty.")
|
|
1754
|
+
|
|
1755
|
+
thread_url = _build_server_url(
|
|
1756
|
+
config, f"/repos/{repo_id}/api/messages/threads/{run_id}"
|
|
1757
|
+
)
|
|
1758
|
+
reply_url = _build_server_url(
|
|
1759
|
+
config, f"/repos/{repo_id}/api/messages/{run_id}/reply"
|
|
1760
|
+
)
|
|
1761
|
+
resume_url = _build_server_url(
|
|
1762
|
+
config, f"/repos/{repo_id}/api/flows/{run_id}/resume"
|
|
1763
|
+
)
|
|
1764
|
+
|
|
1765
|
+
marker = None
|
|
1766
|
+
if idempotency_key:
|
|
1767
|
+
marker = f"<!-- car-idempotency-key:{idempotency_key.strip()} -->"
|
|
1768
|
+
|
|
1769
|
+
try:
|
|
1770
|
+
thread = _request_json(
|
|
1771
|
+
"GET", thread_url, token_env=config.server_auth_token_env
|
|
1772
|
+
)
|
|
1773
|
+
except (
|
|
1774
|
+
httpx.HTTPError,
|
|
1775
|
+
httpx.ConnectError,
|
|
1776
|
+
httpx.TimeoutException,
|
|
1777
|
+
OSError,
|
|
1778
|
+
) as exc:
|
|
1779
|
+
_raise_exit(
|
|
1780
|
+
"Failed to query run thread via hub server. Ensure 'car hub serve' is running.",
|
|
1781
|
+
cause=exc,
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
run_status = ((thread.get("run") or {}) if isinstance(thread, dict) else {}).get(
|
|
1785
|
+
"status"
|
|
1786
|
+
)
|
|
1787
|
+
if run_status != "paused":
|
|
1788
|
+
_raise_exit(
|
|
1789
|
+
f"Run {run_id} is not paused-awaiting-input (status={run_status or 'unknown'})."
|
|
1790
|
+
)
|
|
1791
|
+
|
|
1792
|
+
duplicate = False
|
|
1793
|
+
reply_seq = None
|
|
1794
|
+
if marker:
|
|
1795
|
+
replies = thread.get("reply_history", []) if isinstance(thread, dict) else []
|
|
1796
|
+
for entry in replies if isinstance(replies, list) else []:
|
|
1797
|
+
reply = entry.get("reply") if isinstance(entry, dict) else None
|
|
1798
|
+
existing_body = (reply.get("body") or "") if isinstance(reply, dict) else ""
|
|
1799
|
+
if marker in existing_body:
|
|
1800
|
+
duplicate = True
|
|
1801
|
+
reply_seq = entry.get("seq") if isinstance(entry, dict) else None
|
|
1802
|
+
break
|
|
1803
|
+
|
|
1804
|
+
if not duplicate:
|
|
1805
|
+
post_body = body
|
|
1806
|
+
if marker:
|
|
1807
|
+
post_body = f"{body}\n\n{marker}"
|
|
1808
|
+
try:
|
|
1809
|
+
reply_resp = _request_form_json(
|
|
1810
|
+
"POST",
|
|
1811
|
+
reply_url,
|
|
1812
|
+
form={"body": post_body},
|
|
1813
|
+
token_env=config.server_auth_token_env,
|
|
1814
|
+
force_multipart=True,
|
|
1815
|
+
)
|
|
1816
|
+
reply_seq = reply_resp.get("seq")
|
|
1817
|
+
except (
|
|
1818
|
+
httpx.HTTPError,
|
|
1819
|
+
httpx.ConnectError,
|
|
1820
|
+
httpx.TimeoutException,
|
|
1821
|
+
OSError,
|
|
1822
|
+
) as exc:
|
|
1823
|
+
_raise_exit("Failed to post dispatch reply.", cause=exc)
|
|
1824
|
+
|
|
1825
|
+
resumed = False
|
|
1826
|
+
resume_status = None
|
|
1827
|
+
if resume:
|
|
1828
|
+
try:
|
|
1829
|
+
resume_resp = _request_json(
|
|
1830
|
+
"POST", resume_url, payload={}, token_env=config.server_auth_token_env
|
|
1831
|
+
)
|
|
1832
|
+
resumed = True
|
|
1833
|
+
resume_status = resume_resp.get("status")
|
|
1834
|
+
except (
|
|
1835
|
+
httpx.HTTPError,
|
|
1836
|
+
httpx.ConnectError,
|
|
1837
|
+
httpx.TimeoutException,
|
|
1838
|
+
OSError,
|
|
1839
|
+
) as exc:
|
|
1840
|
+
_raise_exit("Reply posted but resume failed.", cause=exc)
|
|
1841
|
+
|
|
1842
|
+
payload = {
|
|
1843
|
+
"repo_id": repo_id,
|
|
1844
|
+
"run_id": run_id,
|
|
1845
|
+
"reply_seq": reply_seq,
|
|
1846
|
+
"duplicate": duplicate,
|
|
1847
|
+
"resumed": resumed,
|
|
1848
|
+
"resume_status": resume_status,
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
if output_json:
|
|
1852
|
+
typer.echo(json.dumps(payload, indent=2 if pretty else None))
|
|
1853
|
+
return
|
|
1854
|
+
|
|
1855
|
+
typer.echo(
|
|
1856
|
+
f"Reply {'reused' if duplicate else 'posted'} for run {run_id}"
|
|
1857
|
+
+ (f" (seq={reply_seq})" if reply_seq else "")
|
|
1858
|
+
)
|
|
1859
|
+
if resume:
|
|
1860
|
+
typer.echo(f"Run resumed: status={resume_status or 'unknown'}")
|
|
1861
|
+
|
|
1862
|
+
|
|
1786
1863
|
@telegram_app.command("start")
|
|
1787
1864
|
def telegram_start(
|
|
1788
1865
|
path: Optional[Path] = typer.Option(None, "--path", help="Repo or hub root path"),
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Template repo config manager to centralize repo-config mutations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from ...core.config import CONFIG_FILENAME, load_hub_config
|
|
10
|
+
from ...core.locks import file_lock
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TemplatesConfigError(Exception):
|
|
14
|
+
"""Error in templates configuration."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TemplateReposManager:
|
|
18
|
+
"""Manager for template repos in hub config."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, hub_config_path: Path) -> None:
|
|
21
|
+
"""Initialize the manager with a hub config path."""
|
|
22
|
+
self.hub_config_path = hub_config_path
|
|
23
|
+
self._data: dict[str, Any] = {}
|
|
24
|
+
self._load()
|
|
25
|
+
|
|
26
|
+
def _load(self) -> None:
|
|
27
|
+
"""Load the hub config YAML."""
|
|
28
|
+
if not self.hub_config_path.exists():
|
|
29
|
+
raise TemplatesConfigError(
|
|
30
|
+
f"Hub config file not found: {self.hub_config_path}"
|
|
31
|
+
)
|
|
32
|
+
try:
|
|
33
|
+
data = yaml.safe_load(self.hub_config_path.read_text(encoding="utf-8"))
|
|
34
|
+
if not isinstance(data, dict):
|
|
35
|
+
raise TemplatesConfigError(
|
|
36
|
+
f"Hub config must be a YAML mapping: {self.hub_config_path}"
|
|
37
|
+
)
|
|
38
|
+
self._data = data
|
|
39
|
+
except yaml.YAMLError as exc:
|
|
40
|
+
raise TemplatesConfigError(f"Invalid YAML in hub config: {exc}") from exc
|
|
41
|
+
except OSError as exc:
|
|
42
|
+
raise TemplatesConfigError(f"Failed to read hub config: {exc}") from exc
|
|
43
|
+
|
|
44
|
+
def save(self) -> None:
|
|
45
|
+
"""Save the hub config YAML."""
|
|
46
|
+
lock_path = self.hub_config_path.parent / (self.hub_config_path.name + ".lock")
|
|
47
|
+
with file_lock(lock_path):
|
|
48
|
+
self.hub_config_path.write_text(
|
|
49
|
+
yaml.safe_dump(self._data, sort_keys=False), encoding="utf-8"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def list_repos(self) -> list[dict[str, Any]]:
|
|
53
|
+
"""List all configured template repos."""
|
|
54
|
+
templates_config = self._data.get("templates", {})
|
|
55
|
+
if not isinstance(templates_config, dict):
|
|
56
|
+
templates_config = {}
|
|
57
|
+
repos = templates_config.get("repos", [])
|
|
58
|
+
if not isinstance(repos, list):
|
|
59
|
+
repos = []
|
|
60
|
+
return repos
|
|
61
|
+
|
|
62
|
+
def add_repo(
|
|
63
|
+
self,
|
|
64
|
+
repo_id: str,
|
|
65
|
+
url: str,
|
|
66
|
+
trusted: Optional[bool] = None,
|
|
67
|
+
default_ref: str = "main",
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Add a template repo."""
|
|
70
|
+
self._require_templates_enabled()
|
|
71
|
+
|
|
72
|
+
templates_config = self._data.setdefault("templates", {})
|
|
73
|
+
if not isinstance(templates_config, dict):
|
|
74
|
+
raise TemplatesConfigError("Invalid templates config in hub config")
|
|
75
|
+
templates_config.setdefault("enabled", True)
|
|
76
|
+
|
|
77
|
+
repos = templates_config.setdefault("repos", [])
|
|
78
|
+
if not isinstance(repos, list):
|
|
79
|
+
raise TemplatesConfigError("Invalid repos config in hub config")
|
|
80
|
+
|
|
81
|
+
existing_ids = {repo.get("id") for repo in repos if isinstance(repo, dict)}
|
|
82
|
+
if repo_id in existing_ids:
|
|
83
|
+
raise TemplatesConfigError(
|
|
84
|
+
f"Repo ID '{repo_id}' already exists. Use a unique ID."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
new_repo = {
|
|
88
|
+
"id": repo_id,
|
|
89
|
+
"url": url,
|
|
90
|
+
"default_ref": default_ref,
|
|
91
|
+
}
|
|
92
|
+
if trusted is not None:
|
|
93
|
+
new_repo["trusted"] = trusted
|
|
94
|
+
|
|
95
|
+
repos.append(new_repo)
|
|
96
|
+
|
|
97
|
+
def remove_repo(self, repo_id: str) -> None:
|
|
98
|
+
"""Remove a template repo."""
|
|
99
|
+
templates_config = self._data.get("templates", {})
|
|
100
|
+
if not isinstance(templates_config, dict):
|
|
101
|
+
templates_config = {}
|
|
102
|
+
repos = templates_config.get("repos", [])
|
|
103
|
+
if not isinstance(repos, list):
|
|
104
|
+
repos = []
|
|
105
|
+
|
|
106
|
+
original_count = len(repos)
|
|
107
|
+
filtered_repos = [
|
|
108
|
+
repo
|
|
109
|
+
for repo in repos
|
|
110
|
+
if isinstance(repo, dict) and repo.get("id") != repo_id
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
if len(filtered_repos) == original_count:
|
|
114
|
+
raise TemplatesConfigError(f"Repo ID '{repo_id}' not found in config.")
|
|
115
|
+
|
|
116
|
+
templates_config["repos"] = filtered_repos
|
|
117
|
+
|
|
118
|
+
def set_trusted(self, repo_id: str, trusted: bool) -> None:
|
|
119
|
+
"""Set the trusted status of a template repo."""
|
|
120
|
+
templates_config = self._data.get("templates", {})
|
|
121
|
+
if not isinstance(templates_config, dict):
|
|
122
|
+
templates_config = {}
|
|
123
|
+
repos = templates_config.get("repos", [])
|
|
124
|
+
if not isinstance(repos, list):
|
|
125
|
+
repos = []
|
|
126
|
+
|
|
127
|
+
found = False
|
|
128
|
+
for repo in repos:
|
|
129
|
+
if isinstance(repo, dict) and repo.get("id") == repo_id:
|
|
130
|
+
repo["trusted"] = trusted
|
|
131
|
+
found = True
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
if not found:
|
|
135
|
+
raise TemplatesConfigError(f"Repo ID '{repo_id}' not found in config.")
|
|
136
|
+
|
|
137
|
+
def _require_templates_enabled(self) -> None:
|
|
138
|
+
"""Ensure templates are enabled in config."""
|
|
139
|
+
templates_config = self._data.get("templates", {})
|
|
140
|
+
if not isinstance(templates_config, dict):
|
|
141
|
+
templates_config = {}
|
|
142
|
+
enabled = templates_config.get("enabled", True)
|
|
143
|
+
if enabled is False:
|
|
144
|
+
raise TemplatesConfigError(
|
|
145
|
+
"Templates are disabled. Set templates.enabled=true in the hub config to enable."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_template_repos_manager(hub: Optional[Path]) -> TemplateReposManager:
|
|
150
|
+
"""Load a TemplateReposManager for the given hub path."""
|
|
151
|
+
try:
|
|
152
|
+
config = load_hub_config(hub or Path.cwd())
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
typer.echo(str(exc), err=True)
|
|
155
|
+
raise typer.Exit(code=1) from None
|
|
156
|
+
hub_config_path = config.root / CONFIG_FILENAME
|
|
157
|
+
return TemplateReposManager(hub_config_path)
|