continuity-guard 0.5.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.
Files changed (76) hide show
  1. continuity_guard-0.5.0/CHANGELOG.md +253 -0
  2. continuity_guard-0.5.0/CONTRIBUTING.md +58 -0
  3. continuity_guard-0.5.0/Dockerfile +47 -0
  4. continuity_guard-0.5.0/LICENSE +21 -0
  5. continuity_guard-0.5.0/MANIFEST.in +17 -0
  6. continuity_guard-0.5.0/PKG-INFO +768 -0
  7. continuity_guard-0.5.0/README.md +722 -0
  8. continuity_guard-0.5.0/SECURITY.md +95 -0
  9. continuity_guard-0.5.0/TESTING.md +512 -0
  10. continuity_guard-0.5.0/config.example.toml +227 -0
  11. continuity_guard-0.5.0/continuity_guard/__init__.py +14 -0
  12. continuity_guard-0.5.0/continuity_guard/actions.py +230 -0
  13. continuity_guard-0.5.0/continuity_guard/cli.py +565 -0
  14. continuity_guard-0.5.0/continuity_guard/client.py +311 -0
  15. continuity_guard-0.5.0/continuity_guard/config.example.toml +227 -0
  16. continuity_guard-0.5.0/continuity_guard/config.py +243 -0
  17. continuity_guard-0.5.0/continuity_guard/daemon.py +1060 -0
  18. continuity_guard-0.5.0/continuity_guard/detectors.py +500 -0
  19. continuity_guard-0.5.0/continuity_guard/handoff.py +195 -0
  20. continuity_guard-0.5.0/continuity_guard/integrations.py +259 -0
  21. continuity_guard-0.5.0/continuity_guard/protocol.py +212 -0
  22. continuity_guard-0.5.0/continuity_guard/proxy.py +414 -0
  23. continuity_guard-0.5.0/continuity_guard/quota.py +572 -0
  24. continuity_guard-0.5.0/continuity_guard/state.py +200 -0
  25. continuity_guard-0.5.0/continuity_guard/supervise.py +250 -0
  26. continuity_guard-0.5.0/continuity_guard/transport.py +131 -0
  27. continuity_guard-0.5.0/continuity_guard.egg-info/PKG-INFO +768 -0
  28. continuity_guard-0.5.0/continuity_guard.egg-info/SOURCES.txt +74 -0
  29. continuity_guard-0.5.0/continuity_guard.egg-info/dependency_links.txt +1 -0
  30. continuity_guard-0.5.0/continuity_guard.egg-info/entry_points.txt +5 -0
  31. continuity_guard-0.5.0/continuity_guard.egg-info/top_level.txt +1 -0
  32. continuity_guard-0.5.0/docs/README.md +23 -0
  33. continuity_guard-0.5.0/docs/account-window.md +150 -0
  34. continuity_guard-0.5.0/docs/calibrate.md +124 -0
  35. continuity_guard-0.5.0/docs/confirmation-window.md +133 -0
  36. continuity_guard-0.5.0/docs/failure-mode-evidence.md +220 -0
  37. continuity_guard-0.5.0/docs/integrating.md +148 -0
  38. continuity_guard-0.5.0/docs/platform-support.md +71 -0
  39. continuity_guard-0.5.0/docs/quota-accuracy.md +149 -0
  40. continuity_guard-0.5.0/docs/quota-adapters.md +139 -0
  41. continuity_guard-0.5.0/docs/v0.4.2-hardening-and-backstop.md +163 -0
  42. continuity_guard-0.5.0/docs/wrapup-signal.md +262 -0
  43. continuity_guard-0.5.0/pyproject.toml +47 -0
  44. continuity_guard-0.5.0/setup.cfg +4 -0
  45. continuity_guard-0.5.0/systemd/continuity-guard.service +30 -0
  46. continuity_guard-0.5.0/tests/armed_real_agent.sh +110 -0
  47. continuity_guard-0.5.0/tests/bench_accuracy.py +487 -0
  48. continuity_guard-0.5.0/tests/campaign/agent.py +257 -0
  49. continuity_guard-0.5.0/tests/campaign/burn_claude.py +164 -0
  50. continuity_guard-0.5.0/tests/campaign/burn_review.py +223 -0
  51. continuity_guard-0.5.0/tests/campaign/run_burn.sh +101 -0
  52. continuity_guard-0.5.0/tests/campaign/run_phase.py +235 -0
  53. continuity_guard-0.5.0/tests/fake_cli_agent.sh +47 -0
  54. continuity_guard-0.5.0/tests/fuzz_protocol.py +423 -0
  55. continuity_guard-0.5.0/tests/mock_openai_server.py +143 -0
  56. continuity_guard-0.5.0/tests/pi_verify.sh +129 -0
  57. continuity_guard-0.5.0/tests/real_faults.sh +159 -0
  58. continuity_guard-0.5.0/tests/run_matrix.sh +309 -0
  59. continuity_guard-0.5.0/tests/soak_stability.py +429 -0
  60. continuity_guard-0.5.0/tests/test_authority.py +306 -0
  61. continuity_guard-0.5.0/tests/test_ceiling_signal.py +193 -0
  62. continuity_guard-0.5.0/tests/test_packaging.py +122 -0
  63. continuity_guard-0.5.0/tests/test_protocol.py +102 -0
  64. continuity_guard-0.5.0/tests/test_quota.py +350 -0
  65. continuity_guard-0.5.0/tests/test_wrapup_race.py +151 -0
  66. continuity_guard-0.5.0/tests/verify_quota_tracking.py +163 -0
  67. continuity_guard-0.5.0/tools/cg_proxy.py +19 -0
  68. continuity_guard-0.5.0/tools/cg_supervise.py +19 -0
  69. continuity_guard-0.5.0/tools/install_phase1_units.sh +162 -0
  70. continuity_guard-0.5.0/tools/langgraph_agent_loop.py +261 -0
  71. continuity_guard-0.5.0/tools/make_windows_proof.py +172 -0
  72. continuity_guard-0.5.0/tools/phase1.py +466 -0
  73. continuity_guard-0.5.0/tools/real_agent_loop.py +466 -0
  74. continuity_guard-0.5.0/tools/real_session_loop.py +352 -0
  75. continuity_guard-0.5.0/tools/setup_nucbox.sh +152 -0
  76. continuity_guard-0.5.0/tools/soak.py +390 -0
@@ -0,0 +1,253 @@
1
+ # Changelog
2
+
3
+ All notable changes to Continuity Guard are recorded here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project uses a
5
+ simple `MAJOR.MINOR.PATCH` scheme.
6
+
7
+ ## [0.5.0] - 2026-08-29
8
+
9
+ ### Fixed
10
+ - **`pipx install` could not run the documented integration paths.** The wheel
11
+ ships `packages = ["continuity_guard"]`, so `tools/` was never in it, while
12
+ README and `docs/integrating.md` told users to run `python -m tools.cg_proxy`
13
+ and `tools/cg_supervise.py`. Both raised `ModuleNotFoundError` on every
14
+ installed copy. Worse, `cg_supervise` holds the ONLY `write_cache("anthropic")`
15
+ call, so `cg quota` could never show a Claude percentage after a clean
16
+ install -- the README's own opening example was unreachable for the user it
17
+ addressed. Every test and campaign run executes from a checkout where
18
+ `tools/` is simply on the path, which is why nothing caught it.
19
+
20
+ The proxy and supervisor now live in the package as `continuity_guard.proxy`
21
+ and `continuity_guard.supervise`, with `cg-proxy` and `cg-supervise` console
22
+ entry points. `tools/*.py` remain as thin shims so existing invocations by
23
+ path -- systemd units, running phase-1 loops -- keep working unchanged.
24
+
25
+ ### Added
26
+ - **`cg wrap`** -- run a CLI agent under the watchdog with no code and no
27
+ config: `cg wrap -- claude -p "..."`. Observe-only regardless of profile,
28
+ and it says so on the first line before it does anything. Arming stays a
29
+ separate deliberate act in the daemon's configuration.
30
+
31
+ ### Changed
32
+ - Version 0.5.0 rather than 0.4.3. Wrap-up that actually reaches the agent,
33
+ three working ceilings, named plan windows, a socket that is not a kill
34
+ oracle, spend that survives a restart, and confirm-polls on quota are not a
35
+ patch release, and shipping them under 0.4.2 would confuse anyone who
36
+ installed last week.
37
+ - Docs reconciled with HEAD: `HANDOFF.md` tree pointer, its 19/19 vs 21/21
38
+ contradiction, and its claim that the socket is `0666` "on purpose" (false
39
+ since the authority fix, and dangerous as guidance); README's matrix listing
40
+ stopped at row 18 under a "21 checks" heading; `docs/quota-adapters.md` still
41
+ said adapters were unimplemented, Codex unverified, and Claude published no
42
+ percentage -- all three now settled and marked as the research record.
43
+
44
+ ## [Unreleased]
45
+
46
+ ### Changed
47
+ - **The handoff claim is narrowed to what it can actually deliver.** The docs
48
+ described "every long session ends with its state written down" and "the thing
49
+ that means I never lose a run". Neither survives contact with the mechanism:
50
+ what is preserved is the run's observed reach and its *enumerable* progress,
51
+ not the agent's understanding — the unarticulated hypothesis, the discarded
52
+ approach and why. That is context, it dies with the context, and summarising
53
+ it is lossy in a way the reader cannot audit. The claim is now "a warm start
54
+ on enumerable work, and a guarantee that the run's reach is never unknown",
55
+ with the two limits that follow from it stated: task shape dominates the
56
+ value, and findings decay (measured — two of four findings in one handoff were
57
+ already fixed hours later, and a third was wrong on its own terms). See
58
+ README "What the handoff is and is not" and `docs/wrapup-signal.md` "The
59
+ claim, narrowed to what holds".
60
+ - **Handoffs carry findings, not an index.** The agent-written document listed
61
+ which areas were touched and nothing about what was found, which is a
62
+ bookmark: a successor could not continue from it without redoing the work.
63
+ It now carries the task, condensed findings with observed-at stamps, what was
64
+ ruled out, what remains, and the exact command that resumes past the work
65
+ already done. Ceiling-aware — a budget does not "reset", and saying so sent a
66
+ reader to wait for something that never comes.
67
+
68
+ ### Security
69
+ - **An armed daemon could be made to terminate arbitrary processes.** Four
70
+ facts combined: the Unix socket was created world-writable (`0o666`);
71
+ `protocol.sanitize()` type-checked HELLO fields but did not authorise them,
72
+ so peer-supplied `overrides` became effective policy *including* `action`;
73
+ and `actions._identity_ok()` fell back to "is anything alive with this PID"
74
+ whenever `start_time` was absent. Any local account could announce another
75
+ process's PID with `start_time=0` and a policy that terminated on the first
76
+ frame, and the daemon would signal that process and its group — a kill
77
+ primitive for everything the daemon's user could signal. Found by a
78
+ high-reasoning review of `daemon.py`/`detectors.py`, then confirmed
79
+ end-to-end before it was believed.
80
+
81
+ Four independent fixes, because an operator will eventually widen one of them
82
+ for a good reason: the socket defaults to `0o600` (`CG_SOCKET_MODE` widens it
83
+ deliberately); operator-only keys are stripped from peer overrides and the
84
+ removal is logged; termination requires a verified `start_time` where the
85
+ platform can supply one, and refuses rather than assuming where it cannot;
86
+ and the announced PID must be the connecting peer or a descendant of it
87
+ (`SO_PEERCRED`), failing which termination is disarmed for that session while
88
+ observation continues. `tests/test_authority.py` reproduces the original
89
+ exploit — the victim process dies against the pre-fix code and survives
90
+ after.
91
+
92
+ ### Fixed
93
+ - **The hard backstop could be widened by a learned baseline, breaking
94
+ invariant 4.** `_adaptive_floor` applied its `is_backstop` guard only to the
95
+ flat learning margin, so `max(configured, base * adaptive_k)` widened the
96
+ hard ceiling anyway — both while learning and after the baseline froze. A
97
+ 30-minute hard silence ceiling with `adaptive_k = 6` and one learned
98
+ 20-minute gap became **120 minutes**: a frozen process sat undetected for two
99
+ hours under a configuration that asked for thirty. The guard is now a single
100
+ unconditional return, since the bug was a branch that forgot it.
101
+ - **Ceilings reset when the daemon restarted.** Every HELLO built a fresh
102
+ session with `total_tokens = 0` and a new clock, so a client reconnecting
103
+ after a restart (systemd `Restart=always`, an OOM) got a full budget and a
104
+ fresh deadline and could spend the whole allowance twice. Accumulated spend
105
+ and the original start time are now persisted (`session_carry`) and restored
106
+ on reconnect, bounded by age so a session id reused days later starts clean.
107
+ - **EOF was recorded as death without checking the process.** A supervisor
108
+ exiting or a TCP reset produced EOF while the agent ran on; that was written
109
+ to the ledger as a death and could drive `flap`. Where the PID is checkable
110
+ and still alive, the daemon now says what it actually observed.
111
+ - **Unbounded idle connections.** A peer could open connections, never send a
112
+ HELLO, and hold them; there was no cap and no handshake timeout, so fds and
113
+ buffers grew until real agents could not connect. The fuzzer closes its
114
+ sockets, so its churn never built this up. Now capped (`CG_MAX_CONNS`, 512)
115
+ with a handshake timeout (`CG_HANDSHAKE_TIMEOUT`, 60s).
116
+ - **Short writes were treated as delivered.** `conn.send()` on a non-blocking
117
+ socket returns a byte count that was discarded, so under output-buffer
118
+ pressure a truncated WRAPUP was logged as sent — the client drops the
119
+ malformed line and never learns a ceiling is near.
120
+ - **Silent `status.json` write failures** left `cg status` stale while looking
121
+ authoritative. Reported once, and again when it changes.
122
+ - **Three stale claims, found by the tool reviewing itself.** A ceiling-stopped
123
+ accuracy run handed over four reviews; three carried findings and all three
124
+ were real: `TESTING.md:24` unpacked a `v0.4.1` tarball, `README.md:393` quoted
125
+ `any-level FP 0.6%` where the measured figure is 0.1%, and
126
+ `tests/run_matrix.sh:2` still described "19 realtime checks" after row #20
127
+ landed. The last was introduced by the commit that added that row.
128
+ - **The ceiling wrap-up signal never reached the agent.** `progress()` only
129
+ writes to the socket, so the sole reader was the process-wide link watcher,
130
+ whose loop sleeps `WATCH_INTERVAL` (5s) before its first pass.
131
+ `should_wrap_up()` — the call the documentation tells agents to make —
132
+ answered from a field that had not been populated yet, so an agent that
133
+ checked the signal after its work and exited raced the timer and lost. It
134
+ only appeared to work when a unit of work happened to be long enough for a
135
+ tick to land inside it. `should_wrap_up()` and `headroom()` now drain the
136
+ socket first. Found against a real Claude plan window; see
137
+ `docs/wrapup-signal.md` for the timeline and the before/after table.
138
+ - **Handoffs printed the window reset time as a raw epoch** (`resets
139
+ 1787993400`) — the single field a reader most needs off that document. Now
140
+ rendered as a local timestamp. The wire frame still carries the integer.
141
+ - **`[account]` was ignored on SIGHUP.** `_reload()` rebuilt profiles and
142
+ defaults but never re-read the block, so a reload logged success and changed
143
+ neither the threshold, the grace, the poll interval, nor the background
144
+ classification.
145
+ - **`_quota_sent` held one value per session name and was never cleaned up.**
146
+ Broken twice over. The plan-window signal and the configured-ceiling signal
147
+ wrote the same key, so one silently unblocked the other — measured live, a
148
+ budget wrap-up stayed suppressed until the quota poll happened to overwrite
149
+ it, arriving at 5,839 tokens against a 1,700 cap instead of at 2,014. And
150
+ because nothing removed an entry, a session reusing a name that had already
151
+ been warned could never be warned again for the daemon's lifetime: a fail-open
152
+ on the one signal that exists to save work. Now a set per session, cleared on
153
+ close.
154
+ - **The account block was logged only when a value changed**, making silence
155
+ mean both "reloaded, nothing moved" and "never reloaded at all" — the
156
+ ambiguity that hid the bug above. The effective settings are now stated on
157
+ every reload and once at startup.
158
+
159
+ ### Added
160
+ - **`[account] windows`** — name which plan windows may bind. The daemon has
161
+ always used the *highest* window, which is right when you want protection from
162
+ whichever allowance runs out first, but it made "warn me at 15% of my session"
163
+ inexpressible: a 7d weekly sitting at 66% binds first, so every threshold below
164
+ that fires on the first poll having done no work, and writes an empty handoff.
165
+ Measured twice before this existed. An unmatched name binds nothing and the
166
+ daemon says so once per provider — silently protecting nobody looks exactly
167
+ like nothing being near a ceiling.
168
+ - **`budget` and `deadline` now signal.** `docs/wrapup-signal.md` has named
169
+ them the signallable ceilings since the feature was designed — "fired when a
170
+ ceiling detector returns warn" — but the only path that ever sent a WRAPUP
171
+ frame was the plan-window poll. A session with a token cap or a wall-clock cap
172
+ got a log line and then, at `hard`, a termination, with no chance in between
173
+ to save its work: two of the three ceilings the document claimed to cover were
174
+ silent. `daemon._ceiling_wrapup()`, reported with real headroom (tokens
175
+ remaining to `hard`, or seconds).
176
+ - **Matrix check #20 — the ceiling signal reaches the agent.** Two tests, both
177
+ regression-proven against the broken code rather than merely passing against
178
+ the fixed code. `tests/test_wrapup_race.py` (client visibility, link watcher
179
+ disabled entirely so the drain does the work rather than a lucky tick): 6 of 9
180
+ fail before the fix, 9/9 after. `tests/test_ceiling_signal.py` (real daemon;
181
+ `budget` and `deadline` delivery, and suppression not outliving a session):
182
+ 0 of 9 before, 9/9 after. The matrix is now 20 checks.
183
+
184
+ ## [0.4.2] - 2026-08-27
185
+
186
+ First release published to PyPI. `pipx install continuity-guard`.
187
+
188
+ ### Added
189
+ - **Cross-platform transport.** The daemon now listens on a Unix domain socket
190
+ on Linux/macOS and on `127.0.0.1:9099` on Windows; `socket = "tcp://HOST:PORT"`
191
+ (or `":PORT"`, or `CG_TCP_PORT`) forces TCP anywhere. `continuity_guard/transport.py`
192
+ is the single resolver used by the daemon, the client, the CLI, and the fuzzer.
193
+ - **`cg` command-line interface** — `init`, `install`, `run`, `status`, `watch`,
194
+ `history`, `profiles`, `reload`, `doctor`. `cg install` writes a systemd
195
+ `--user` unit on Linux, a launchd agent on macOS, and prints NSSM instructions
196
+ on Windows.
197
+ - **Packaging.** `pyproject.toml` with zero runtime dependencies, `cg` and
198
+ `cg-daemon` entry points, wheel + sdist, and a `Dockerfile` (python:3.12-slim,
199
+ non-root, baked default config).
200
+ - **`protocol.sanitize()`** — every decoded frame is normalised to well-typed
201
+ fields or `None` before it reaches a detector.
202
+ - **`decay.hard_floor`** — an absolute backstop (default 200s) under the
203
+ adaptive decay detector, so the terminate level can never be pulled down by a
204
+ learned baseline.
205
+ - **`Store.prune`** — history tables are bounded and checkpointed hourly from
206
+ the daemon; `PRAGMA wal_checkpoint(TRUNCATE)` runs every prune, not only when
207
+ rows were deleted.
208
+ - **Client reconnect.** A session whose daemon restarts under it (systemd
209
+ `Restart=always`, an OOM kill) re-connects and re-HELLOs with the same
210
+ identity, throttled and silent.
211
+ - **`docs/integrating.md`** — the four integration paths (proxy, library,
212
+ supervisor, callbacks), per-agent supervisor recipes, and the three wiring
213
+ footguns that cause a healthy agent to be killed.
214
+ - **`tests/soak_stability.py`** — a multi-day leak *assertion*: drives heavy
215
+ connection churn at a throwaway daemon, samples RSS / fds / threads / WAL, and
216
+ exits non-zero on a memory slope, an fd leak, thread growth, or a stalled
217
+ checkpoint.
218
+ - **`tests/fuzz_protocol.py`** — hostile-input fuzzing folded into the matrix as
219
+ check 19; runs against both Unix and TCP transports.
220
+
221
+ ### Fixed
222
+ - **`decay` hard-level false positives** went from 1.2% to 0.0% at high sample
223
+ counts (recall unchanged) after adding `hard_floor`.
224
+ - **Daemon crashes on hostile input** — a non-integer `pid`, deeply nested
225
+ JSON, and binary garbage each previously reached an unhandled exception in the
226
+ selector loop. `decode()` now catches `RecursionError`; framing is wrapped;
227
+ `sanitize()` runs first.
228
+ - **Accept backlog** under a connection flood — `_accept` now drains the whole
229
+ backlog per wakeup and the listen queue is 256.
230
+ - **`cg_proxy` gzip bug** — the proxy forwarded `Accept-Encoding: gzip` but
231
+ stripped `Content-Encoding`, producing a client-side decode error. It now
232
+ forces `Accept-Encoding: identity` upstream.
233
+ - **CLI f-string** used a PEP 701 nested-same-quote form that fails on the
234
+ minimum supported Python (3.11).
235
+ - **`config.example.toml`** is now shipped inside the package, so `cg init`
236
+ works from a `pip`/`pipx` install.
237
+
238
+ ### Evidence
239
+ - Matrix **19/19** (18 realtime checks + protocol fuzz) on Raspberry Pi 5
240
+ (aarch64) and x86-64 Linux, Python 3.12/3.13.
241
+ - Adversarial corpus (default profile, 3600 healthy + 4000 faulty):
242
+ **hard-level false positives 0.0%**, recall 100% on all ten fault classes.
243
+ - Phase-1 shadow-mode soak across two hosts, ~13 models and 5 integration
244
+ paths: **zero genuine healthy sessions would have been terminated**
245
+ (one harness artifact, annotated, ledger kept append-only).
246
+
247
+ ## [0.4.1] and earlier
248
+
249
+ Pre-PyPI reference builds, distributed as tarballs. 18-check matrix, adaptive
250
+ `decay` without `hard_floor`, Unix-socket transport only. See `git log` for the
251
+ full history.
252
+
253
+ [0.4.2]: https://github.com/ak2tx/continuity-guard/releases/tag/v0.4.2
@@ -0,0 +1,58 @@
1
+ # Contributing
2
+
3
+ Bug reports and small fixes are welcome. Before a larger change, open an issue
4
+ — the answer may be that it was tried and measured and did not work, and
5
+ `docs/` will say which measurement.
6
+
7
+ ## The one rule
8
+
9
+ **A change to detector or protocol semantics is not done until it is
10
+ measured.** This project is a watchdog: it can end someone's work. An
11
+ improvement that is only argued for is a regression waiting to be found by a
12
+ user at three in the morning.
13
+
14
+ Definition of done:
15
+
16
+ ```bash
17
+ ./tests/run_matrix.sh # 22 checks against real processes, ~14 min
18
+ python3 tests/bench_accuracy.py # hard-level FP 0.0%, recall 100%
19
+ ```
20
+
21
+ plus a short note in the repo saying what changed and **which measurement
22
+ justified it**. If a measurement contradicts a design intuition, trust the
23
+ measurement and write down which intuition was wrong. `docs/` is full of
24
+ those; they are the most useful pages in it.
25
+
26
+ ## Two instruments, and which is authoritative for what
27
+
28
+ | | use it for | it cannot see |
29
+ |---|---|---|
30
+ | `tests/bench_accuracy.py` | tuning. Thousands of sessions in seconds against a simulated clock, on nine deliberately adversarial *healthy* workloads | a real process, a real `SIGSTOP`, a real kill |
31
+ | `tests/run_matrix.sh` | enforcement. 22 checks against real processes | high-N false-positive rates |
32
+
33
+ Run **both** after any detector change. Shipping something validated only by
34
+ the benchmark once let a stopped process go undetected; real hardware caught
35
+ it, not the model.
36
+
37
+ Row #22 installs the built wheel into a clean venv and runs the commands the
38
+ README names, from outside the checkout. It exists because for the whole life
39
+ of 0.4.2 the two documented integration paths were missing from the wheel and
40
+ every test passed anyway — they all ran from a checkout where `tools/` was
41
+ simply on the path.
42
+
43
+ ## Invariants
44
+
45
+ `HANDOFF.md` lists seven. Each was learned by breaking it. Do not regress
46
+ them, and if you think one is wrong, bring the measurement.
47
+
48
+ The first is the one that matters most: **hard-level false positives must be
49
+ zero.** Warn-level noise is survivable; spurious termination is not.
50
+
51
+ ## Style
52
+
53
+ Match the surrounding code. Comments explain *why*, especially why something
54
+ is not the obvious approach — a comment that says what the line does is noise,
55
+ one that records the failure that shaped it is the reason the file is
56
+ maintainable.
57
+
58
+ Zero runtime dependencies. Python 3.11+. Keep it that way.
@@ -0,0 +1,47 @@
1
+ # Continuity Guard -- the daemon (and, with a command override, the proxy).
2
+ # Zero Python dependencies, so this stays small.
3
+ #
4
+ # docker build -t continuity-guard .
5
+ #
6
+ # # daemon: mount a config and a dir for the socket + state
7
+ # docker run -d --name cg \
8
+ # -v "$PWD/config.toml:/etc/continuity-guard/config.toml:ro" \
9
+ # -v cg-state:/var/lib/continuity-guard \
10
+ # continuity-guard
11
+ #
12
+ # # proxy in the same network namespace as the daemon container
13
+ # docker run -d --name cg-proxy --network container:cg \
14
+ # -v cg-state:/var/lib/continuity-guard \
15
+ # continuity-guard \
16
+ # python -m tools.cg_proxy --upstream http://host.docker.internal:11434 --port 8111
17
+ #
18
+ # The daemon writes its socket where the config's `socket =` points; keep that
19
+ # on the shared volume (e.g. /var/lib/continuity-guard/cg.sock) so the proxy,
20
+ # supervisor, or library in another container can reach it.
21
+
22
+ FROM python:3.12-slim AS base
23
+
24
+ RUN useradd --system --create-home --home-dir /home/cg cg
25
+ WORKDIR /app
26
+
27
+ COPY pyproject.toml README.md ./
28
+ COPY continuity_guard ./continuity_guard
29
+ COPY tools ./tools
30
+
31
+ RUN pip install --no-cache-dir . \
32
+ && mkdir -p /etc/continuity-guard /var/lib/continuity-guard \
33
+ && chown -R cg /var/lib/continuity-guard
34
+
35
+ # default config location; override by mounting your own or setting CG_CONFIG
36
+ ENV CG_CONFIG=/etc/continuity-guard/config.toml \
37
+ PYTHONUNBUFFERED=1 \
38
+ PYTHONPATH=/app
39
+ USER cg
40
+
41
+ # a sane default config baked in (shadow mode, socket on the shared volume)
42
+ RUN mkdir -p /home/cg/.config && \
43
+ printf 'socket = "/var/lib/continuity-guard/cg.sock"\ndb = "/var/lib/continuity-guard/state.db"\n[defaults]\nshadow = true\n' \
44
+ > /home/cg/default-config.toml
45
+
46
+ # if no config is mounted, fall back to the baked default
47
+ ENTRYPOINT ["sh", "-c", "exec cg-daemon -c \"${CG_CONFIG}\" 2>/dev/null || exec cg-daemon -c /home/cg/default-config.toml"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ak2tx LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ include LICENSE
2
+ include CHANGELOG.md
3
+ include README.md
4
+ include TESTING.md
5
+ # A security policy must travel with the source it describes -- someone
6
+ auditing an sdist offline should not have to find the repo to learn how to
7
+ report a finding or what the threat model is.
8
+ include SECURITY.md
9
+ include CONTRIBUTING.md
10
+ include Dockerfile
11
+ include config.example.toml
12
+ recursive-include continuity_guard *.toml
13
+ recursive-include docs *.md
14
+ recursive-include tests *.py *.sh *.toml
15
+ recursive-include tools *.py *.sh
16
+ recursive-include systemd *
17
+ prune **/__pycache__