kijito-tools 0.2.6 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kijito-tools",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Installer for kijito-tools: copies the context-tracking, session catch-up, and self-clear scripts plus the Kijito skills into ~/.claude.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -3,6 +3,23 @@
3
3
  All notable changes to kijito-inbox-monitor are documented in this file.
4
4
  The format is based on Keep a Changelog, and this project follows Semantic Versioning.
5
5
 
6
+ ## [0.5.8] - 2026-09-26
7
+
8
+ ### Fixed
9
+ - **Runs on native Windows** (reported from Windows 11, CPython 3.14). Four problems, each of which stopped
10
+ the producer or corrupted delivery there:
11
+ - Startup crashed with `AttributeError: module 'os' has no attribute 'geteuid'`. The private-file guard's
12
+ owner and 0600 checks are POSIX-only now; on Windows it still refuses anything that is not a regular
13
+ file.
14
+ - A new events file never became durable. Windows cannot open a directory to fsync it, so every sync
15
+ reported failure and the cursor was held forever, re-delivering the same mail. The directory fsync is
16
+ skipped on Windows (NTFS journals directory metadata).
17
+ - Every directory level printed a "writable by other local users" warning, because Windows reports
18
+ directories as mode 0777. That warning is POSIX-only now.
19
+ - On a cp1252 console or Git Bash pipe, `--help` crashed with `UnicodeEncodeError`, and so did any event
20
+ whose message body had a character outside cp1252 (an emoji, CJK) - mid-delivery. stdout is now
21
+ written as UTF-8, which JSON Lines requires anyway, and the help text is ASCII.
22
+
6
23
  ## [0.5.7] - 2026-09-25
7
24
 
8
25
  ### Added
@@ -1,13 +1,13 @@
1
1
  # The upstream release providers/monitor is a byte-exact copy of (row M314).
2
2
  # Written by scripts/import-monitor.sh; verified by tests/vendored_monitor_test.sh. Never hand-edit.
3
3
  repo=KijitoAI/kijito-inbox-monitor
4
- sha=9ac160aa98d8205a9eabe05378d9fd7ac25d8273
5
- tree=65ee5cf22640992369c09d6d6f1707116093908b
6
- tag=v0.5.7
4
+ sha=e3c69db68451e3e55b5b3da9c98546706fff7bff
5
+ tree=70a8b4dfc04c7832aec37cb9124178e75f7248c9
6
+ tag=v0.5.8
7
7
  file=100644 8d88ccb0fd09b9144505f985b4f882db278fac1f .github/workflows/publish-npm.yml
8
8
  file=100644 19ce3471a2cffa689be9d261049c1e74e3e7a25e .github/workflows/publish-pypi.yml
9
9
  file=100644 4534290a5dd36d18d07a782b784b6b0ad5ee38fc .gitignore
10
- file=100644 483f8aaa29ab3500ff421315bca2d894b3c40fc6 CHANGELOG.md
10
+ file=100644 b5cd0382bcfa90cd71fa39577a41a47707ac2bac CHANGELOG.md
11
11
  file=100644 d645695673349e3947e8e5ae42332d0ac3164cd7 LICENSE
12
12
  file=100644 f6b0e5634f3fd4156c1c000efb31578bfb068cf3 NOTICE
13
13
  file=100644 73d2bf5a5d5d6cb622db014285cd9ada4c705724 README.md
@@ -15,12 +15,12 @@ file=100644 72f0c148f1e721ac1bb4a38e5cae690355774314 RELEASING.md
15
15
  file=100755 fca1203267cb726736b0c8f239e2d74c40a7a895 arm-hive-monitor.sh
16
16
  file=100755 bbe3f07de13dab8a8a7a4798481156ae4a46ab8f bin/cli.js
17
17
  file=100644 fffa8bd23749a321285bd942a3f4f4a98936129c com.kijito.inbox-monitor.plist.template
18
- file=100644 fc953e43bcd3069a1dc426ddefc751d8f01cf599 docs/DESIGN.md
18
+ file=100644 b0c07b55c75e6061e6c82d5e23c2b7ddad5131cf docs/DESIGN.md
19
19
  file=100644 4f5e53a8633e60e6c57c451e28546f8af8783fe8 kijito-inbox-monitor@.service.template
20
- file=100644 db09ae46bf223a01ea0c2903cc886e6e09b21d43 kijito_inbox_monitor.py
21
- file=100644 9cea8637ea9c61323ac2298da3d521454eabd85a package.json
22
- file=100644 d0af8757fa4d3c3f4591f64a0e3d67d1637a9210 pyproject.toml
20
+ file=100644 692aadd5fe603556e0ba0e9358c1487022b2bc05 kijito_inbox_monitor.py
21
+ file=100644 53538cfa0dd2a25f68d54b2a62fb072d599cec83 package.json
22
+ file=100644 44b676233aecdc02dc462a4144dd29120ff05d73 pyproject.toml
23
23
  file=100755 b34a5c775385ecfaefd4b374c1fc1aa082a85d14 scripts/migrate-systemd-unit.sh
24
24
  file=100755 22acef6d6875395a81c701eb535030d0a78b0df1 scripts/mutation-check.py
25
25
  file=100755 acba6196213736922fc4f7135749386d85d172a6 scripts/prepublish-gate.sh
26
- file=100644 8d881e5f8562d4ce6b1b22fc97bf9f1ade1d52fb test_kijito_monitor.py
26
+ file=100644 affc54d7cf969631c3f2d9160995e933cde17a60 test_kijito_monitor.py
@@ -28,7 +28,9 @@ no pip installs) that polls the Kijito inbox and emits one event per new message
28
28
  running, as NDJSON on stdout and/or exec-a-command-per-event. It is the client-side liveness watcher: it
29
29
  keeps a running agent's inbox live by waking it between tool calls. It is not a server, and not a
30
30
  notification service. POSIX target (Linux/macOS); Windows runs interval-only (no SIGUSR1 seam, no flock,
31
- per §10/§7.3).
31
+ per §10/§7.3). On Windows the private-file guard checks only "regular file" (there is no POSIX owner or
32
+ mode; access is the profile's inherited ACL), a directory fsync is skipped (Windows cannot open a directory
33
+ to fsync it; NTFS journals the metadata), and stdout is written as UTF-8 whatever the console code page.
32
34
 
33
35
  ## 2. The problem
34
36
 
@@ -33,7 +33,7 @@ try:
33
33
  except ImportError: # pragma: no cover - Windows
34
34
  fcntl = None
35
35
 
36
- __version__ = "0.5.7"
36
+ __version__ = "0.5.8"
37
37
  SOURCE = "kijito-inbox"
38
38
  # A named User-Agent is REQUIRED: api.kijito.ai is fronted by a WAF that 403s the default Python-urllib UA.
39
39
  USER_AGENT = "kijito-inbox-monitor/%s" % __version__
@@ -1261,6 +1261,12 @@ def _assert_private_fd(fd, path):
1261
1261
  st = os.fstat(fd)
1262
1262
  if not stat.S_ISREG(st.st_mode):
1263
1263
  raise InsecureFile("%s is not a regular file" % path)
1264
+ if not IS_POSIX:
1265
+ # WINDOWS HAS NO POSIX OWNER OR MODE BITS TO CHECK (praetor, Windows 11 native, 2026-09-26: os.geteuid
1266
+ # does not exist there, so this line crashed the producer at startup). Access there is an ACL, and a
1267
+ # file created under the user's profile inherits a user-only ACL; st_mode reports a synthetic
1268
+ # 0666/0444 that says nothing about who can read it. The regular-file check above still holds.
1269
+ return
1264
1270
  if st.st_uid != os.geteuid():
1265
1271
  raise InsecureFile("%s is owned by uid %d, not by us (uid %d)" % (path, st.st_uid, os.geteuid()))
1266
1272
  cur = st.st_mode & 0o777
@@ -1364,6 +1370,10 @@ def _makedirs_private(path):
1364
1370
  # directory is never validated, and an existing one is exactly where a hostile path would already be.
1365
1371
  # A sticky directory (/tmp, mode 1777) is excluded: the sticky bit is precisely what makes a shared
1366
1372
  # writable directory safe, and warning about it would train the reader to ignore this line.
1373
+ # POSIX only: Windows reports every directory as 0777 (access is an ACL), so the warning would fire on
1374
+ # every level of every path and say nothing true.
1375
+ if not IS_POSIX:
1376
+ return
1367
1377
  seen = path
1368
1378
  while True:
1369
1379
  try:
@@ -1386,7 +1396,13 @@ def _fsync_dir(path):
1386
1396
  os.replace is atomic for a concurrent READER, but atomicity is not durability: after a power loss
1387
1397
  the new file's contents can be on disk while the directory entry still names the old inode - i.e. a
1388
1398
  silently OLDER cursor. Syncing the file alone (which is all we did) does not cover the rename.
1399
+
1400
+ Windows cannot open a directory for fsync at all, so this always failed there and the events sink held
1401
+ its cursor forever on a new file. NTFS journals directory metadata itself, so there is nothing further
1402
+ to do: report success.
1389
1403
  """
1404
+ if not IS_POSIX:
1405
+ return True
1390
1406
  try:
1391
1407
  fd = os.open(path, os.O_RDONLY)
1392
1408
  except OSError:
@@ -3703,7 +3719,7 @@ def build_parser():
3703
3719
  p.add_argument("--wait", type=int, default=50,
3704
3720
  help="Long-poll hold (s) requested from /api/notify/pending so new mail wakes the watcher "
3705
3721
  "near-instantly at ~the same request rate (default 50; the server clamps to its own max). "
3706
- "0 disables long-poll → plain interval polling at --poll-seconds. If the server doesn't "
3722
+ "0 disables long-poll -> plain interval polling at --poll-seconds. If the server doesn't "
3707
3723
  "support long-poll, the client auto-falls back to interval polling (no redeploy needed). "
3708
3724
  "Clean shutdown during a held poll can take up to --wait seconds (a supervisor's SIGKILL "
3709
3725
  "mid-hold is safe - state is persisted every cycle).")
@@ -3840,7 +3856,25 @@ def validate_args(args):
3840
3856
  "not multi-persona/all-personas - each persona has its own cursor")
3841
3857
 
3842
3858
 
3859
+ def _utf8_stdout():
3860
+ """Write stdout as UTF-8 whatever the locale says (praetor, Windows 11 native, 2026-09-26).
3861
+
3862
+ A Windows console or a Git Bash pipe reports cp1252, so `--help` (a non-ASCII character) and, far worse,
3863
+ any EVENT carrying a message body outside cp1252 (an emoji, CJK) raised UnicodeEncodeError - a
3864
+ ValueError, which the stdout sink's OSError guard does not catch, so it killed the producer mid-delivery.
3865
+ JSON Lines is UTF-8 by definition, so that is the right encoding for every consumer. No-op where stdout
3866
+ is already UTF-8 or cannot be reconfigured.
3867
+ """
3868
+ enc = (getattr(sys.stdout, "encoding", None) or "").lower().replace("-", "").replace("_", "")
3869
+ if enc != "utf8" and hasattr(sys.stdout, "reconfigure"):
3870
+ try:
3871
+ sys.stdout.reconfigure(encoding="utf-8")
3872
+ except (ValueError, OSError):
3873
+ pass
3874
+
3875
+
3843
3876
  def main(argv=None):
3877
+ _utf8_stdout()
3844
3878
  args = build_parser().parse_args(argv)
3845
3879
  # A pure read of an existing report: no token, no network, no state file, no watch loop. Placed
3846
3880
  # before validate_args so a heartbeat can call it without satisfying the watcher's own config.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kijito-inbox-monitor",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
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
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "kijito-inbox-monitor"
10
- version = "0.5.7"
10
+ version = "0.5.8"
11
11
  description = "Zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -6274,3 +6274,83 @@ class PersistedUnreadCountTest(unittest.TestCase):
6274
6274
 
6275
6275
  def test_zero_loads(self):
6276
6276
  self.assertEqual(self._load({"unread": 0})["unread"], 0)
6277
+
6278
+
6279
+ class WindowsNativeTest(unittest.TestCase):
6280
+ """praetor's report (Windows 11 native, CPython 3.14, 2026-09-26): the producer crashed at startup on
6281
+ `os.geteuid` (POSIX-only) in _assert_private_fd, and `--help` crashed on a cp1252 console. The next two
6282
+ walls after that were `_fsync_dir` (Windows cannot open a directory to fsync it, so the events sink held
6283
+ its cursor forever on a new file) and a spurious "writable by other users" warning (Windows reports
6284
+ every directory as 0777). Windows is SIMULATED here: IS_POSIX False and os.geteuid removed."""
6285
+
6286
+ def setUp(self):
6287
+ self._d = tempfile.TemporaryDirectory()
6288
+ self.addCleanup(self._d.cleanup)
6289
+ self._posix, km.IS_POSIX = km.IS_POSIX, False
6290
+ self.addCleanup(setattr, km, "IS_POSIX", self._posix)
6291
+ self._geteuid = getattr(os, "geteuid", None)
6292
+ if self._geteuid is not None:
6293
+ del os.geteuid
6294
+ self.addCleanup(setattr, os, "geteuid", self._geteuid)
6295
+
6296
+ def test_a_regular_file_passes_without_geteuid_and_is_not_chmodded(self):
6297
+ p = os.path.join(self._d.name, "events.jsonl")
6298
+ with open(p, "w"):
6299
+ pass
6300
+ os.chmod(p, 0o644)
6301
+ fd = os.open(p, os.O_RDONLY)
6302
+ try:
6303
+ km._assert_private_fd(fd, p) # crashed with AttributeError before the fix
6304
+ finally:
6305
+ os.close(fd)
6306
+ self.assertEqual(os.stat(p).st_mode & 0o777, 0o644, "no POSIX mode repair is attempted off POSIX")
6307
+
6308
+ def test_a_non_regular_file_is_still_refused(self):
6309
+ fd = os.open(self._d.name, os.O_RDONLY)
6310
+ try:
6311
+ with self.assertRaises(km.InsecureFile):
6312
+ km._assert_private_fd(fd, self._d.name)
6313
+ finally:
6314
+ os.close(fd)
6315
+
6316
+ def test_fsync_dir_reports_success(self):
6317
+ self.assertTrue(km._fsync_dir(os.path.join(self._d.name, "no-such-dir")))
6318
+
6319
+ def test_no_writable_directory_warning_off_posix(self):
6320
+ d = os.path.join(self._d.name, "open")
6321
+ os.mkdir(d)
6322
+ os.chmod(d, 0o777)
6323
+ buf = _capture_stderr(self)
6324
+ km._makedirs_private(os.path.join(d, "sub"))
6325
+ self.assertNotIn("writable by other local users", buf.getvalue())
6326
+
6327
+ def test_the_events_file_sink_writes_and_syncs(self):
6328
+ # End to end: before the fix a new events file made sync() return False on every poll (the
6329
+ # directory fsync), so the cursor never advanced and mail was re-delivered forever.
6330
+ sink = km.RotatingFileSink(os.path.join(self._d.name, "argus.jsonl"), 0, 3)
6331
+ self.addCleanup(sink.close)
6332
+ self.assertTrue(sink.write('{"event": "new", "content": "caf\\u00e9 \\u2713"}\n'))
6333
+ self.assertTrue(sink.sync())
6334
+
6335
+
6336
+ class Utf8StdoutTest(unittest.TestCase):
6337
+ """A cp1252 stdout (a Windows console, a Git Bash pipe) must neither crash `--help` nor an event whose
6338
+ message body is outside cp1252. Real subprocesses, because the defect is the interpreter's own encoding."""
6339
+
6340
+ HERE = os.path.dirname(os.path.abspath(__file__))
6341
+
6342
+ def _run(self, code):
6343
+ env = dict(os.environ, PYTHONIOENCODING="cp1252")
6344
+ env.pop("PYTHONUTF8", None)
6345
+ return subprocess.run([sys.executable, "-c", code], cwd=self.HERE, env=env, capture_output=True, timeout=60)
6346
+
6347
+ def test_help_does_not_crash_on_cp1252(self):
6348
+ r = self._run("import kijito_inbox_monitor as km, sys\ntry:\n km.main(['--help'])\nexcept SystemExit as e:\n sys.exit(e.code)")
6349
+ self.assertEqual(r.returncode, 0, r.stderr.decode("utf-8", "replace")[-300:])
6350
+
6351
+ def test_an_event_outside_cp1252_is_delivered_as_utf8(self):
6352
+ r = self._run("import kijito_inbox_monitor as km\nkm._utf8_stdout()\n"
6353
+ "em = km.Emitter('stdout-jsonl', None, 220, False)\n"
6354
+ "assert em.lifecycle('armed', persona='\\u6f22\\U0001F600') is True")
6355
+ self.assertEqual(r.returncode, 0, r.stderr.decode("utf-8", "replace")[-300:])
6356
+ self.assertIn("\u6f22\U0001F600", r.stdout.decode("utf-8"))