kijito-tools 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +220 -0
- package/bin/cli.js +34 -0
- package/install.sh +161 -0
- package/package.json +50 -0
- package/providers/_shared/wake-core.mjs +358 -0
- package/providers/claude/CLAUDE.md.snippet +31 -0
- package/providers/claude/install.sh +186 -0
- package/providers/claude/scripts/arm-session.sh +66 -0
- package/providers/claude/scripts/claude-armed.sh +101 -0
- package/providers/claude/scripts/heartbeat-watchdog.sh +209 -0
- package/providers/claude/scripts/inbox-selftest.sh +277 -0
- package/providers/claude/scripts/kijito-persona-lib.sh +94 -0
- package/providers/claude/scripts/kijito-qa-pass.sh +12 -0
- package/providers/claude/scripts/lifecycle-lib.sh +167 -0
- package/providers/claude/scripts/myctx.sh +46 -0
- package/providers/claude/scripts/self-clear.sh +134 -0
- package/providers/claude/scripts/session-autosend.sh +85 -0
- package/providers/claude/scripts/session-catchup-hint.sh +330 -0
- package/providers/claude/scripts/statusline-context.sh +67 -0
- package/providers/claude/skills/kijito-qa-memory/SKILL.md +110 -0
- package/providers/claude/skills/kijito-recall/SKILL.md +53 -0
- package/providers/claude/skills/kijito-start/SKILL.md +122 -0
- package/providers/claude/wiring/README.md +93 -0
- package/providers/claude/wiring/kijito-heartbeat@.service +21 -0
- package/providers/codex/README.md +75 -0
- package/providers/codex/codex-kijito-parity-plan.md +265 -0
- package/providers/codex/docs-codex-setup.md +64 -0
- package/providers/codex/install.mjs +184 -0
- package/providers/codex/n0-capability-probe-protocol.md +472 -0
- package/providers/codex/n0-harness/README.md +57 -0
- package/providers/codex/n0-harness/cli.mjs +58 -0
- package/providers/codex/n0-harness/evidence-manifest.mjs +77 -0
- package/providers/codex/n0-harness/fixture.mjs +279 -0
- package/providers/codex/n0-harness/lib.mjs +141 -0
- package/providers/codex/n0-harness/manifest.mjs +36 -0
- package/providers/codex/n0-harness/oracle.mjs +464 -0
- package/providers/codex/n0-harness/parser.mjs +200 -0
- package/providers/codex/n0-harness/prompt.mjs +43 -0
- package/providers/codex/n0-harness/snapshot.mjs +120 -0
- package/providers/codex/n0-harness/specimen.mjs +37 -0
- package/providers/codex/notify/kijito-notify-count.mjs +117 -0
- package/providers/codex/notify/kijito-notify-count.test.mjs +76 -0
- package/providers/codex/plans/gate4-battery-log.md +184 -0
- package/providers/codex/plans/gate4-battery-protocol.md +43 -0
- package/providers/codex/plans/hive-user-first-plan-QA.md +34 -0
- package/providers/codex/plans/hive-user-first-plan.md +247 -0
- package/providers/codex/release-manifest.json +53 -0
- package/providers/codex/same-chat-continuation-plan-gate.md +235 -0
- package/providers/codex/same-chat-continuation-plan.md +458 -0
- package/providers/codex/skills/kijito-qa-memory/SKILL.md +193 -0
- package/providers/codex/skills/kijito-qa-memory/agents/openai.yaml +4 -0
- package/providers/codex/skills/kijito-recall/SKILL.md +53 -0
- package/providers/codex/skills/kijito-start/SKILL.md +173 -0
- package/providers/codex/skills/kijito-start/agents/openai.yaml +4 -0
- package/providers/codex/test/n0-cli-refusal.test.mjs +31 -0
- package/providers/codex/test/n0-counterexample-matrix.test.mjs +118 -0
- package/providers/codex/test/n0-guard-census-core.mjs +1372 -0
- package/providers/codex/test/n0-guard-census.json +35479 -0
- package/providers/codex/test/n0-guard-census.mjs +31 -0
- package/providers/codex/test/n0-guard-census.test.mjs +374 -0
- package/providers/codex/test/n0-guard-counterexamples.mjs +1068 -0
- package/providers/codex/test/n0-guard-independent-projection.mjs +70 -0
- package/providers/codex/test/n0-guard-independent-projection.test.mjs +53 -0
- package/providers/codex/test/n0-guard-manifest-author.mjs +424 -0
- package/providers/codex/test/n0-guard-mutation-operators.test.mjs +244 -0
- package/providers/codex/test/n0-guard-mutation-runner.mjs +599 -0
- package/providers/codex/test/n0-guard-remainder-classes.test.mjs +314 -0
- package/providers/codex/test/n0-guard-sanitizer-pairs.test.mjs +68 -0
- package/providers/codex/test/n0-guard-shared-context-predicates.test.mjs +84 -0
- package/providers/codex/test/n0-harness.test.mjs +553 -0
- package/providers/codex/test/n0-notable-regressions.test.mjs +70 -0
- package/providers/codex/test/n0-subject-boundary.mjs +97 -0
- package/providers/codex/test/n0-subject-boundary.test.mjs +72 -0
- package/providers/codex/test/same-chat-plan-preflight.sh +167 -0
- package/providers/codex/tools/refresh-manifest.mjs +81 -0
- package/providers/codex/wake-helper/TRANSPORT-NOTES.md +25 -0
- package/providers/codex/wake-helper/integration.test.mjs +285 -0
- package/providers/codex/wake-helper/kijito-wake-helper.mjs +488 -0
- package/providers/codex/wake-helper/kijito-wake-helper.test.mjs +128 -0
- package/providers/codex/wake-helper/mock-daemon.mjs +64 -0
- package/providers/codex/wake-helper/status-probe.mjs +45 -0
- package/providers/codex/wake-helper/ws-uds.mjs +153 -0
- package/providers/monitor/.github/workflows/publish-npm.yml +38 -0
- package/providers/monitor/.github/workflows/publish-pypi.yml +30 -0
- package/providers/monitor/CHANGELOG.md +465 -0
- package/providers/monitor/IMPORT-PROVENANCE.md +68 -0
- package/providers/monitor/LICENSE +202 -0
- package/providers/monitor/NOTICE +7 -0
- package/providers/monitor/OPAQUE-OUTPUT-ENFORCEMENT.md +45 -0
- package/providers/monitor/README.md +571 -0
- package/providers/monitor/RELEASING.md +125 -0
- package/providers/monitor/arm-hive-monitor.sh +13 -0
- package/providers/monitor/bin/cli.js +43 -0
- package/providers/monitor/com.kijito.inbox-monitor.plist.template +59 -0
- package/providers/monitor/docs/DESIGN.md +984 -0
- package/providers/monitor/kijito-inbox-monitor@.service.template +95 -0
- package/providers/monitor/kijito_inbox_monitor.py +3666 -0
- package/providers/monitor/package.json +35 -0
- package/providers/monitor/pyproject.toml +42 -0
- package/providers/monitor/scripts/mutation-check.py +399 -0
- package/providers/monitor/scripts/prepublish-gate.sh +284 -0
- package/providers/monitor/test_kijito_monitor.py +5609 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kijito-inbox-monitor",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Pointer to the Python package kijito-inbox-monitor. A zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives. Install the real tool with pipx/uv/pip.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kijito-inbox-monitor": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"NOTICE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/KijitoAI/kijito-inbox-monitor.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/KijitoAI/kijito-inbox-monitor/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/KijitoAI/kijito-inbox-monitor#readme",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"kijito",
|
|
27
|
+
"inbox",
|
|
28
|
+
"monitor",
|
|
29
|
+
"watcher",
|
|
30
|
+
"agent",
|
|
31
|
+
"cli",
|
|
32
|
+
"python",
|
|
33
|
+
"pipx"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# setuptools (not uv_build): uv_build cannot build a single bare top-level .py
|
|
3
|
+
# module yet (astral-sh/uv#16766). setuptools auto-handles a lone module via
|
|
4
|
+
# py-modules, and >=77 gives PEP 639 SPDX license support.
|
|
5
|
+
requires = ["setuptools>=77"]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "kijito-inbox-monitor"
|
|
10
|
+
version = "0.5.0"
|
|
11
|
+
description = "Zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = "Apache-2.0"
|
|
15
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
16
|
+
authors = [{ name = "Arcada Labs", email = "jason@arcadalabs.com" }]
|
|
17
|
+
keywords = ["kijito", "inbox", "monitor", "watcher", "agent", "liveness", "cli", "notifications"]
|
|
18
|
+
dependencies = []
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Operating System :: POSIX",
|
|
24
|
+
"Operating System :: MacOS",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Topic :: Communications",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/KijitoAI/kijito-inbox-monitor"
|
|
33
|
+
Repository = "https://github.com/KijitoAI/kijito-inbox-monitor"
|
|
34
|
+
"Bug Tracker" = "https://github.com/KijitoAI/kijito-inbox-monitor/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
kijito-inbox-monitor = "kijito_inbox_monitor:main"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
# Single flat module at the repo root. Explicit py-modules overrides flat-layout
|
|
41
|
+
# auto-discovery so the test module is not swept into the wheel.
|
|
42
|
+
py-modules = ["kijito_inbox_monitor"]
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Behavioural mutation check: break each guarantee on purpose, confirm the suite NOTICES.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS. A green suite is not evidence. It says the tests pass, not that they can FAIL when the
|
|
5
|
+
behaviour they name is removed. Every fix in this repo is verified by deliberately reintroducing the bug
|
|
6
|
+
and requiring the suite to go red; anything that survives marks a test that is decorative.
|
|
7
|
+
|
|
8
|
+
WHAT IT REFUSES TO COUNT AS A CATCH, each learned from a mutant that lied here:
|
|
9
|
+
· a mutant whose pattern is no longer in the source -> VACUOUS (the code moved; the test proved nothing)
|
|
10
|
+
· a mutant that does not COMPILE -> proves the suite noticed a SyntaxError, not a defect
|
|
11
|
+
· a mutant where nearly every test ERRORS -> the program crashed rather than misbehaved
|
|
12
|
+
Only a compiling mutant that produces FAILURES counts.
|
|
13
|
+
|
|
14
|
+
USAGE
|
|
15
|
+
python3 scripts/mutation-check.py # exits non-zero if any mutation survives
|
|
16
|
+
|
|
17
|
+
EXTENDING IT. MUTATIONS is a list of (label, pattern, replacement) or (label, [(pat, rep), ...]) for
|
|
18
|
+
multi-part edits. Each entry must remove exactly ONE guarantee. When a mutation survives, ask three
|
|
19
|
+
questions before adding a test: is the property untested, is the code under test the code that actually
|
|
20
|
+
RUNS (a duplicated implementation will hide this), and could an OLDER guard be catching the scenario first?
|
|
21
|
+
All three have happened in this repo.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import re, shutil, subprocess, sys, tempfile, os
|
|
25
|
+
MUTANT_TIMEOUT = 90 # a mutant that hangs must not stall the gate; it is reported, never counted
|
|
26
|
+
SRC = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "kijito_inbox_monitor.py")
|
|
27
|
+
TESTS = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "test_kijito_monitor.py")
|
|
28
|
+
M=[
|
|
29
|
+
("HIGH1a: silence read as end-of-chain again",
|
|
30
|
+
" if not poll.continuation_ok:\n # Absent or malformed continuation: the server did not answer. NOT exhaustion.\n return (rows, False)",
|
|
31
|
+
" pass"),
|
|
32
|
+
("HIGH1a: malformed continuation coerced to a valid None",
|
|
33
|
+
" nb, nb_ok = None, False",
|
|
34
|
+
" nb, nb_ok = None, True"),
|
|
35
|
+
("HIGH1a: absent field treated as an affirmed null",
|
|
36
|
+
" nb_raw = data.get(\"next_before_id\", _MISSING)",
|
|
37
|
+
" nb_raw = data.get(\"next_before_id\")"),
|
|
38
|
+
("HIGH1b: continuation no longer checked against oldest row",
|
|
39
|
+
" if poll.next_before_id is not None and poll.next_before_id != oldest:",
|
|
40
|
+
" if False:"),
|
|
41
|
+
("HIGH1b: check weakened to allow a lower continuation",
|
|
42
|
+
" if poll.next_before_id is not None and poll.next_before_id != oldest:\n # The chain skips",
|
|
43
|
+
" if poll.next_before_id is not None and poll.next_before_id > oldest:\n # The chain skips"),
|
|
44
|
+
("HIGH2: corrupt state collapses back into absent",
|
|
45
|
+
" return CORRUPT_STATE\n if not ((cursor is None or _is_int(cursor))",
|
|
46
|
+
" return None\n if not ((cursor is None or _is_int(cursor))"),
|
|
47
|
+
("HIGH2: corrupt state baselines to the newest id",
|
|
48
|
+
" self.cursor = min((m[\"id\"] for m in items), default=0) - 1\n new_items = sorted(items, key=lambda m: m[\"id\"])",
|
|
49
|
+
" self.cursor = max((m[\"id\"] for m in items), default=0)"),
|
|
50
|
+
("MEDIUM: completed walk no longer releases the forced pin",
|
|
51
|
+
" release_earned = closed",
|
|
52
|
+
" release_earned = False"),
|
|
53
|
+
("MEDIUM: forced pin released with NO authoritative evidence",
|
|
54
|
+
" if self.pin_forced and complete and blocked_at is None:",
|
|
55
|
+
" if self.pin_forced and blocked_at is None:"),
|
|
56
|
+
("MEDIUM: forced pin ignored entirely when advancing",
|
|
57
|
+
" if self.pin_forced:\n high = None # still forced: the watermark holds",
|
|
58
|
+
" if False:\n high = None"),
|
|
59
|
+
("L6-HIGH1: pin_forced not persisted",
|
|
60
|
+
" if pin_forced:\n d[\"pin_forced\"] = True",
|
|
61
|
+
" pass"),
|
|
62
|
+
("L6-HIGH1: persisted pin ignored on load",
|
|
63
|
+
" self.pin_forced = loaded[\"pin_forced\"] or not loaded[\"pin_evidence_intact\"]",
|
|
64
|
+
" self.pin_forced = not loaded[\"pin_evidence_intact\"]"),
|
|
65
|
+
("L6-HIGH2: empty page claiming more is trusted",
|
|
66
|
+
" if poll.next_before_id is not None:\n # EMPTY PAGE CLAIMING THERE IS MORE",
|
|
67
|
+
" if False:\n # EMPTY PAGE CLAIMING THERE IS MORE"),
|
|
68
|
+
# L6-HIGH3 moved into the shared consistency rule in round 7; mutate it THERE or the entry goes
|
|
69
|
+
# vacuous and silently stops defending anything.
|
|
70
|
+
("L6-HIGH3: contradictory withholding+terminal trusted",
|
|
71
|
+
" if n and nb is None:\n consistent = False",
|
|
72
|
+
" if False:\n consistent = False"),
|
|
73
|
+
("L7-HIGH1: exec exit status discarded again",
|
|
74
|
+
" if r.returncode != 0:",
|
|
75
|
+
" if False:"),
|
|
76
|
+
("L7-HIGH1: cursor advances over what was not delivered",
|
|
77
|
+
" high = max(sorted(delivered)",
|
|
78
|
+
" high = max([m[\"id\"] for m in new_items]"),
|
|
79
|
+
("L7-HIGH1: the delivery gate is removed from the advance",
|
|
80
|
+
" if blocked_at is not None and high is not None:",
|
|
81
|
+
" if False:"),
|
|
82
|
+
("L7-HIGH1: delivery no longer stops at the first failure (order lost)",
|
|
83
|
+
" if blocked_at is not None:\n break",
|
|
84
|
+
" if False:\n break"),
|
|
85
|
+
("L7-HIGH1: a suppressed author read as a FAILED delivery (would pin forever)",
|
|
86
|
+
" return True\n ev = {\"event\": \"new\"",
|
|
87
|
+
" return False\n ev = {\"event\": \"new\""),
|
|
88
|
+
("L7-HIGH1: a failed sink write reported as a delivery",
|
|
89
|
+
" sys.stderr.write(\"kijito-inbox-monitor: WARNING event write FAILED, holding the cursor: %s\\n\" % e)\n return False",
|
|
90
|
+
" return True"),
|
|
91
|
+
("L7-MEDIUM: the durability barrier is skipped",
|
|
92
|
+
" if delivered and not self.emitter.sync(self.persona):",
|
|
93
|
+
" if False:"),
|
|
94
|
+
("L7-MEDIUM: the barrier syncs every persona's sink, not this one's",
|
|
95
|
+
" s = self._sinks_by_persona.get(persona or \"_all\")\n if s is not None:\n ok = s.sync() and ok",
|
|
96
|
+
" for s in self._sinks_by_persona.values():\n ok = s.sync() and ok"),
|
|
97
|
+
# ★ loom re-audit 8: deleting a CALL proves the call is present; it does NOT prove the ANSWER is
|
|
98
|
+
# read. Both forms are kept deliberately - the weak one catches removal, the strong one catches
|
|
99
|
+
# "called it and ignored what it said", which is the defect that actually shipped.
|
|
100
|
+
("L8-HIGH3: the state directory is not fsynced at all (call removed)",
|
|
101
|
+
" if not _fsync_dir(dirn):",
|
|
102
|
+
" if False and not _fsync_dir(dirn):"),
|
|
103
|
+
("L8-HIGH3: ★ the directory fsync is CALLED and its failure IGNORED (result mutation)",
|
|
104
|
+
" if not _fsync_dir(dirn):\n sys.stderr.write(\"kijito-inbox-monitor: WARNING state-file directory fsync FAILED",
|
|
105
|
+
" if _fsync_dir(dirn) is None:\n sys.stderr.write(\"kijito-inbox-monitor: WARNING state-file directory fsync FAILED"),
|
|
106
|
+
("L8-HIGH3: save reports success regardless of durability",
|
|
107
|
+
" return False\n return True\n except OSError as e:",
|
|
108
|
+
" return True\n return True\n except OSError as e:"),
|
|
109
|
+
("L8-HIGH1: the event stream is opened with the umask again",
|
|
110
|
+
" self._fh = _open_private(self.path, \"a\", encoding=\"utf-8\")",
|
|
111
|
+
" self._fh = open(self.path, \"a\", encoding=\"utf-8\")"),
|
|
112
|
+
("L8-HIGH1: an already-leaked file is left world-readable",
|
|
113
|
+
" if cur != PRIVATE_FILE_MODE:",
|
|
114
|
+
" if False:"),
|
|
115
|
+
("L8-HIGH1: the lock sidecar goes back to the umask",
|
|
116
|
+
" self._lockf = _open_private(self.path + \".lock\", \"a+\")",
|
|
117
|
+
" self._lockf = open(self.path + \".lock\", \"a+\")"),
|
|
118
|
+
("L8-HIGH2: a newly created event file does not sync its directory entry",
|
|
119
|
+
" if not existed:",
|
|
120
|
+
" if False:"),
|
|
121
|
+
("L8-HIGH2: a rotation does not sync the rewritten directory entries",
|
|
122
|
+
" self._dir_pending = True\n self._reopen_or_break()",
|
|
123
|
+
" self._reopen_or_break()"),
|
|
124
|
+
("L8-HIGH2: sync() ignores the pending directory entry",
|
|
125
|
+
" if self._dir_pending:",
|
|
126
|
+
" if False:"),
|
|
127
|
+
("L7-MEDIUM: the sink is never fsynced",
|
|
128
|
+
" os.fsync(self._fh.fileno())\n self._pending = False",
|
|
129
|
+
" self._pending = False"),
|
|
130
|
+
("L7-HIGH2: pin_forced read leniently again (a JSON 1 unpins)",
|
|
131
|
+
" pin_forced = _flag(\"pin_forced\")",
|
|
132
|
+
" pin_forced = d.get(\"pin_forced\") is True"),
|
|
133
|
+
("L7-HIGH2: a malformed pin field no longer fails closed",
|
|
134
|
+
" if not strict_ok:",
|
|
135
|
+
" if False:"),
|
|
136
|
+
("L7-HIGH2: gap_alerted accepts a bool again",
|
|
137
|
+
" elif _is_int(alerted):",
|
|
138
|
+
" elif isinstance(alerted, int):"),
|
|
139
|
+
("L7-HIGH3: a case-only identity is treated as a different source again",
|
|
140
|
+
" if identity_migratable(ident, self.identity):",
|
|
141
|
+
" if False:"),
|
|
142
|
+
("L7-HIGH3: identity migration widened to ignore host/path too",
|
|
143
|
+
" if stored[:4] != current[:4]:\n return False",
|
|
144
|
+
" if False:\n return False"),
|
|
145
|
+
("L7-HIGH4: the inverse contradiction is trusted again",
|
|
146
|
+
" elif not n and nb is not None:\n consistent = False",
|
|
147
|
+
" elif False:\n consistent = False"),
|
|
148
|
+
("L7-HIGH4b: the gap check ignores an UNANSWERED continuation",
|
|
149
|
+
" if not poll.continuation_ok:\n # SILENCE IS NOT AN ANSWER HERE EITHER.",
|
|
150
|
+
" if False:\n # SILENCE IS NOT AN ANSWER HERE EITHER."),
|
|
151
|
+
("L7-HIGH4b: an unanswered window can still RELEASE a pin",
|
|
152
|
+
" complete = (poll.omitted == 0 and poll.consistent and poll.continuation_ok",
|
|
153
|
+
" complete = (poll.omitted == 0 and poll.consistent and True"),
|
|
154
|
+
("L7-HIGH4: the gap check ignores consistency again",
|
|
155
|
+
" if not poll.consistent:\n # A SELF-CONTRADICTORY WINDOW",
|
|
156
|
+
" if False:\n # A SELF-CONTRADICTORY WINDOW"),
|
|
157
|
+
("L7-HIGH4: a contradictory window can release a pin again",
|
|
158
|
+
" complete = (poll.omitted == 0 and poll.consistent",
|
|
159
|
+
" complete = (poll.omitted == 0 and True"),
|
|
160
|
+
("L7-HIGH5: the corruption pin loses its release floor",
|
|
161
|
+
" if self.pin_release_at is not None:\n floor = max(floor, self.pin_release_at)",
|
|
162
|
+
" pass"),
|
|
163
|
+
("L7-HIGH5: the release floor is never recorded",
|
|
164
|
+
" if self.pin_forced and self.state_corrupt and self.pin_release_at is None and items:",
|
|
165
|
+
" if False:"),
|
|
166
|
+
("L7-HIGH5: the release floor is not persisted (dies on restart)",
|
|
167
|
+
" if pin_release_at is not None:\n d[\"pin_release_at\"] = pin_release_at",
|
|
168
|
+
" pass"),
|
|
169
|
+
("L7-HIGH5: delivered ids are tracked ONLY while pinned again",
|
|
170
|
+
" uncovered = {i for i in delivered if i > (self.cursor or 0)}",
|
|
171
|
+
" uncovered = {i for i in delivered if i > (self.cursor or 0)} if pinned else set()"),
|
|
172
|
+
# Found by adversarially re-reading round 7 before submitting it: a pin discharged on a poll that
|
|
173
|
+
# could not deliver throws away the release floor, and the restart cannot rebuild it.
|
|
174
|
+
("L7-HIGH5: a pin is discharged by a poll that FAILED to deliver (walk proof)",
|
|
175
|
+
" if release_earned and blocked_at is None:",
|
|
176
|
+
" if release_earned:"),
|
|
177
|
+
("L7-HIGH5: a pin is discharged by a poll that FAILED to deliver (complete-window proof)",
|
|
178
|
+
" if self.pin_forced and complete and blocked_at is None:",
|
|
179
|
+
" if self.pin_forced and complete:"),
|
|
180
|
+
# Found by running the repro LIVE and reading what was persisted - no fixture asserted it.
|
|
181
|
+
("L7-HIGH5: a released pin leaves its state behind",
|
|
182
|
+
" self.pin_forced = False\n self.pin_release_at = None\n self.state_corrupt = False",
|
|
183
|
+
" self.pin_forced = False"),
|
|
184
|
+
("L7-poison: content bytes can wedge the watermark again (sanitiser removed)",
|
|
185
|
+
" try:\n s.encode(\"utf-8\")\n except UnicodeEncodeError:\n s = s.encode(\"utf-8\", \"replace\").decode(\"utf-8\")\n return s.replace(\"\\x00\", \"\") if \"\\x00\" in s else s",
|
|
186
|
+
" return s"),
|
|
187
|
+
("L7-poison: the sink crashes instead of reporting a failed delivery",
|
|
188
|
+
" except (OSError, UnicodeError, ValueError) as e:",
|
|
189
|
+
" except OSError as e:"),
|
|
190
|
+
("L9-H1: O_NOFOLLOW dropped (symlink followed again)",
|
|
191
|
+
" | getattr(os, \"O_NOFOLLOW\", 0) | getattr(os, \"O_NONBLOCK\", 0))",
|
|
192
|
+
" | getattr(os, \"O_NONBLOCK\", 0))"),
|
|
193
|
+
("L9-H1: O_NONBLOCK dropped (a FIFO would hang the watcher)",
|
|
194
|
+
" | getattr(os, \"O_NOFOLLOW\", 0) | getattr(os, \"O_NONBLOCK\", 0))",
|
|
195
|
+
" | getattr(os, \"O_NOFOLLOW\", 0))"),
|
|
196
|
+
("L9-H1: the re-read after chmod is dropped (repair assumed, not verified)",
|
|
197
|
+
" again = os.fstat(fd).st_mode & 0o777",
|
|
198
|
+
" again = PRIVATE_FILE_MODE"),
|
|
199
|
+
("L9-H1: the owner check is dropped",
|
|
200
|
+
" if st.st_uid != os.geteuid():",
|
|
201
|
+
" if False:"),
|
|
202
|
+
("L9-H1: the regular-file check is dropped",
|
|
203
|
+
" if not stat.S_ISREG(st.st_mode):",
|
|
204
|
+
" if False:"),
|
|
205
|
+
("L9-H1: ★ an untightenable file is WARNED ABOUT and written anyway (the round-8 behaviour)",
|
|
206
|
+
" if again != PRIVATE_FILE_MODE:\n raise InsecureFile",
|
|
207
|
+
" if False:\n raise InsecureFile"),
|
|
208
|
+
("L9-H2: pre-existing rotated archives are not repaired",
|
|
209
|
+
" for archive in self._archive_paths():\n _repair_mode(archive)",
|
|
210
|
+
" for archive in []:\n _repair_mode(archive)"),
|
|
211
|
+
# L10-M4: the CALL survives, only its RANGE narrows back to current retention. Deleting a call proves
|
|
212
|
+
# the call is present; it does not prove the range it walks is the right one.
|
|
213
|
+
("L10-M4: archive repair is bounded by CURRENT retention again (a shrunk `keep` strands the rest)",
|
|
214
|
+
" return sorted(os.path.join(d, n) for n in names\n"
|
|
215
|
+
" if n.startswith(base + \".\") and n[len(base) + 1:].isdigit())",
|
|
216
|
+
" return sorted(os.path.join(d, \"%s.%d\" % (base, i)) for i in range(1, self.keep + 2)\n"
|
|
217
|
+
" if \"%s.%d\" % (base, i) in names)"),
|
|
218
|
+
# L10-H1: two mutations on ONE fix. The first deletes the CALL, the second keeps the call and discards
|
|
219
|
+
# only its ANSWER - which is the exact defect loom found, and the one a call-deletion cannot detect.
|
|
220
|
+
("L10-H1: the state file itself is not repaired (call deleted)",
|
|
221
|
+
" if not _repair_mode(self.path): # the state file itself",
|
|
222
|
+
" if False: # the state file itself"),
|
|
223
|
+
("L10-H1: ★ _repair_mode's verdict is DISCARDED again at the call site (result-mutation)",
|
|
224
|
+
" if not _repair_mode(self.path): # the state file itself",
|
|
225
|
+
" _repair_mode(self.path)\n if False: # the state file itself"),
|
|
226
|
+
("L10-H1: the state-file READ path follows symlinks again (O_NOFOLLOW dropped)",
|
|
227
|
+
" fd = os.open(self.path,\n"
|
|
228
|
+
" os.O_RDONLY | getattr(os, \"O_NOFOLLOW\", 0) | getattr(os, \"O_NONBLOCK\", 0))",
|
|
229
|
+
" fd = os.open(self.path, os.O_RDONLY | getattr(os, \"O_NONBLOCK\", 0))"),
|
|
230
|
+
("L10-H1: an unsafe state file is trusted instead of failing closed",
|
|
231
|
+
" if self.unsafe:",
|
|
232
|
+
" if False:"),
|
|
233
|
+
# L10-H2: the release condition. A permanent fail-closed is the same bug as a fail-open, facing the
|
|
234
|
+
# other way - so the mutation restores the PERMANENT refusal and the suite must notice the recovery
|
|
235
|
+
# never happens.
|
|
236
|
+
("L10-H2: ★ a refused persona sink is cached PERMANENTLY again (no release condition)",
|
|
237
|
+
" retry_at = self._broken_sinks.get(key)\n if retry_at is not None and _monotonic() < retry_at:",
|
|
238
|
+
" retry_at = self._broken_sinks.get(key)\n if retry_at is not None:"),
|
|
239
|
+
("L10-H2: a recovered sink never clears its warning suppression",
|
|
240
|
+
" _clear_persona_warning(key)",
|
|
241
|
+
" pass"),
|
|
242
|
+
# L10-M3: the self-test. Call-mutation AND result-mutation, because the defect was purely in the result.
|
|
243
|
+
("L10-M3: ★ self_test discards the emitter's answer again and reports emit=OK (result-mutation)",
|
|
244
|
+
" emit_ok = bool(self.emitter.new({\"id\": 0, \"from\": \"self-test\", \"content\": \"synthetic emit OK\",\n"
|
|
245
|
+
" \"created\": _now_iso(), \"_persona\": self.persona}))",
|
|
246
|
+
" emit_ok = True\n"
|
|
247
|
+
" self.emitter.new({\"id\": 0, \"from\": \"self-test\", \"content\": \"synthetic emit OK\",\n"
|
|
248
|
+
" \"created\": _now_iso(), \"_persona\": self.persona})"),
|
|
249
|
+
("L9-M3: makedirs goes back to leaf-only",
|
|
250
|
+
" for d in reversed(missing):\n try:\n os.mkdir(d, PRIVATE_DIR_MODE)",
|
|
251
|
+
" for d in reversed(missing[:1]):\n try:\n os.mkdir(d, PRIVATE_DIR_MODE)"),
|
|
252
|
+
("L9-M4: ★ save()'s durability answer is discarded again (result ignored at the CALL SITE)",
|
|
253
|
+
" if durable is False:\n self._state_not_durable()",
|
|
254
|
+
" if False:\n self._state_not_durable()"),
|
|
255
|
+
("L9-M5: a broken sink falls through to stdout instead of failing delivery",
|
|
256
|
+
" if sink is _BROKEN_SINK:\n return False",
|
|
257
|
+
" if sink is _BROKEN_SINK:\n return True"),
|
|
258
|
+
("L9-M5: a failed reopen escapes the poll loop again",
|
|
259
|
+
" if self._broken is not None or self._fh is None:\n self._reopen_or_break()",
|
|
260
|
+
" if False:\n self._reopen_or_break()"),
|
|
261
|
+
("L7-item7: the lock fd is never released",
|
|
262
|
+
" try:\n self._lockf.close()\n finally:\n self._lockf = None",
|
|
263
|
+
" pass"),
|
|
264
|
+
("L6-HIGH4: zero-byte state reads as absent again",
|
|
265
|
+
" return CORRUPT_STATE\n try:\n d = json.loads(raw)",
|
|
266
|
+
" return None\n try:\n d = json.loads(raw)"),
|
|
267
|
+
("L6-MEDIUM: bool accepted as an int again",
|
|
268
|
+
" return isinstance(v, int) and not isinstance(v, bool)",
|
|
269
|
+
" return isinstance(v, int)"),
|
|
270
|
+
("L6-MEDIUM: duplicate ids allowed",
|
|
271
|
+
" if m[\"id\"] in seen_ids:",
|
|
272
|
+
" if False:"),
|
|
273
|
+
("L6-MEDIUM: uninterpretable truncation reads as no-omission",
|
|
274
|
+
" elif trunc is not _MISSING and trunc is not False:",
|
|
275
|
+
" elif False:"),
|
|
276
|
+
# ---- re-audit 11 (river): acknowledge-before-deliver in the ALARM path -------------------------
|
|
277
|
+
# ★ A1 is the one that mattered: this exact mutation SURVIVED all 242 tests before this round,
|
|
278
|
+
# because the dead-man's switch - the event README sells as the headline feature - had no test.
|
|
279
|
+
("L11-A1: ★ the liveness DOWN alert is deleted outright (it survived 242 tests before)",
|
|
280
|
+
' down_reason = poll.reason or "unreachable"\n'
|
|
281
|
+
' self._alarm("alert", "source is DOWN: %s" % down_reason,\n'
|
|
282
|
+
' reason=down_reason,\n'
|
|
283
|
+
' consecutive_failures=self.failures,\n'
|
|
284
|
+
' seconds=self.failures * args.poll_seconds)',
|
|
285
|
+
' pass'),
|
|
286
|
+
("L11-F1: the DOWN alert loses its stderr fallback (undelivered == silent again)",
|
|
287
|
+
' sys.stderr.write("kijito-inbox-monitor: %s EVENT UNDELIVERED (persona %r): %s\\n"\n'
|
|
288
|
+
' % (event.upper(), self.persona, log_text))',
|
|
289
|
+
' pass'),
|
|
290
|
+
("L11-F1: ★ the gap alarm latches BEFORE delivery again (an undelivered alarm never re-raises)",
|
|
291
|
+
' if self._alarm("alert", gap_reason,',
|
|
292
|
+
' self.gap_alerted = cursor_at\n'
|
|
293
|
+
' if self._alarm("alert", gap_reason,'),
|
|
294
|
+
("L11-F1: WatchTarget.lifecycle drops the emitter's answer again",
|
|
295
|
+
" return self.emitter.lifecycle(event, **fields)",
|
|
296
|
+
" self.emitter.lifecycle(event, **fields)"),
|
|
297
|
+
("L11-F1: the FAST-PATH recovered edge stops reporting an undelivered event",
|
|
298
|
+
' self._alarm("recovered", "source recovered", cursor=self.cursor)',
|
|
299
|
+
' self.lifecycle("recovered", cursor=self.cursor)'),
|
|
300
|
+
("L11-F1: the MAIN-PATH recovered edge stops reporting an undelivered event",
|
|
301
|
+
' self._alarm("recovered", "source recovered", cursor=self.cursor)',
|
|
302
|
+
' self.lifecycle("recovered", cursor=self.cursor)'),
|
|
303
|
+
("L11-F3: the startup persona list dedupes EXACTLY again (case-variants self-deadlock)",
|
|
304
|
+
" key = p.casefold()\n if p and key not in seen:",
|
|
305
|
+
" key = p\n if p and key not in seen:"),
|
|
306
|
+
|
|
307
|
+
# ---- the containment residual: InsecureFile is an OSError, NOT a FatalConfig ---------------------
|
|
308
|
+
# Each arm gets its OWN entry, deliberately: river's binding condition on this fix, because a
|
|
309
|
+
# containment guard with no mutation is EXACTLY how this defect stayed invisible for two rounds. The
|
|
310
|
+
# narrowing below is the original bug, restored verbatim - a guard that names the wrong exception type
|
|
311
|
+
# is indistinguishable at a glance from one that works, so only a mutation can tell them apart.
|
|
312
|
+
("R0-arm1: DIRECTORY rediscovery catches FatalConfig only, so a hostile lock kills the producer",
|
|
313
|
+
"except (FatalConfig, OSError) as e:\n # \u2605 THE CATCH",
|
|
314
|
+
"except FatalConfig as e:\n # \u2605 THE CATCH"),
|
|
315
|
+
("R0-arm2: COUNTS discovery catches FatalConfig only, so a hostile lock kills the producer",
|
|
316
|
+
"except (FatalConfig, OSError) as e:\n # Same widening",
|
|
317
|
+
"except FatalConfig as e:\n # Same widening"),
|
|
318
|
+
("R0-arm3: the STARTUP path catches FatalConfig only, so one hostile persona stops all the others",
|
|
319
|
+
'except (FatalConfig, OSError) as e:\n _warn_persona_once(p, "cannot watch persona',
|
|
320
|
+
'except FatalConfig as e:\n _warn_persona_once(p, "cannot watch persona'),
|
|
321
|
+
# A duplicate FatalConfig arm compiles and is simply unreachable, so this deletes the backstop
|
|
322
|
+
# without introducing a SyntaxError the harness would refuse to count.
|
|
323
|
+
("R0-arm4: the top-level OSError backstop is gone, so an escaping one is a traceback again",
|
|
324
|
+
" except OSError as e:\n # THE BACKSTOP",
|
|
325
|
+
" except FatalConfig as e:\n # THE BACKSTOP"),
|
|
326
|
+
("R0: containment degrades into a watcher that is UP and watching nothing (fail-closed removed)",
|
|
327
|
+
" if not targets:\n # FAIL CLOSED.",
|
|
328
|
+
" if False:\n # FAIL CLOSED."),
|
|
329
|
+
# ladybug's review finding on c6e1699: the urgent alarm was gated by --no-stranded-alerts, so the
|
|
330
|
+
# stranded flag's own advice ("set this if you keep deliberate test inboxes") silenced a
|
|
331
|
+
# higher-severity alarm about real members. Re-coupling them is the regression worth pinning.
|
|
332
|
+
("A1: the urgent-unanswered alarm is re-coupled to --no-stranded-alerts (one flag silences both again)",
|
|
333
|
+
" if counts_available and not args.no_urgent_alerts:",
|
|
334
|
+
" if counts_available and not args.no_stranded_alerts:"),
|
|
335
|
+
]
|
|
336
|
+
def run(src):
|
|
337
|
+
# RELEASE WHAT WE ACQUIRE (Loom re-audit 10, L6). The temp tree was never removed and the source file
|
|
338
|
+
# was written through an unbound handle, so a full gate run leaked one directory per mutant plus a
|
|
339
|
+
# descriptor each. In a tool whose whole purpose is to catch resource and lifecycle defects, leaking
|
|
340
|
+
# both is not merely untidy - it is the harness exhibiting the class it is meant to detect.
|
|
341
|
+
d=tempfile.mkdtemp()
|
|
342
|
+
try:
|
|
343
|
+
shutil.copy(TESTS, os.path.join(d,"test_kijito_monitor.py"))
|
|
344
|
+
with open(os.path.join(d,"kijito_inbox_monitor.py"),"w") as fh:
|
|
345
|
+
fh.write(src)
|
|
346
|
+
# PIN THE WARNING FILTER. Inherited PYTHONWARNINGS=error turns a mutant's leaked fd into an ERROR,
|
|
347
|
+
# which silently converts "survived" into "caught" - a gate whose verdict depends on the caller's
|
|
348
|
+
# environment is not a gate (Loom re-audit 9, MEDIUM: 55+ ResourceWarnings under warnings=error).
|
|
349
|
+
env=dict(os.environ); env["PYTHONWARNINGS"]="default"
|
|
350
|
+
try:
|
|
351
|
+
p=subprocess.run([sys.executable,"-m","unittest","test_kijito_monitor"],cwd=d,capture_output=True,
|
|
352
|
+
text=True,env=env,timeout=MUTANT_TIMEOUT)
|
|
353
|
+
except subprocess.TimeoutExpired:
|
|
354
|
+
# A mutant that HANGS is not caught - nothing asserted anything, the suite simply stopped. It also
|
|
355
|
+
# used to stall the gate itself indefinitely, which is how a quality tool becomes one nobody runs.
|
|
356
|
+
return None, "HUNG"
|
|
357
|
+
return p.returncode, p.stderr
|
|
358
|
+
finally:
|
|
359
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
360
|
+
with open(SRC) as fh: base=fh.read()
|
|
361
|
+
rc,_=run(base)
|
|
362
|
+
if rc!=0: print("BASELINE NOT GREEN"); sys.exit(1)
|
|
363
|
+
print("baseline: GREEN\n")
|
|
364
|
+
surv=[]
|
|
365
|
+
for label,pat,rep in M:
|
|
366
|
+
if pat not in base:
|
|
367
|
+
print("!! PATTERN NOT FOUND (vacuous):",label); surv.append(label); continue
|
|
368
|
+
mut=base.replace(pat,rep,1)
|
|
369
|
+
try: compile(mut,"m","exec")
|
|
370
|
+
except SyntaxError as e:
|
|
371
|
+
print("!! DOES NOT COMPILE:",label,e); surv.append(label); continue
|
|
372
|
+
rc,err=run(mut)
|
|
373
|
+
if rc is None:
|
|
374
|
+
print("!! HUNG, not a catch:",label); surv.append(label); continue
|
|
375
|
+
m=re.search(r"FAILED \((.*)\)",err); det=m.group(1) if m else None
|
|
376
|
+
total=re.search(r"Ran (\d+) tests",err)
|
|
377
|
+
nerr=re.search(r"errors=(\d+)",det or ""); nfail=re.search(r"failures=(\d+)",det or "")
|
|
378
|
+
if rc==0: print("SURVIVED ",label); surv.append(label)
|
|
379
|
+
elif total is None or det is None:
|
|
380
|
+
# A RUN THAT PRODUCED NO TEST VERDICT IS NOT A CATCH (Loom re-audit 10, M5). `det` used to default
|
|
381
|
+
# to the string "error" when no summary matched, and the chain's terminal `else` was the OPTIMISTIC
|
|
382
|
+
# arm - so a mutant that killed the interpreter outright (a signal, os._exit, an import-time crash)
|
|
383
|
+
# matched no failure pattern and was recorded as CAUGHT. Nothing asserted anything; the process
|
|
384
|
+
# merely died, which proves the mutation is DETECTABLE but not that any TEST detects it.
|
|
385
|
+
# `total` was computed here and never read - the same dropped-answer half of the class, inside the
|
|
386
|
+
# gate itself. It is now the liveness precondition: no "Ran N tests" means the suite never ran, and
|
|
387
|
+
# a check that could not run must never be scored as a check that passed.
|
|
388
|
+
print("!! NO TEST VERDICT (crash/signal/exit %s), not a catch:"%rc,label); surv.append(label)
|
|
389
|
+
elif nfail is None and nerr:
|
|
390
|
+
# STRICTLY: no FAILURES means no test NOTICED the behaviour - it only noticed the program
|
|
391
|
+
# breaking. The old form allowed this whenever errors were under half the suite, which is how
|
|
392
|
+
# two error-only mutants were being counted as caught while the docstring said otherwise
|
|
393
|
+
# (Loom re-audit 9, MEDIUM). A threshold here is a way of being slightly wrong on purpose.
|
|
394
|
+
print("!! ERRORS ONLY, not a catch:",label,det); surv.append(label)
|
|
395
|
+
else: print("caught ",label," (%s)"%det)
|
|
396
|
+
print()
|
|
397
|
+
print("%d SURVIVED"%len(surv) if surv else "all %d mutations caught"%len(M))
|
|
398
|
+
for x in surv: print(" -",x)
|
|
399
|
+
sys.exit(1 if surv else 0)
|