ctrlrelay 0.13.0__tar.gz → 0.15.0__tar.gz
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.
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/CHANGELOG.md +71 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/PKG-INFO +1 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/config/orchestrator.yaml.example +14 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/architecture.md +8 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/configuration.md +111 -3
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/feedback-loop.md +6 -3
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/operations.md +93 -0
- ctrlrelay-0.15.0/docs/superpowers/plans/2026-10-30-status-page.md +304 -0
- ctrlrelay-0.15.0/docs/superpowers/specs/2026-10-30-status-page-design.md +240 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/pyproject.toml +1 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/server.py +27 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/cli.py +283 -1
- ctrlrelay-0.15.0/src/ctrlrelay/core/agent_env.py +228 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/config.py +41 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/dispatcher.py +112 -2
- ctrlrelay-0.15.0/src/ctrlrelay/core/events.py +173 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/github.py +96 -5
- ctrlrelay-0.15.0/src/ctrlrelay/core/publish.py +189 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/state.py +356 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/worktree.py +289 -3
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/install.py +5 -4
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/pipelines/dev.py +536 -57
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/pipelines/post_merge.py +18 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/pipelines/secops.py +71 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/pipelines/task.py +178 -26
- ctrlrelay-0.15.0/src/ctrlrelay/templates/systemd/web.service.template +22 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/__init__.py +1 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/health.py +167 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/links.py +35 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/server.py +347 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/static/index.html +684 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/status.py +745 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/transcript.py +529 -0
- ctrlrelay-0.15.0/src/ctrlrelay/web/waiting.py +534 -0
- ctrlrelay-0.15.0/tests/_publish_fakes.py +25 -0
- ctrlrelay-0.15.0/tests/fixtures/gh_search_issues.json +162 -0
- ctrlrelay-0.15.0/tests/fixtures/transcript_sample.jsonl +105 -0
- ctrlrelay-0.15.0/tests/test_agent_no_keys.py +1229 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_binary_resolution.py +3 -2
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_claim_reconciliation.py +26 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_dev_integration.py +23 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_dev_pipeline.py +62 -70
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_dispatch_outcome_hook.py +28 -14
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_dispatcher.py +38 -19
- ctrlrelay-0.15.0/tests/test_event_emission.py +1065 -0
- ctrlrelay-0.15.0/tests/test_events.py +425 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_install.py +22 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_relay_alert_off_lock.py +2 -1
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_resume_prompt_contract.py +14 -4
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_task_pipeline.py +10 -0
- ctrlrelay-0.15.0/tests/test_web_health.py +228 -0
- ctrlrelay-0.15.0/tests/test_web_links.py +60 -0
- ctrlrelay-0.15.0/tests/test_web_server.py +1111 -0
- ctrlrelay-0.15.0/tests/test_web_status.py +1425 -0
- ctrlrelay-0.15.0/tests/test_web_transcript.py +645 -0
- ctrlrelay-0.15.0/tests/test_web_waiting.py +1171 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_worktree.py +0 -23
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/.gitignore +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/LICENSE +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/README.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/SECURITY.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/_config.yml +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/bridge.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/cli.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/development.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/getting-started.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/index.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/docs/personalization.md +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/poller.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_archived.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_config.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_network.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_obs.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_poller.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_question_backoff.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_redact.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_setup.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.13.0 → ctrlrelay-0.15.0}/tests/test_unreviewed_pr_announcement.py +0 -0
|
@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.0] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
The dev and task agents no longer inherit the usual GitHub credentials;
|
|
13
|
+
ctrlrelay does the pushing, PR opening and commenting itself
|
|
14
|
+
after the agent exits. One new
|
|
15
|
+
database table is added automatically and the new config values have
|
|
16
|
+
defaults, so **no config edit is required** to upgrade.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Dev and task agents run with inherited `GH_*` and `GITHUB_*` variables
|
|
21
|
+
and gh's login removed from their environment, git credential helpers and
|
|
22
|
+
git's SSH disabled, and the D-Bus and runtime-dir variables pointed away
|
|
23
|
+
from the desktop keyring. After the agent exits the
|
|
24
|
+
relay pushes the run branch from a relay-private git dir to an explicit
|
|
25
|
+
GitHub URL, opens the PR from the DONE checkpoint (or reuses an open PR on the run
|
|
26
|
+
branch), pushes fix rounds,
|
|
27
|
+
fetches failed CI logs and posts task reports. It refuses to push the
|
|
28
|
+
default branch and only overwrites a remote branch whose head it recorded
|
|
29
|
+
pushing. secops keeps GitHub access (#235, closes #219)
|
|
30
|
+
- Status page: the phase bar shows the furthest phase seen in the recent
|
|
31
|
+
transcript and pipeline events, CI waits are
|
|
32
|
+
shown as waiting rather than reading, and the PR is linked (#239, closes
|
|
33
|
+
#238)
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Optional `agent.sandbox: bwrap` with `agent.sandbox_hide_paths` to run the
|
|
38
|
+
dev and task agents under bubblewrap with credential paths hidden. Off by
|
|
39
|
+
default (#235)
|
|
40
|
+
- A `relay_pushes` table recording the heads ctrlrelay pushed (#235)
|
|
41
|
+
- Status page: a live counter and dot, server health, GitHub links, and the
|
|
42
|
+
list of skipped issues (#233, closes #232)
|
|
43
|
+
|
|
44
|
+
### Known limits
|
|
45
|
+
|
|
46
|
+
- The agent still runs as the same OS user. Without the bwrap sandbox it
|
|
47
|
+
can read credential files on disk; with it, only the paths in
|
|
48
|
+
`agent.sandbox_hide_paths` are hidden. It can still run ssh directly, reach
|
|
49
|
+
the Linux keyring by resetting the D-Bus address, or read a token kept
|
|
50
|
+
under some other variable name, and the macOS keychain is not covered. Running the agent as a separate user
|
|
51
|
+
is tracked in #236
|
|
52
|
+
|
|
53
|
+
## [0.14.0] - 2026-10-31
|
|
54
|
+
|
|
55
|
+
A read-only status page. `state.db` switches to WAL mode and gains an
|
|
56
|
+
`events` table; both happen automatically. The page is off until you start
|
|
57
|
+
it. **No config edit is required** to upgrade.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- `ctrlrelay web start|stop|status` and a `ctrlrelay-web` systemd unit: a
|
|
62
|
+
read-only page plus `/api/status` and `/api/events` JSON showing what each
|
|
63
|
+
agent is doing (phase and last command), the waiting line, questions that
|
|
64
|
+
need you, today's counts, and a feed of dispatches, PRs, CI, reviews,
|
|
65
|
+
merges, messages and alerts. New `web:` config block (defaults
|
|
66
|
+
`127.0.0.1:8787`, 7 days). Requests for other host names are refused; add
|
|
67
|
+
LAN names to `web.allowed_hosts` (#217, closes #209)
|
|
68
|
+
- An `events` table, written best-effort by the daemon on a separate
|
|
69
|
+
connection with a 0.25s lock wait, so a slow or failed write never affects
|
|
70
|
+
the work it records (#217)
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- `state.db` uses WAL mode so the page can read while the daemon writes. If
|
|
75
|
+
the switch cannot get the lock at startup it is deferred to the next start
|
|
76
|
+
instead of failing (#217)
|
|
77
|
+
|
|
10
78
|
## [0.13.0] - 2026-10-30
|
|
11
79
|
|
|
12
80
|
New database columns and one new config value. All of them are added
|
|
@@ -1771,7 +1839,9 @@ pipeline).
|
|
|
1771
1839
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
1772
1840
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
1773
1841
|
|
|
1774
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.
|
|
1842
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.15.0...HEAD
|
|
1843
|
+
[0.15.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.14.0...v0.15.0
|
|
1844
|
+
[0.14.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.13.0...v0.14.0
|
|
1775
1845
|
[0.13.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.4...v0.13.0
|
|
1776
1846
|
[0.12.4]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.2...v0.12.4
|
|
1777
1847
|
[0.12.3]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.2...v0.12.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
|
|
5
5
|
Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
|
|
6
6
|
Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
|
|
@@ -88,6 +88,20 @@ dashboard:
|
|
|
88
88
|
url: "https://ctrlrelay-dashboard.example.com"
|
|
89
89
|
auth_token_env: "CTRLRELAY_DASHBOARD_TOKEN"
|
|
90
90
|
|
|
91
|
+
# Read-only status page: `ctrlrelay web start`. Loopback by default; set
|
|
92
|
+
# host to this machine's LAN address to open it from the home network.
|
|
93
|
+
# It has no login: never expose it to the internet.
|
|
94
|
+
web:
|
|
95
|
+
host: "127.0.0.1"
|
|
96
|
+
port: 8787
|
|
97
|
+
history_days: 7
|
|
98
|
+
refresh_seconds: 5
|
|
99
|
+
github_check_seconds: 60
|
|
100
|
+
stale_after_minutes: 15
|
|
101
|
+
# Extra names you open the page by from another machine (the Host
|
|
102
|
+
# header); loopback, web.host and this machine's hostname are allowed.
|
|
103
|
+
allowed_hosts: []
|
|
104
|
+
|
|
91
105
|
# Scheduled jobs run in-process by the poller daemon. Cron expressions are
|
|
92
106
|
# standard 5-field (minute hour dom month dow) and evaluate in the `timezone`
|
|
93
107
|
# set above. Change `secops_cron` to e.g. "0 6 * * 1" for weekly (Mondays).
|
|
@@ -70,6 +70,14 @@ is the only place ctrlrelay calls out to Claude Code.
|
|
|
70
70
|
- **Environment additions:** `CTRLRELAY_SESSION_ID` and `CTRLRELAY_STATE_FILE`
|
|
71
71
|
are the only contract between dispatcher and child. Anything else the agent
|
|
72
72
|
needs (issue body, PR rules, etc.) goes into the prompt.
|
|
73
|
+
- **Credentials (#219):** every spawn says whether it gets GitHub access
|
|
74
|
+
(`spawn_session(..., github_access=...)`, no default). dev and task spawn
|
|
75
|
+
without it: GitHub tokens, `gh`'s login, git credential helpers, SSH and
|
|
76
|
+
the desktop keyring are cut off (`core/agent_env.py`), and ctrlrelay
|
|
77
|
+
pushes the branch, opens the PR or posts the report itself afterwards
|
|
78
|
+
(`core/publish.py`, `WorktreeManager.push_run_branch`). secops still
|
|
79
|
+
spawns with access. See
|
|
80
|
+
[What the agent can reach](configuration.md#what-the-agent-can-reach-219).
|
|
73
81
|
- **Result:** `SessionResult(session_id, exit_code, state, stdout, stderr,
|
|
74
82
|
agent_session_id)`. `session_id` is our composite orchestrator id;
|
|
75
83
|
`agent_session_id` is Claude's own UUID, parsed from the JSON stdout and
|
|
@@ -26,6 +26,7 @@ paths: { ... }
|
|
|
26
26
|
claude: { ... }
|
|
27
27
|
transport: { ... }
|
|
28
28
|
dashboard: { ... }
|
|
29
|
+
web: { ... }
|
|
29
30
|
repos: [ ... ]
|
|
30
31
|
```
|
|
31
32
|
|
|
@@ -38,6 +39,7 @@ repos: [ ... ]
|
|
|
38
39
|
| `claude` | object | no | (defaults) | See [claude](#claude). |
|
|
39
40
|
| `transport` | object | **yes** | — | See [transport](#transport). |
|
|
40
41
|
| `dashboard` | object | no | (defaults) | See [dashboard](#dashboard). |
|
|
42
|
+
| `web` | object | no | (defaults) | The read-only status page. See [web](#web). |
|
|
41
43
|
| `repos` | list | no | `[]` | See [repos](#repos). |
|
|
42
44
|
| `schedules` | object | no | (defaults) | See [schedules](#schedules). |
|
|
43
45
|
| `personalization` | object | no | unset | Cross-machine sync of Claude state. See [personalization](#personalization) and [Personalization sync]({{ '/personalization/' | relative_url }}). |
|
|
@@ -86,7 +88,78 @@ claude:
|
|
|
86
88
|
|
|
87
89
|
ctrlrelay always invokes claude with `--dangerously-skip-permissions` so the
|
|
88
90
|
agent does not pause on tool-permission prompts in headless runs. This is
|
|
89
|
-
intentional — the orchestrator runs unattended.
|
|
91
|
+
intentional — the orchestrator runs unattended. The CLI's other unattended
|
|
92
|
+
modes either need every shell command allow-listed or filter by command text,
|
|
93
|
+
which a wrapper script walks past, so they are not used as a guard. The guard
|
|
94
|
+
is the next section: the agent holds no GitHub credentials.
|
|
95
|
+
|
|
96
|
+
### What the agent can reach (#219)
|
|
97
|
+
|
|
98
|
+
The dev and task agents run **without GitHub access**. ctrlrelay starts them
|
|
99
|
+
with:
|
|
100
|
+
|
|
101
|
+
- every `GH_*` and `GITHUB_*` variable removed (`GH_TOKEN`, `GITHUB_TOKEN`,
|
|
102
|
+
`GH_ENTERPRISE_TOKEN`, ...), and every inherited `CTRLRELAY_*` variable
|
|
103
|
+
(the chat bot tokens) except the two the checkpoint needs;
|
|
104
|
+
- `GH_CONFIG_DIR` pointed at a fresh empty directory, so `gh` finds no login;
|
|
105
|
+
- `DBUS_SESSION_BUS_ADDRESS` pointed at nothing, so `gh` and other libsecret
|
|
106
|
+
clients cannot fall back to the desktop keyring;
|
|
107
|
+
- `credential.helper` reset to empty (through `GIT_CONFIG_COUNT`),
|
|
108
|
+
`GIT_SSH_COMMAND=false` and `SSH_AUTH_SOCK` removed, so git cannot
|
|
109
|
+
authenticate over HTTPS or SSH, including through an `insteadOf` rewrite;
|
|
110
|
+
- `GIT_TERMINAL_PROMPT=0`, so nothing waits on a prompt.
|
|
111
|
+
|
|
112
|
+
Everything else is inherited unchanged, so the model CLI and the project's
|
|
113
|
+
toolchain behave as before. ctrlrelay then does the GitHub side itself, with
|
|
114
|
+
its own credentials, after the agent exits:
|
|
115
|
+
|
|
116
|
+
- **dev:** the agent commits on the run's branch and writes a PR title and
|
|
117
|
+
description in its DONE checkpoint. ctrlrelay pushes the branch and opens
|
|
118
|
+
the pull request. After a CI or conflict fix round it pushes again, and it
|
|
119
|
+
fetches the failed-job logs the agent used to read with `gh run view`.
|
|
120
|
+
- **task:** the agent writes its report in its DONE checkpoint and ctrlrelay
|
|
121
|
+
posts it as the issue comment.
|
|
122
|
+
- **secops** still runs **with** the operator's GitHub access. Its agent
|
|
123
|
+
reads Dependabot alerts and pull requests and merges what the policy
|
|
124
|
+
allows during the run. Moving it would mean ctrlrelay doing those reads
|
|
125
|
+
up front and the merges afterwards from a decision the agent returns.
|
|
126
|
+
|
|
127
|
+
Pushes are refused, and logged as `publish.push.refused`, for `main`,
|
|
128
|
+
`master`, the repository's default branch (read from the GitHub API, not
|
|
129
|
+
from the local clone) and any branch that is not the run's own. The push
|
|
130
|
+
goes to `https://github.com/<owner>/<repo>.git` (never `origin`) from a
|
|
131
|
+
private git dir with hooks off and only ctrlrelay's own git config, pushes the
|
|
132
|
+
commit it checked rather than the ref name, and uses `--force-with-lease`
|
|
133
|
+
against the head this session last pushed (recorded in the state DB). An
|
|
134
|
+
open PR found for the branch is reused only if its head repo is this repo and
|
|
135
|
+
its base is the default branch. Text the agent wrote is posted with `@`-mentions and
|
|
136
|
+
issue-closing keywords disabled and its length capped (20,000 characters);
|
|
137
|
+
the PR's own `Closes #N` is added by ctrlrelay.
|
|
138
|
+
|
|
139
|
+
**What this guarantees.** No GitHub credential is handed to the agent through
|
|
140
|
+
its environment. ctrlrelay chooses the push URL, the branch and the PR base.
|
|
141
|
+
`main`, `master`, the default branch and any branch other than the run's own
|
|
142
|
+
are refused, and a branch that already exists on GitHub is only pushed over if
|
|
143
|
+
its head is one ctrlrelay recorded pushing. Agent-written text is sanitized.
|
|
144
|
+
An agent that only uses its normal tooling (git, gh, ssh as configured) cannot
|
|
145
|
+
push. The push itself runs from a fresh relay-owned git dir under
|
|
146
|
+
`~/.ctrlrelay/push/`, reads the clone's branch heads from its ref files, and
|
|
147
|
+
never reads the clone's config, which every worktree shares.
|
|
148
|
+
|
|
149
|
+
**What it does not guarantee.** The agent still runs as your user. A
|
|
150
|
+
deliberately hostile agent can read `~/.config/gh/hosts.yml`, `~/.ssh` and
|
|
151
|
+
anything else in your home directory and use them by hand; it can write
|
|
152
|
+
anywhere your user can, including your dotfiles and ctrlrelay's install; and
|
|
153
|
+
it can write to `~/.ctrlrelay/push/`, the bare clone's object store and any
|
|
154
|
+
`objects/info/alternates` chain it adds there, racing a push. On macOS the
|
|
155
|
+
login keychain is reachable whatever the environment says. Closing that needs
|
|
156
|
+
a separate user or a read-only home (#236). The next setting hides the
|
|
157
|
+
credential paths; it does not stop the writes.
|
|
158
|
+
|
|
159
|
+
| Key | Type | Default | Description |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `sandbox` | `"none"` or `"bwrap"` | `"none"` | `"bwrap"` runs the dev and task agents under bubblewrap with `sandbox_hide_paths` replaced by empty mounts. Linux only; needs `bwrap` installed and unprivileged user namespaces allowed (on Ubuntu 24.04+ an AppArmor profile for `bwrap`). ctrlrelay refuses to start if it is set and `bwrap` is missing. |
|
|
162
|
+
| `sandbox_hide_paths` | list of strings | `~/.ssh`, `~/.config/gh`, `~/.git-credentials`, `~/.config/git/credentials`, `~/.netrc`, `~/.local/share/keyrings`, `$XDG_RUNTIME_DIR/bus` | Paths hidden under `sandbox: bwrap`. A directory becomes an empty tmpfs, a file or socket becomes `/dev/null`, a missing path is skipped. |
|
|
90
163
|
|
|
91
164
|
## transport
|
|
92
165
|
|
|
@@ -205,6 +278,36 @@ dashboard:
|
|
|
205
278
|
The dashboard is optional. Leaving `url` empty (the default) is the supported
|
|
206
279
|
no-op configuration.
|
|
207
280
|
|
|
281
|
+
## web
|
|
282
|
+
|
|
283
|
+
The read-only status page served by `ctrlrelay web start` (see
|
|
284
|
+
[Operations: status page]({{ '/operations/' | relative_url }}#status-page)). Every key is optional;
|
|
285
|
+
without a `web:` block the defaults below apply.
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
web:
|
|
289
|
+
host: "127.0.0.1"
|
|
290
|
+
port: 8787
|
|
291
|
+
history_days: 7
|
|
292
|
+
refresh_seconds: 5
|
|
293
|
+
github_check_seconds: 60
|
|
294
|
+
stale_after_minutes: 15
|
|
295
|
+
allowed_hosts: [] # e.g. ["mybox", "mybox.lan"]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Key | Type | Default | Description |
|
|
299
|
+
|---|---|---|---|
|
|
300
|
+
| `host` | string | `"127.0.0.1"` | Address to listen on, exactly. Loopback by default so a fresh install is never reachable from other machines. Set it to this machine's LAN address to open the page from the home network. `0.0.0.0` listens on every interface and is warned about at startup. |
|
|
301
|
+
| `port` | int (1-65535) | `8787` | Port to listen on. |
|
|
302
|
+
| `history_days` | int (>= 1) | `7` | Days of feed the page shows, and what `/api/events` returns without `?since=`. |
|
|
303
|
+
| `refresh_seconds` | int (>= 1) | `5` | How often the page asks `/api/status` again. |
|
|
304
|
+
| `github_check_seconds` | int (>= 15) | `60` | How often the waiting line asks GitHub (one search, plus one query per `include_labels` label). |
|
|
305
|
+
| `stale_after_minutes` | int (>= 1) | `15` | A running card is marked "looks stuck" after this long with no new agent step, unless ctrlrelay itself has a CI wait or review open for it. |
|
|
306
|
+
| `allowed_hosts` | list of strings | `[]` | Extra names the page may be opened by. A request is served only when its `Host` header is `localhost`, `127.0.0.1`, `[::1]`, `host` (unless it is `0.0.0.0`/`::`), this machine's hostname, the hostname plus `.local`, or one of these, alone or followed by `:port`. Anything else, or no `Host`, gets `421`. Matching ignores case and a trailing dot. This stops a web page on another site from pointing its own name at this server (DNS rebinding) and reading it. |
|
|
307
|
+
|
|
308
|
+
The page has no login and shows repository names, issue titles and the
|
|
309
|
+
questions agents asked. **Never expose it to the internet.**
|
|
310
|
+
|
|
208
311
|
## repos
|
|
209
312
|
|
|
210
313
|
A list of repositories the orchestrator manages.
|
|
@@ -253,7 +356,7 @@ and ask the operator), or `never` (skip).
|
|
|
253
356
|
| `exclude_labels` | `["manual", "operator", "instruction"]` | Issue labels that tell the poller "this isn't for the agent". See [exclude_labels](#reposautomationexclude_labels) below. |
|
|
254
357
|
| `include_labels` | `[]` | Issue labels that opt an issue **into** the dev pipeline regardless of who is (or isn't) assigned. See [include_labels](#reposautomationinclude_labels) below. |
|
|
255
358
|
| `require_labels` | `[]` | Issue labels that gate the plain-assignment trigger: when set, assignment alone is no longer enough — the issue must also carry one of these labels. See [require_labels](#reposautomationrequire_labels) below. |
|
|
256
|
-
| `ci_wait_timeout_seconds` | `600` |
|
|
359
|
+
| `ci_wait_timeout_seconds` | `600` | Unused since #219 (the agent no longer waits on CI; ctrlrelay does). Still accepted so existing configs load. See [ci_wait_timeout_seconds](#reposautomationci_wait_timeout_seconds) below. |
|
|
257
360
|
|
|
258
361
|
The current secops and dev pipelines read these settings to bias their prompts
|
|
259
362
|
to Claude — they're not enforced by hard-coded checks.
|
|
@@ -400,7 +503,12 @@ repos:
|
|
|
400
503
|
|
|
401
504
|
### repos[].automation.ci_wait_timeout_seconds
|
|
402
505
|
|
|
403
|
-
|
|
506
|
+
**Not used by the dev pipeline since #219.** The agent no longer has GitHub
|
|
507
|
+
access, so it no longer waits on CI: ctrlrelay pushes the branch after the
|
|
508
|
+
agent exits and its own PR verifier waits on CI. The key is still accepted so
|
|
509
|
+
existing configs load. What follows describes the earlier behaviour.
|
|
510
|
+
|
|
511
|
+
The dev pipeline prompt told Claude to run `ctrlrelay ci wait --pr <PR>
|
|
404
512
|
--repo <repo> --timeout <N>` before signaling DONE — a single blocking call
|
|
405
513
|
that polls GitHub every 15s until CI finishes, fails, or the timeout hits.
|
|
406
514
|
No tokens get generated while it's blocked; on a repo with a slow CI suite,
|
|
@@ -151,9 +151,12 @@ Step-by-step:
|
|
|
151
151
|
15–17. **Loop until terminal.** If the resumed session blocks again, steps
|
|
152
152
|
8–14 repeat (capped at `DEFAULT_MAX_BLOCKED_ROUNDS`, currently 5). On
|
|
153
153
|
`DONE`/`FAILED`, the loop exits.
|
|
154
|
-
18. **
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
18. **Publish and verify.** On `DONE` the agent has only committed; it has
|
|
155
|
+
no GitHub access (#219). ctrlrelay pushes the run's branch, opens the PR
|
|
156
|
+
from the title and description in the checkpoint, then verifies the PR
|
|
157
|
+
is mergeable and CI is green. If not, it resumes the agent with the
|
|
158
|
+
failed-job logs, pushes the fix itself and re-verifies (up to
|
|
159
|
+
`DEFAULT_MAX_FIX_ATTEMPTS`, currently 3).
|
|
157
160
|
19. **Operator notified.** The poller pushes a final `✅ PR ready: <url>` (or
|
|
158
161
|
`❌ Failed: …`) message via the bridge.
|
|
159
162
|
|
|
@@ -21,6 +21,7 @@ ctrlrelay generates platform-appropriate service unit files for you with
|
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| Bridge | `ctrlrelay bridge start` | Delivers BLOCKED questions to Telegram and routes answers back. |
|
|
23
23
|
| Poller | `ctrlrelay poller start --interval 300` | Watches GitHub for newly assigned issues and runs the dev pipeline. |
|
|
24
|
+
| Status page (optional) | `ctrlrelay web start` | Read-only page and JSON API of what ctrlrelay is doing. See [Status page](#status-page). |
|
|
24
25
|
|
|
25
26
|
Both should restart on failure and on login.
|
|
26
27
|
|
|
@@ -234,6 +235,97 @@ systemctl --user status ctrlrelay-poller
|
|
|
234
235
|
journalctl --user -u ctrlrelay-poller -f
|
|
235
236
|
```
|
|
236
237
|
|
|
238
|
+
## Status page
|
|
239
|
+
|
|
240
|
+
`ctrlrelay web start` serves a read-only page and JSON API showing what each
|
|
241
|
+
agent is doing (phase, current activity, last command), what is waiting and
|
|
242
|
+
why, the questions that need an answer, and a feed of what ctrlrelay did and
|
|
243
|
+
said. It is a third process next to the bridge and the poller, so a slow or
|
|
244
|
+
broken page can never stall them.
|
|
245
|
+
|
|
246
|
+
On a running card, **phase** (the bar) is the furthest step the session has
|
|
247
|
+
reached, from the agent's transcript plus ctrlrelay's own CI, review and PR
|
|
248
|
+
events, so it never moves back. **Current activity** is the most recent step:
|
|
249
|
+
an open CI wait or review if it started after the agent's latest step, else
|
|
250
|
+
that step. It can be earlier than the bar, for example a read after the PR
|
|
251
|
+
or a fix round after review. The card links the PR once ctrlrelay has
|
|
252
|
+
recorded its number.
|
|
253
|
+
|
|
254
|
+
It is **read-only**: it opens `state.db` read-only, reads the tail of each
|
|
255
|
+
agent's transcript, and asks GitHub (through the `gh` login ctrlrelay already
|
|
256
|
+
uses) for the waiting line once a minute. It writes nothing but its PID file
|
|
257
|
+
(`web.pid`, next to `state.db`), and serves `GET` only; any other method is
|
|
258
|
+
`405`. It has **no login** and shows repository names, issue titles and the
|
|
259
|
+
questions agents asked, so **never expose it to the internet**. Keep it on
|
|
260
|
+
loopback or on the home network.
|
|
261
|
+
|
|
262
|
+
| Path | What |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `/` | The page. Refreshes itself every `web.refresh_seconds`. |
|
|
265
|
+
| `/api/status` | Everything on the page, as JSON (the contract is in `src/ctrlrelay/web/status.py`). A section that could not be read is `null` and `errors` says why; `[]` means "read, and empty". The feed holds at most the newest `feed_limit` (200) events: `feed_total` is how many the history window holds (`null` if unreadable) and `feed_truncated` is `true` when some were left out, in which case the page says "showing newest N of M". |
|
|
266
|
+
| `/api/events?since=<epoch seconds>` | The feed, newest first, at most `limit` (200) events. Without `since`, the last `web.history_days` days. Carries the same `feed_total` and `feed_truncated`, plus the `limit` used. |
|
|
267
|
+
|
|
268
|
+
The page header shows how long ago it last heard from the server ("updated
|
|
269
|
+
12s ago", ticking every second) and a dot that is green while the last good
|
|
270
|
+
answer is at most two refresh intervals old and red otherwise. Elapsed times
|
|
271
|
+
on cards tick between polls too.
|
|
272
|
+
|
|
273
|
+
Fields added in #232 (all in `/api/status`):
|
|
274
|
+
|
|
275
|
+
| Field | What |
|
|
276
|
+
|---|---|
|
|
277
|
+
| `url` | On every `running`, `needs_you`, `waiting.items`, `skipped` and `feed` item (and on `/api/events` events): a link to the GitHub issue, or for a feed event that names a PR by number, the PR. Built only as `https://github.com/<owner>/<repo>/issues/<n>` or `/pull/<n>`, from a repo matching `owner/name` and an integer; `null` otherwise (a secops run with no issue, a restart). A `pr_url` an agent reported is never used. |
|
|
278
|
+
| `skipped` | Every open issue assigned to you that the poller will **not** pick up and is not running, from the same search as the waiting line (no extra GitHub calls): `repo`, `issue_number`, `title`, `url`, `updated`/`updated_ts` and `reason`, one of `repo not in config`, `excluded label <label>`, `already handled once (won't retry)`, `missing required label <label>` (several joined with "or": any one admits it), `assigned by someone else`. `null` until GitHub has answered once; `waiting.status` and `waiting.last_good_ts` say how current it is. The page shows it collapsed, with a count. |
|
|
279
|
+
| `server` | This machine's health, read on each request: `load_1m`, `load_5m`, `load_15m`, `cpu_count`, `memory` (`total_bytes`, `available_bytes`, `available_pct`, `low`), `disks` (one each for `home` and `worktrees`: `name`, `path`, `total_bytes`, `free_bytes`, `free_pct`, `low`), `uptime_s`, and `unavailable` (why any number is `null`; memory and uptime are `null` off Linux). `low` is true below 10% free and the page marks it. The whole section is `null`, with a line in `errors`, only if reading it failed outright. |
|
|
280
|
+
|
|
281
|
+
Try it on loopback first (the default `web.host` is `127.0.0.1`, port `8787`):
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
ctrlrelay web start
|
|
285
|
+
ctrlrelay web status
|
|
286
|
+
curl -s http://127.0.0.1:8787/api/status | python3 -m json.tool | head -40
|
|
287
|
+
curl -s "http://127.0.0.1:8787/api/events?since=$(( $(date +%s) - 3600 ))"
|
|
288
|
+
ctrlrelay web stop
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
To open it from another machine on the home network, set `web.host` in
|
|
292
|
+
`orchestrator.yaml` to this machine's LAN address (see
|
|
293
|
+
[web]({{ '/configuration/' | relative_url }}#web)), then run it under
|
|
294
|
+
systemd. `ctrlrelay install systemd` renders `ctrlrelay-web.service` with the
|
|
295
|
+
other two units. It refuses to overwrite units that already exist, so on a
|
|
296
|
+
host where the bridge and poller are installed, render into a scratch
|
|
297
|
+
directory and copy only the web unit:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
ctrlrelay install systemd --workdir ~/Projects/ctrlrelay --target-dir ~/ctrlrelay-units
|
|
301
|
+
cp ~/ctrlrelay-units/ctrlrelay-web.service ~/.config/systemd/user/
|
|
302
|
+
rm -r ~/ctrlrelay-units # the other two carry the chat token
|
|
303
|
+
systemctl --user daemon-reload
|
|
304
|
+
systemctl --user enable --now ctrlrelay-web.service
|
|
305
|
+
journalctl --user -u ctrlrelay-web -n 20
|
|
306
|
+
tail -n 20 ~/.ctrlrelay/logs/web.log
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The first line in `web.log` is a `web.started` event naming the address it
|
|
310
|
+
bound. If `web.host` is `0.0.0.0` it also logs a warning: that listens on
|
|
311
|
+
every interface, which is rarely what you want.
|
|
312
|
+
|
|
313
|
+
The page answers only to names it knows: `localhost`, `127.0.0.1`, `[::1]`,
|
|
314
|
+
`web.host`, and this machine's own hostname (plus `.local`). A request whose
|
|
315
|
+
`Host` header carries any other name gets `421 misdirected request`, and the
|
|
316
|
+
refused name is logged once as `web.misdirected_host`. So if you open it from
|
|
317
|
+
another machine by a name that is none of those, for example a DNS name your
|
|
318
|
+
router hands out, add that name to `web.allowed_hosts` and restart the unit:
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
web:
|
|
322
|
+
host: "192.168.1.20"
|
|
323
|
+
allowed_hosts: ["mybox.lan"]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`grep misdirected_host ~/.ctrlrelay/logs/web.log` shows any name that was
|
|
327
|
+
refused, which is the quickest way to find the one to add.
|
|
328
|
+
|
|
237
329
|
## Scheduled jobs
|
|
238
330
|
|
|
239
331
|
The poller daemon also hosts an in-process cron (APScheduler). The jobs run
|
|
@@ -300,6 +392,7 @@ Tail them:
|
|
|
300
392
|
tail -f ~/.ctrlrelay/logs/poller.log
|
|
301
393
|
tail -f ~/.ctrlrelay/logs/bridge.log
|
|
302
394
|
tail -f ~/.ctrlrelay/logs/poller.error.log
|
|
395
|
+
tail -f ~/.ctrlrelay/logs/web.log # if the status page is installed
|
|
303
396
|
```
|
|
304
397
|
|
|
305
398
|
The poller prints one line per detected issue and one line per pipeline outcome.
|