py-ib-controller 0.1.2__tar.gz → 0.1.4__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.
Files changed (78) hide show
  1. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/workflows/ci.yml +1 -1
  2. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/workflows/publish.yml +4 -3
  3. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/PKG-INFO +51 -5
  4. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/README.md +50 -4
  5. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/AgentMain.java +16 -1
  6. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/docs/Configuration.md +21 -0
  7. py_ib_controller-0.1.4/docs/development.md +252 -0
  8. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/cli.py +78 -9
  9. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/config.py +89 -9
  10. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/control_loop.py +120 -29
  11. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/ibcontroller.toml.example +21 -5
  12. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/ibkr_settings.toml.example +3 -23
  13. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/labels.json +14 -1
  14. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/ibcontroller-agent.jar +0 -0
  15. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/labels.py +44 -0
  16. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/launcher.py +139 -33
  17. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/logging_setup.py +76 -9
  18. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/login.py +31 -7
  19. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/recognisers.py +155 -16
  20. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller.env +14 -4
  21. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/PKG-INFO +51 -5
  22. py_ib_controller-0.1.4/py_ib_controller.egg-info/scm_version.json +8 -0
  23. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_cli.py +116 -2
  24. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_config.py +141 -3
  25. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_control_loop.py +280 -8
  26. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_labels.py +9 -0
  27. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_launcher.py +175 -3
  28. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_logging_setup.py +43 -0
  29. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_login.py +87 -24
  30. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_recognisers.py +155 -4
  31. py_ib_controller-0.1.2/docs/development.md +0 -132
  32. py_ib_controller-0.1.2/py_ib_controller.egg-info/scm_version.json +0 -8
  33. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/ISSUE_TEMPLATE.md +0 -0
  34. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/dependabot.yml +0 -0
  35. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.gitignore +0 -0
  36. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.pre-commit-config.yaml +0 -0
  37. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.sdkmanrc +0 -0
  38. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/CONTRIBUTING.md +0 -0
  39. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/LICENSE.txt +0 -0
  40. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/Makefile +0 -0
  41. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/ComponentLookup.java +0 -0
  42. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/EventBridge.java +0 -0
  43. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/Protocol.java +0 -0
  44. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/Security.java +0 -0
  45. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/WriteOps.java +0 -0
  46. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/docs/Architecture.svg +0 -0
  47. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/__init__.py +0 -0
  48. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/actions.py +0 -0
  49. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/agent_client.py +0 -0
  50. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/app_dirs.py +0 -0
  51. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/builtin_settings.toml +0 -0
  52. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/labels.json.example +0 -0
  53. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/diagnostics.py +0 -0
  54. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/dispatch.py +0 -0
  55. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/main.py +0 -0
  56. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/py.typed +0 -0
  57. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/schedule.py +0 -0
  58. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/settings.py +0 -0
  59. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/SOURCES.txt +0 -0
  60. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/dependency_links.txt +0 -0
  61. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/entry_points.txt +0 -0
  62. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/requires.txt +0 -0
  63. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/scm_file_list.json +0 -0
  64. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/top_level.txt +0 -0
  65. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/pyproject.toml +0 -0
  66. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/setup.cfg +0 -0
  67. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/__init__.py +0 -0
  68. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/conftest.py +0 -0
  69. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/fakes.py +0 -0
  70. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_actions.py +0 -0
  71. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_agent_client.py +0 -0
  72. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_app_dirs.py +0 -0
  73. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_diagnostics.py +0 -0
  74. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_dispatch.py +0 -0
  75. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_main.py +0 -0
  76. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_schedule.py +0 -0
  77. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_settings.py +0 -0
  78. {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_trace_integration.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  - uses: actions/checkout@v7
22
22
 
23
23
  - name: Install uv and Python
24
- uses: astral-sh/setup-uv@v10.1.0
24
+ uses: astral-sh/setup-uv@v10.2.0
25
25
  with:
26
26
  python-version: ${{ matrix.python-version }}
27
27
 
@@ -26,7 +26,7 @@ jobs:
26
26
  fetch-depth: 0
27
27
 
28
28
  - name: Install uv and Python
29
- uses: astral-sh/setup-uv@v10.1.0
29
+ uses: astral-sh/setup-uv@v10.2.0
30
30
  with:
31
31
  python-version: ${{ env.PYTHON_VERSION }}
32
32
 
@@ -95,21 +95,22 @@ jobs:
95
95
  ./dist/*.tar.gz ./dist/*.whl
96
96
 
97
97
  - name: Generate changelog
98
+ id: git-cliff
98
99
  uses: orhun/git-cliff-action@v4
99
100
  with:
100
101
  # no cliff.toml -- git-cliff's built-in default config already
101
102
  # groups Conventional Commits (feat/fix/docs/...) correctly
102
103
  args: --latest --tag ${{ github.ref_name }} --strip header
103
- output: CHANGELOG_release.md
104
104
 
105
105
  - name: Assemble release notes
106
106
  env:
107
107
  TAG: ${{ github.ref_name }}
108
108
  REPO: ${{ github.repository }}
109
+ CHANGELOG: ${{ steps.git-cliff.outputs.content }}
109
110
  run: |
110
111
  PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)
111
112
  {
112
- cat CHANGELOG_release.md
113
+ echo "$CHANGELOG"
113
114
  echo
114
115
  echo "## Installation"
115
116
  echo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-ib-controller
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Login/launch controller for TWS and IBKR Gateway, via an in-process Java agent
5
5
  Author-email: gnzsnz <8376642+gnzsnz@users.noreply.github.com>
6
6
  License-Expression: Apache-2.0
@@ -52,7 +52,9 @@ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --pr
52
52
 
53
53
  https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
54
54
 
55
- To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
55
+ You can check the full list of [cli parameters](#cli-parameters) or [config keys](#configuration).
56
+
57
+ To it build from source:
56
58
 
57
59
  ```bash
58
60
  # 0. Fetch the code
@@ -70,9 +72,11 @@ uv run ibcontroller init
70
72
  uv run ibcontroller run
71
73
  ```
72
74
 
75
+ For a detailed build guide please check the [development guide](./docs/development.md).
76
+
73
77
  `ibcontroller run` scaffolds the config directory automatically the first time
74
78
  `ibcontroller.toml` is missing (same effect as running `init` first), so it can also be
75
- run directly on a fresh install -- it will still fail with a clear error naming whichever
79
+ run directly on a fresh install. It will still fail with a clear error naming whichever
76
80
  setting is actually missing (in practice, just the two credential env vars).
77
81
 
78
82
  ### CLI parameters
@@ -179,6 +183,11 @@ Each also accepts a `_FILE`-suffixed variant (`IBC_USERID_FILE`,
179
183
  `IBC_PASSWORD_FILE`) that reads the value from a file instead —
180
184
  Docker/Compose secrets, so a value never has to sit in the process environment.
181
185
 
186
+ The startup `IBC_*` environment dump masks any variable whose name contains
187
+ `PASSWORD`, `USERID`, `SECRET`, `TOKEN`, `TOTP` or `KEY` (e.g.
188
+ `IBC_PASSWORD_PAPER`, set by other tooling), except `_FILE` variants, which
189
+ only hold a path.
190
+
182
191
  ### File location
183
192
 
184
193
  When environment variable `IBC_APP_DIR` is not set, then
@@ -224,6 +233,8 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
224
233
  | `log_dir` | `IBC_LOG_DIR` | Where ibcontroller's own log file lives; always resolved at startup | platform default |
225
234
  | `log_level` | `IBC_LOG_LEVEL` | `debug`/`info`/`warning`/`error` | `info` |
226
235
  | `log_sink` | `IBC_LOG_SINK` | `"std"` (console only) or `"file"` (only, under `log_dir`) — exclusive, not both. Covers `ibcontroller-{instance}.log` and `gateway-{instance}.log` only; the Java agent log and the wire trace are always file, unaffected — see "Logging" below | `"std"` |
236
+ | `log_max_bytes` | `IBC_LOG_MAX_BYTES` | Rotate log/trace files at this size in bytes; `0` disables rotation. Only respected when `log_sink="file"` | `10485760` (10 MiB) |
237
+ | `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
227
238
  | `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
228
239
  | `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
229
240
  | (env only) | `IBC_APP_DIR`, `--app-dir` | Override file locations (config/log/run) for container mode; `--app-dir`/`init`/`run` set the env var for the invocation, same effect |
@@ -241,8 +252,10 @@ TWS/ibgateway settings
241
252
  | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
242
253
  | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
243
254
  | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
255
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` fires | `"exit"` |
244
256
  | `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | `"hh:mm AM/PM"` daily auto-restart time. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
245
257
  | `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | `"hh:mm AM/PM"` daily auto-logoff time; same "Lock and Exit" radio-button pair as `auto_restart_time` -- if both are set, `auto_restart_time` wins | `None` |
258
+ | `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone`. Auto-detected from `TZ`, then `/etc/localtime` -- no setup needed in the common case; never overwrites an existing `TimeZone` line | auto-detected |
246
259
  | `cold_restart_time` | `IBC_COLD_RESTART_TIME` | TWS and Gateway. `"HH:MM"` 24-hour local time; every Sunday, ibcontroller closes the instance tidily and relaunches with a full fresh login, forcing IBKR's weekly Sunday 01:00 US/Eastern token-invalidation reauth -- not a GUI setting, see "Scheduled shutdown" below | `None` |
247
260
  | `closedown_at` | `IBC_CLOSEDOWN_AT` | TWS and Gateway. `"HH:MM"` (daily) or `"<Weekday> HH:MM"` (weekly); closes the instance tidily at that time, no relaunch -- not a GUI setting, see "Scheduled shutdown" below | `None` |
248
261
 
@@ -288,7 +301,7 @@ US/Eastern invalidation window if that's the intent.
288
301
 
289
302
  ## Declarative configuration
290
303
 
291
- Beyond `ibcontroller.toml`, three things let you shape ibcontroller's behavior without
304
+ Beyond `ibcontroller.toml`, two things let you shape ibcontroller's behavior without
292
305
  writing any Python: **`labels.json`** (window/button text, plus simple pop-up dismissal
293
306
  rules) and **`ibkr_settings.toml`** (Global Configuration settings applied to Gateway/TWS
294
307
  at startup). Each is covered below.
@@ -409,10 +422,17 @@ scrollback.
409
422
  | --- | --- | --- | --- |
410
423
  | `ibcontroller-{instance}.log` | ibcontroller app control flow | `log_sink=file` (else console) | `log_level` (default `info`) |
411
424
  | `gateway-{instance}.log` | TWS/Gateway own stdout/stderr | `log_sink=file` (else console) | TWS/Gateway's own |
412
- | `ibcontroller-java-agent-{instance}.log` | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
425
+ | `ibcontroller-java-agent-{instance}.log` (see rotation note below) | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
413
426
  | `cmd-{instance}.jsonl` | raw wire: commands sent + results, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
414
427
  | `events-{instance}.jsonl` | raw wire: every event message received, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
415
428
 
429
+ **Rotation** (`IBC_LOG_MAX_BYTES`/`IBC_LOG_BACKUP_COUNT`, default 10 MiB x 5
430
+ backups, **on by default**) applies to every file above in file mode. Set
431
+ `IBC_LOG_MAX_BYTES=0` to disable it (unbounded files). Python's files keep
432
+ their usual name active, with `.1`/`.2`/... as old backups. The Java agent log
433
+ is the opposite: once rotation is on, the active file is
434
+ `ibcontroller-java-agent-{instance}.log.0`, not the unsuffixed name.
435
+
416
436
  ### Log levels
417
437
 
418
438
  `IBC_LOG_LEVEL` (`debug`/`info`/`warning`/`error`, default `info`) controls
@@ -484,6 +504,32 @@ or both.
484
504
  instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
485
505
  without interleaving, even if copied or globbed out of their directory.
486
506
 
507
+ ## Exit codes
508
+
509
+ `ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
510
+ code, grouped by what a supervisor (a container's `restart:` policy, systemd's
511
+ `Restart=`) should do about it. The specific cause is always in the logs and in the
512
+ `stopped: <CAUSE>` line.
513
+
514
+ | Code | Meaning | Supervisor action |
515
+ | --- | --- | --- |
516
+ | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close) | Do not restart |
517
+ | `1` | Unhandled exception (a bug) | Restart, report |
518
+ | `2` | CLI usage error | Fix the command |
519
+ | `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
520
+ | `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
521
+ | `75` (`EX_TEMPFAIL`) | 2FA not approved in time, `mfa_timeout_action=exit` | Retry per policy |
522
+ | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready, login timed out with no 2FA dialog) | Retry |
523
+
524
+ `77`/`78` work with systemd's `RestartPreventExitStatus=`.
525
+
526
+ Some login-blocking dialogs IB reports as a server-side condition -- not a credential
527
+ rejection -- never reach this table at all: a "Login failed"/"Login Error" dialog, or a
528
+ Gateway "Connection to server failed" reason other than a known credential/account one
529
+ (e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
530
+ login, unbounded, the same way a scheduled `cold_restart_time` does -- the container
531
+ itself never exits or restarts for these.
532
+
487
533
  ## License
488
534
 
489
535
  ```text
@@ -30,7 +30,9 @@ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --pr
30
30
 
31
31
  https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
32
32
 
33
- To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
33
+ You can check the full list of [cli parameters](#cli-parameters) or [config keys](#configuration).
34
+
35
+ To it build from source:
34
36
 
35
37
  ```bash
36
38
  # 0. Fetch the code
@@ -48,9 +50,11 @@ uv run ibcontroller init
48
50
  uv run ibcontroller run
49
51
  ```
50
52
 
53
+ For a detailed build guide please check the [development guide](./docs/development.md).
54
+
51
55
  `ibcontroller run` scaffolds the config directory automatically the first time
52
56
  `ibcontroller.toml` is missing (same effect as running `init` first), so it can also be
53
- run directly on a fresh install -- it will still fail with a clear error naming whichever
57
+ run directly on a fresh install. It will still fail with a clear error naming whichever
54
58
  setting is actually missing (in practice, just the two credential env vars).
55
59
 
56
60
  ### CLI parameters
@@ -157,6 +161,11 @@ Each also accepts a `_FILE`-suffixed variant (`IBC_USERID_FILE`,
157
161
  `IBC_PASSWORD_FILE`) that reads the value from a file instead —
158
162
  Docker/Compose secrets, so a value never has to sit in the process environment.
159
163
 
164
+ The startup `IBC_*` environment dump masks any variable whose name contains
165
+ `PASSWORD`, `USERID`, `SECRET`, `TOKEN`, `TOTP` or `KEY` (e.g.
166
+ `IBC_PASSWORD_PAPER`, set by other tooling), except `_FILE` variants, which
167
+ only hold a path.
168
+
160
169
  ### File location
161
170
 
162
171
  When environment variable `IBC_APP_DIR` is not set, then
@@ -202,6 +211,8 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
202
211
  | `log_dir` | `IBC_LOG_DIR` | Where ibcontroller's own log file lives; always resolved at startup | platform default |
203
212
  | `log_level` | `IBC_LOG_LEVEL` | `debug`/`info`/`warning`/`error` | `info` |
204
213
  | `log_sink` | `IBC_LOG_SINK` | `"std"` (console only) or `"file"` (only, under `log_dir`) — exclusive, not both. Covers `ibcontroller-{instance}.log` and `gateway-{instance}.log` only; the Java agent log and the wire trace are always file, unaffected — see "Logging" below | `"std"` |
214
+ | `log_max_bytes` | `IBC_LOG_MAX_BYTES` | Rotate log/trace files at this size in bytes; `0` disables rotation. Only respected when `log_sink="file"` | `10485760` (10 MiB) |
215
+ | `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
205
216
  | `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
206
217
  | `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
207
218
  | (env only) | `IBC_APP_DIR`, `--app-dir` | Override file locations (config/log/run) for container mode; `--app-dir`/`init`/`run` set the env var for the invocation, same effect |
@@ -219,8 +230,10 @@ TWS/ibgateway settings
219
230
  | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
220
231
  | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
221
232
  | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
233
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` fires | `"exit"` |
222
234
  | `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | `"hh:mm AM/PM"` daily auto-restart time. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
223
235
  | `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | `"hh:mm AM/PM"` daily auto-logoff time; same "Lock and Exit" radio-button pair as `auto_restart_time` -- if both are set, `auto_restart_time` wins | `None` |
236
+ | `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone`. Auto-detected from `TZ`, then `/etc/localtime` -- no setup needed in the common case; never overwrites an existing `TimeZone` line | auto-detected |
224
237
  | `cold_restart_time` | `IBC_COLD_RESTART_TIME` | TWS and Gateway. `"HH:MM"` 24-hour local time; every Sunday, ibcontroller closes the instance tidily and relaunches with a full fresh login, forcing IBKR's weekly Sunday 01:00 US/Eastern token-invalidation reauth -- not a GUI setting, see "Scheduled shutdown" below | `None` |
225
238
  | `closedown_at` | `IBC_CLOSEDOWN_AT` | TWS and Gateway. `"HH:MM"` (daily) or `"<Weekday> HH:MM"` (weekly); closes the instance tidily at that time, no relaunch -- not a GUI setting, see "Scheduled shutdown" below | `None` |
226
239
 
@@ -266,7 +279,7 @@ US/Eastern invalidation window if that's the intent.
266
279
 
267
280
  ## Declarative configuration
268
281
 
269
- Beyond `ibcontroller.toml`, three things let you shape ibcontroller's behavior without
282
+ Beyond `ibcontroller.toml`, two things let you shape ibcontroller's behavior without
270
283
  writing any Python: **`labels.json`** (window/button text, plus simple pop-up dismissal
271
284
  rules) and **`ibkr_settings.toml`** (Global Configuration settings applied to Gateway/TWS
272
285
  at startup). Each is covered below.
@@ -387,10 +400,17 @@ scrollback.
387
400
  | --- | --- | --- | --- |
388
401
  | `ibcontroller-{instance}.log` | ibcontroller app control flow | `log_sink=file` (else console) | `log_level` (default `info`) |
389
402
  | `gateway-{instance}.log` | TWS/Gateway own stdout/stderr | `log_sink=file` (else console) | TWS/Gateway's own |
390
- | `ibcontroller-java-agent-{instance}.log` | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
403
+ | `ibcontroller-java-agent-{instance}.log` (see rotation note below) | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
391
404
  | `cmd-{instance}.jsonl` | raw wire: commands sent + results, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
392
405
  | `events-{instance}.jsonl` | raw wire: every event message received, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
393
406
 
407
+ **Rotation** (`IBC_LOG_MAX_BYTES`/`IBC_LOG_BACKUP_COUNT`, default 10 MiB x 5
408
+ backups, **on by default**) applies to every file above in file mode. Set
409
+ `IBC_LOG_MAX_BYTES=0` to disable it (unbounded files). Python's files keep
410
+ their usual name active, with `.1`/`.2`/... as old backups. The Java agent log
411
+ is the opposite: once rotation is on, the active file is
412
+ `ibcontroller-java-agent-{instance}.log.0`, not the unsuffixed name.
413
+
394
414
  ### Log levels
395
415
 
396
416
  `IBC_LOG_LEVEL` (`debug`/`info`/`warning`/`error`, default `info`) controls
@@ -462,6 +482,32 @@ or both.
462
482
  instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
463
483
  without interleaving, even if copied or globbed out of their directory.
464
484
 
485
+ ## Exit codes
486
+
487
+ `ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
488
+ code, grouped by what a supervisor (a container's `restart:` policy, systemd's
489
+ `Restart=`) should do about it. The specific cause is always in the logs and in the
490
+ `stopped: <CAUSE>` line.
491
+
492
+ | Code | Meaning | Supervisor action |
493
+ | --- | --- | --- |
494
+ | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close) | Do not restart |
495
+ | `1` | Unhandled exception (a bug) | Restart, report |
496
+ | `2` | CLI usage error | Fix the command |
497
+ | `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
498
+ | `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
499
+ | `75` (`EX_TEMPFAIL`) | 2FA not approved in time, `mfa_timeout_action=exit` | Retry per policy |
500
+ | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready, login timed out with no 2FA dialog) | Retry |
501
+
502
+ `77`/`78` work with systemd's `RestartPreventExitStatus=`.
503
+
504
+ Some login-blocking dialogs IB reports as a server-side condition -- not a credential
505
+ rejection -- never reach this table at all: a "Login failed"/"Login Error" dialog, or a
506
+ Gateway "Connection to server failed" reason other than a known credential/account one
507
+ (e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
508
+ login, unbounded, the same way a scheduled `cold_restart_time` does -- the container
509
+ itself never exits or restarts for these.
510
+
465
511
  ## License
466
512
 
467
513
  ```text
@@ -133,6 +133,14 @@ public final class AgentMain {
133
133
  * Python and read here -- the agent never invents paths or levels itself. When
134
134
  * {@code ibcontroller.logfile} is absent (e.g. {@code make run}), logs go to
135
135
  * stderr only, same as before this mechanism existed.
136
+ *
137
+ * <p>Rotation (gitea #32): {@code -Dibcontroller.logfile.maxbytes=<N>} and
138
+ * {@code -Dibcontroller.logfile.backupcount=<N>} mirror Python's own
139
+ * {@code log_max_bytes}/{@code log_backup_count}. {@code FileHandler}'s own
140
+ * fd can't be rotated externally (the JVM keeps writing to a renamed/unlinked
141
+ * inode), so this uses its 4-arg constructor, which does native generation
142
+ * rotation on the JVM's own thread. {@code maxbytes=0} (the default if the
143
+ * property is absent) keeps the old unbounded, append-only file.
136
144
  */
137
145
  private static void configureLogging() throws Exception {
138
146
  Logger root = Logger.getLogger("");
@@ -145,7 +153,14 @@ public final class AgentMain {
145
153
 
146
154
  String logFile = System.getProperty("ibcontroller.logfile");
147
155
  if (logFile != null && !logFile.isEmpty()) {
148
- FileHandler fileHandler = new FileHandler(logFile, true);
156
+ int maxBytes = Integer.parseInt(
157
+ System.getProperty("ibcontroller.logfile.maxbytes", "0"));
158
+ int backupCount = Integer.parseInt(
159
+ System.getProperty("ibcontroller.logfile.backupcount", "0"));
160
+ FileHandler fileHandler =
161
+ maxBytes > 0
162
+ ? new FileHandler(logFile, maxBytes, Math.max(backupCount, 1), true)
163
+ : new FileHandler(logFile, true);
149
164
  fileHandler.setFormatter(new SimpleFormatter());
150
165
  root.addHandler(fileHandler);
151
166
  } else {
@@ -24,6 +24,10 @@ accepts a `_FILE`-suffixed variant that reads the value from a file instead
24
24
  (Docker/Compose secrets): `IBC_USERID` / `IBC_PASSWORD`,
25
25
  `IBC_USERID_FILE` / `IBC_PASSWORD_FILE`.
26
26
 
27
+ The startup `IBC_*` environment dump masks any variable whose name contains
28
+ `PASSWORD`, `USERID`, `SECRET`, `TOKEN`, `TOTP` or `KEY` (e.g.
29
+ `IBC_PASSWORD_PAPER`), except `_FILE` variants, which only hold a path.
30
+
27
31
  ## IBController specific settings
28
32
 
29
33
  Settings that drive the behaviour of `ibcontroller` itself.
@@ -60,6 +64,7 @@ Settings related to TWS/ibgateway.
60
64
  | `existing_session_action` | `IBC_EXISTING_SESSION_ACTION` | What to do when an existing session is detected -- see below | `"manual"` |
61
65
  | `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | Daily auto-restart time, `"hh:mm AM/PM"` -- see below | `None` (unchanged) |
62
66
  | `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | Daily auto-logoff time, `"hh:mm AM/PM"` -- see below | `None` (unchanged) |
67
+ | `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone` -- see below | auto-detected |
63
68
 
64
69
  ### Accept Incoming Connection
65
70
 
@@ -129,6 +134,21 @@ Time above, just selecting "Auto logoff" instead of "Auto restart". Omit this
129
134
  key (the default) to leave the existing setting unchanged. If both keys are
130
135
  set, Auto Restart Time wins -- set only one of the two in practice.
131
136
 
137
+ ### Time Zone
138
+
139
+ **key:** `time_zone`
140
+ **Environment variable:** `IBC_TIME_ZONE`
141
+
142
+ IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone` key
143
+ (under `[Logon]`) before every launch. No setup needed in the common case:
144
+ auto-detected from the container's own `TZ` environment variable first, then
145
+ from `/etc/localtime`'s zoneinfo symlink target; set `IBC_TIME_ZONE`
146
+ explicitly only when the desired TWS/Gateway session time zone should differ
147
+ from the host/container's own. Never overwrites a `TimeZone` line already
148
+ present in `jts.ini` -- a value found there was set deliberately, either by
149
+ you or by an earlier run of ibcontroller itself. Leaves the key untouched if
150
+ nothing resolves.
151
+
132
152
  ### Cold Restart Time
133
153
 
134
154
  **key:** `cold_restart_time`
@@ -169,6 +189,7 @@ TWS/Gateway itself.
169
189
  | `mfa_timeout` | `IBC_MFA_TIMEOUT` | The real 2FA budget in seconds, mirroring IBKR's own external limit | `180.0` |
170
190
  | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart the login attempt if 2FA times out (instead of giving up) | `false` |
171
191
  | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the wait after a timed-out 2FA when relogin is enabled | `60.0` |
192
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | What to do when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` expires: `exit` (exit 75) or `restart` (relaunch with a fresh login) | `exit` |
172
193
 
173
194
  ## Diagnostics
174
195
 
@@ -0,0 +1,252 @@
1
+ # Development environment
2
+
3
+ How to get `ibcontroller` itself, and the Java agent it drives, running locally.
4
+
5
+ ## Target platform
6
+
7
+ The supported platforms are macOS and Linux.
8
+
9
+ ## Python toolchain
10
+
11
+ ```bash
12
+ curl -LsSf https://astral.sh/uv/install.sh | sh # if uv isn't already installed
13
+ cd ibcontroller
14
+ uv sync # installs the dev dependency group too
15
+ ```
16
+
17
+ Requires Python 3.12+ (pinned in `.python-version`). Day-to-day commands:
18
+
19
+ ```bash
20
+ uv run pytest # test suite (pytest-asyncio, pytest-aiohttp already in the dev group)
21
+ uv run ruff check . # lint
22
+ uv run ruff format . # format
23
+ uv run pyrefly check # type check
24
+ uv run bandit -c pyproject.toml -r ibcontroller/ # security lint
25
+ ```
26
+
27
+ ## pre-commit
28
+
29
+ `.pre-commit-config.yaml` (project root) runs ruff (lint + format), bandit, pyrefly, and basic
30
+ hygiene hooks (trailing whitespace, end-of-file, yaml/toml checks, large-file check) on every
31
+ commit — the same checks as the `uv run` commands above, wired to run automatically instead of by
32
+ hand.
33
+
34
+ ```bash
35
+ uv run pre-commit install # one-time per clone: installs the git hook
36
+ uv run pre-commit run --all-files # run every hook against the whole tree, not just staged files
37
+ ```
38
+
39
+ CI runs the same config, so a clean local `pre-commit run --all-files` is a reliable predictor of
40
+ CI passing.
41
+
42
+ ## Java toolchain
43
+
44
+ The agent needs to build against, and run compatibly with, the JRE TWS/Gateway bundles.
45
+
46
+ **Local dev JDK, via SDKMAN:** a real Azul account/API token is needed for the exact SA-tier build
47
+ above; without one, the closest public match is Zulu's CA (Certified Availability) tier at the
48
+ same JDK version. SDKMAN manages that install and pins the version per project:
49
+
50
+ One-time install (adds SDKMAN's init to your shell rc automatically):
51
+
52
+ ```bash
53
+ curl -s "https://get.sdkman.io" | bash
54
+ # install
55
+ sdk install java 17.0.15-zulu
56
+ ```
57
+
58
+ The pinned Java version lives in `.sdkmanrc` at the project root (checked in):
59
+
60
+ ```env
61
+ java=17.0.15-zulu
62
+ ```
63
+
64
+ Activate it per clone — SDKMAN then sets `JAVA_HOME` for the current shell:
65
+
66
+ ```bash
67
+ sdk env use # reads .sdkmanrc; installs the pinned JDK on first use
68
+ java -version
69
+ openjdk version "17.0.15" 2025-04-15 LTS
70
+ OpenJDK Runtime Environment Zulu17.58+21-CA (build 17.0.15+6-LTS)
71
+ OpenJDK 64-Bit Server VM Zulu17.58+21-CA (build 17.0.15+6-LTS, mixed mode, sharing)
72
+ ```
73
+
74
+ Enable auto-env (`sdkman_auto_env=true` in `~/.sdkman/etc/config`) to have `JAVA_HOME` set
75
+ automatically when entering the project directory, instead of running `sdk env use` in every new
76
+ terminal. Either way, `JAVA_HOME` is the contract: the `Makefile` reads it (`JDK ?= $(JAVA_HOME)`),
77
+ and CI's `actions/setup-java` (zulu 17) sets the same variable, so local and CI build with the
78
+ same pin.
79
+
80
+ This JDK is for *compiling* only — its own version doesn't need to match whatever Gateway/TWS
81
+ happens to bundle locally . The `Makefile`'s `--release 17` tells `javac` to target Java 17
82
+ bytecode/API regardless of which JDK compiles it, so the output runs correctly on whatever
83
+ JRE TWS/gateway is using.
84
+
85
+ The agent is small, so keep the build as plain
86
+ as it can be: `javac` + `jar`, wired through a small `Makefile`.
87
+
88
+ ## TWS / IBKR Gateway itself
89
+
90
+ Download from IBKR directly [gateway](https://www.interactivebrokers.com/en/trading/ibgateway-latest.php) or [TWS](https://www.interactivebrokers.com/en/trading/download-tws.php). Then install.
91
+
92
+ After the first run, `ibcontroller` renames the gateway/TWS script. This is required to manage restarts.
93
+
94
+ ## Building and attaching the agent
95
+
96
+ Java agent file structure:
97
+
98
+ ```text
99
+ agent/src/main/java/ibcontroller/agent/
100
+ AgentMain.java — entry point: both sockets, EventBridge installed before Gateway launches
101
+ Protocol.java — command accept loop, hand-rolled JSON codec, full command dispatch
102
+ ComponentLookup.java — read-only Swing tree walk, dump/get_text
103
+ WriteOps.java — EDT-dispatched writes, set_text/set_checkbox/click
104
+ EventBridge.java — AWTEventListener install + NDJSON event forwarding
105
+ Security.java — password redaction, the credential-field boundary
106
+ ```
107
+
108
+ Build using`Makefile`:
109
+
110
+ ```bash
111
+ make build # build/ibcontroller-agent.jar, then copies it into ibcontroller/ (for uv/pyproject packaging)
112
+ make dist # build the jar, then uv build the Python package (jar is required package-data, see Makefile)
113
+ make clean # rm -rf build dist
114
+ ```
115
+
116
+ Day-to-day cycle: `make build` after any agent-side change, `uv run pytest` for the Python side,
117
+ `pre-commit run --all-files` before pushing.
118
+
119
+ ## Running ibcontroller
120
+
121
+ ```bash
122
+ # from the git repo
123
+ source .venv/bin/activate
124
+ ibcontroller run --dotenv=.env-paper --trading-mode=paper --program=gateway
125
+ ```
126
+
127
+ ## Architecture
128
+
129
+ `ibcontroller` follows a layered architecture with separation of concerns between layers.
130
+ Any contribution must follow this separation of concerns.
131
+
132
+ ![ibcontroller schematic architecture](Architecture.svg)
133
+
134
+ ### Rules for every layer
135
+
136
+ - A layer talks only to the layer directly below it. The exceptions are listed per layer.
137
+ - L3's schema types (`WindowEvent`, `WindowInfo`, `Component`, …) and exceptions are the shared
138
+ vocabulary of every layer above L3. Changing them is a cross-layer change.
139
+ - Everything runs on one asyncio loop. No blocking call on the loop: file and process work goes
140
+ through `anyio.to_thread` or the logging listener threads.
141
+ - One object graph per running instance (`Dispatcher`, `LoginManager`, recogniser registry, sockets,
142
+ settings directory). No module-level state shared between instances.
143
+ - Every user-facing string (window titles, button and menu labels) comes from `labels.json` through
144
+ `labels.py`. Never a Python literal.
145
+ - Credentials stay `Secret` until L3 builds the request line. Nothing logs or traces a credential
146
+ value.
147
+
148
+ ### L1 Process (`launcher.py`)
149
+
150
+ - **Owns:** TWS/Gateway install discovery (macOS and Linux, channel filtering), JRE selection, the
151
+ JVM command line, the per-instance settings directory and socket paths, native-restart
152
+ prevention, spawning the JVM with the agent, draining its stdout, and readiness (`ping`).
153
+ - **Provides:** `LaunchedInstance`: the process, a started `Dispatcher` (L4), socket paths,
154
+ settings directory.
155
+ - **Rules:** one launch per call. Relaunch policy belongs to L6. L1 may construct L3 and L4 objects
156
+ ("spawn and attach").
157
+ - **Known deviation:** `clean_shutdown` lives here and uses L5-1. It belongs to Management (L5).
158
+
159
+ ### L2 Java agent (`agent/`)
160
+
161
+ - **Owns:** everything inside the TWS/Gateway JVM: starting the entry point, the AWT window
162
+ listener and window-ID registry, the command socket (request/response, one client at a time),
163
+ the event socket (`hello`, `snapshot`, `window_opened`/`window_closed`, `keepalive`, `overflow`),
164
+ running every UI read and write on the EDT, and credential redaction (`dump` redacts, `get_text`
165
+ refuses).
166
+ - **Provides:** the wire protocol to L3.
167
+ - **Rules:** the agent reports and executes; it makes no login, dialog or lifecycle decisions. Any
168
+ protocol change updates L3 in the same change.
169
+
170
+ ### L3 Agent client (`agent_client.py`)
171
+
172
+ - **Owns:** the two socket connections, JSON line framing, the typed message schemas
173
+ (`attrs`/`cattrs`), and the typed exceptions mapping L2's error codes.
174
+ - **Provides:** one typed method per agent command; an async iterator over event messages.
175
+ - **Rules:** no scheduling, no fan-out, no imports from other `ibcontroller` modules. `set_text` and
176
+ `set_text_near_label` are the only places a `Secret` is unwrapped.
177
+
178
+ ### L4 Dispatcher (`dispatch.py`)
179
+
180
+ - **Owns:** one `Dispatcher` per instance: both L3 connections, command serialization (one FIFO
181
+ worker, one command on the wire at a time), event fan-out, and the optional wire trace.
182
+ - **Provides:** `send_command`, `window_events`, `tasks` (for L6 to detect a lost connection),
183
+ `start`/`stop`.
184
+ - **Rules:** never interprets commands or events beyond routing them.
185
+
186
+ ### L5-1 Actions (`actions.py`, `labels.py`)
187
+
188
+ - **Owns:** the action vocabulary (`click`, `type_text`, `toggle`, `navigate_menu`, `expand_tree`,
189
+ `dump`, `wait_for_event`, …) and the label data.
190
+ - **Rules:** the only L5 code that calls `send_command`. L3 exceptions propagate unchanged. Actions
191
+ take a `window_id` wherever the agent supports one.
192
+
193
+ ### L5 Domains
194
+
195
+ | Domain | Code | Role |
196
+ | ---------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
197
+ | Login | `login.py`, login-outcome recognisers in `recognisers.py` | Fill credentials, detect success, MFA and failure, report the outcome. |
198
+ | Dialogs | `RecognizerRegistry`, `watch_for_unprompted_windows` | Recognise and handle windows nobody is waiting for. |
199
+ | Settings | `settings.py`, `builtin_settings.toml`, `ibkr_settings.toml` | Apply declared Global Configuration settings. |
200
+ | Scheduling | `schedule.py` | Wall-clock maths for cold restart and close-down. No I/O. |
201
+ | Diagnostic | `diagnostics.py` | Dump window structure for development and support. |
202
+ | Management | not built | Graceful shutdown, restart and other lifecycle operations on the UI. |
203
+
204
+ - **Rules:**
205
+ - Act on the UI only through L5-1.
206
+ - Report outcomes to L6 as return values or typed exceptions. L6 decides relaunch, exit or
207
+ continue.
208
+ - Domains do not call each other; L6 wires them together.
209
+ - A recogniser acts only on the window it matched (`window_id`). The dialogs registry never acts
210
+ on a window with a visible credential field.
211
+ - **Known deviations:**
212
+ - `recognisers.py` and `diagnostics.py` subscribe to `window_events` directly instead of through
213
+ L5-1.
214
+ - The registry construction and the settings sequence live in `control_loop.py`.
215
+ - Management's pieces live in L1 (`clean_shutdown`) and L6 (scheduled shutdown).
216
+
217
+ ### L6 Control loop (`control_loop.py`)
218
+
219
+ - **Owns:** one instance's lifecycle, one cycle per JVM: launch → login (with the dialogs watcher) →
220
+ settings → ready → shutdown. Races every phase against process exit and connection loss,
221
+ classifies why the cycle ended (`ShutdownCause`), applies the relaunch policy, and owns every
222
+ task started during a cycle.
223
+ - **Provides:** `run_control_loop`, cancelled to request a graceful stop. `cli.py` maps its result
224
+ and exceptions to exit codes.
225
+ - **Rules:** the only layer that decides relaunch or stop. Cancellation is the only stop request.
226
+
227
+ ### L7 Control API (planned, not built)
228
+
229
+ The contract below is provisional: it fixes the boundaries, not the interface.
230
+
231
+ - **Purpose:** let something outside the process observe a running instance and request lifecycle
232
+ operations on it: an operator, a supervisor, a container health check.
233
+ - **Owns:** the external interface: transport, access control, request validation, response
234
+ format, and mapping each request to an L6 or Management operation.
235
+ - **Consumes:** L6, for lifecycle requests and instance state (startup state, login state, last
236
+ `ShutdownCause`), and Management (L5) operations, through L6.
237
+ - **Rules:**
238
+ - Never reaches L4, L3 or L2 directly, and never drives the UI through L5-1 itself. A UI
239
+ operation is a Management operation, so serialization, window scoping and logging stay in L5.
240
+ - Never stops, kills or relaunches the process itself. It asks L6, which stays the only owner of
241
+ the cycle.
242
+ - Requests are intents, not blocking calls: a long operation is accepted, and its progress is
243
+ observable through state.
244
+ - Scoped to one instance, like every other layer.
245
+ - Local-only by default. Remote access needs explicit configuration, like IBC's `BindAddress` and
246
+ `ControlFrom`.
247
+ - Never returns credentials. Diagnostic output follows the same redaction as `dump`.
248
+ - **Candidate operations, not committed:** status; stop; restart and cold restart; IBC
249
+ `CommandServer` parity (`STOP`, `EXIT`, `RESTART`, `ENABLEAPI`, `RECONNECTDATA`,
250
+ `RECONNECTACCOUNT`); a diagnostic dump.
251
+ - **Open questions:** transport and access control; which IBC commands to support; whether
252
+ user-authored rules and plugins attach at L7 or at L5.