Files
680ecfbf47 A second Python client, for logosctl, with its own parallel suite (#18)
* feat: a second Python client, for logosctl, with its own suite

This repo drove one CLI. It now drives two, side by side, the same way
the CLI repo ships two binaries: src/logoscore/ is untouched and remains
the one to use, and src/logosctl/ is the new client for the merged CLI.

The port is not a rename. logosctl deleted the flags this client was
built on -- -m, --persistence-path, --module-transport -- and, more
sharply, deleted the entire LOGOSCORE_CLIENT_* family that the old
wrapper used to point a client at a daemon. logosctl honours exactly two
variables, LOGOSCTL_CONFIG_DIR and LOGOSCTL_TOKEN. So everything those
flags expressed now has to be written as a YAML document and installed
with `daemon config set` / `client config set` BEFORE the daemon starts.
That two-phase contract is the substance of the port; the regrouped
subcommands (list-modules -> module ls) are the easy part.

Two asymmetries the port has to respect, both real and neither a typo:
the daemon says `protocol:` where the client says `transport:`, and the
daemon has cert/key/ca_file where the client has only `ca`.

tests/logosctl/ mirrors the existing suite and is a deliberate duplicate
-- the two clients drive genuinely different surfaces, and duplicating
means retiring logoscore is a delete rather than an unpick. The shared
_fullapi_module_cases.py table is IMPORTED, not copied: it describes the
module's contract, not the CLI's, and two copies would drift.

New checks (unit-logosctl, integration-logosctl-{local,tcp,tcp_ssl}) are
separate derivations, so nix builds them concurrently with the logoscore
ones and a failure in the under-validation client cannot mask a
logoscore regression.

The logosctl binary comes from a SECOND flake input pinned to the CLI
branch, rather than by re-pointing the shared one. Re-pinning would have
changed what the frozen logoscore checks build against -- a behavioural
change to the half that was supposed to be untouched, buried in a
215k-line lock diff. Verified: every shared pin is byte-identical to
master. The input is commented with the condition for removing it.

An adversarial review of the first draft found nine defects, all fixed
here. The two worth naming, because both are the same mistake made
twice:

  - the "daemon exited during startup" path read only stderr, but
    logosctl's LogSink dup2s stdout and stderr into one pipe, so the
    reason a daemon died arrives on stdout. Every post-LogSink failure
    raised a bare exit code with nothing attached -- the same dead end
    the CLI's own --detach path had.
  - the startup-timeout error pointed at a log file that teardown had
    already deleted. The log tails now travel with the exception.

Also: the ordering test asserted the run list and the spawn list
separately, so it stayed green with the two phases swapped -- it
certified the one thing it did not check. It now records both into a
single ordered sequence.

Green: unit-logosctl, integration-logosctl-local, and
integration-logosctl-tcp_ssl, the last exercising a real certificate
with subjectAltName and verify_peer on, plus its mirror with the CA
withheld so the test can tell "verified" from "not checking".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: drain the watcher's stderr, and don't trust token_file's type

Both from Copilot's review of #18, both real.

**A subscription could deadlock.** `Subscription.start` pipes stderr and
nothing ever reads it. A pipe nobody drains fills at ~64K and blocks the
child mid-write — and a watcher blocked writing stderr stops writing
stdout, so the event stream stalls forever with no error, no exit and no
timeout. The worst shape a bug can take: it looks like a quiet module.

stderr is now drained by its own thread into a bounded tail. Draining
rather than sending it to /dev/null keeps the diagnostics, and that
turned out to be worth having on its own: a watcher whose process exits
non-zero now reports the exit code AND what it wrote on the way out.
Before, the subscription just went silent, indistinguishable from one
with nothing to emit.

**`token_file` was assumed to be a string.** `setdefault` only fills the
key in when it is ABSENT, so a merged config could still carry a number,
a null or a list — a hand-edited or corrupt file — and `Path(42)` raises
TypeError, aborting the whole config write over a value we were always
going to reject. The function already falls back to auto.json for a path
or a traversal; a non-string is one more thing it cannot honor.

Tests first verified against the unfixed code, where they fail for their
own reasons rather than incidentally: the deadlock test times out waiting
for an event that never comes, the death test observes silence, and the
token_file cases raise TypeError. The deadlock test spawns a real process
that writes 400 KB to stderr — comfortably past any platform's pipe
buffer — and then emits one event; with an undrained pipe it hangs, and
the file takes 30s to fail instead of 1s to pass.

NOT fixed here: src/logoscore/events.py has the same undrained pipe. It
is the frozen client and this PR asserts it is untouched, so it gets its
own change rather than riding along in this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* build: point logosctl at logos-logoscore-cli master

logos-logoscore-cli#76 has merged, so `ctl` lives on the same pin as
`cli`. Drop the temporary second input that tracked the feature branch,
repoint both logosctlBin bindings, and bump the lock to e48fc7f.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 17:04:15 -03:00

46 lines
1.5 KiB
TOML

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "logoscore"
version = "0.1.0"
description = "Python wrapper for the logoscore CLI — launch daemons, load modules, call methods, subscribe to events."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT OR Apache-2.0" }
authors = [
{ name = "Logos", email = "info@logos.co" },
]
keywords = ["logos", "logoscore", "modules", "ipc"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: Apache Software License",
]
dependencies = []
[project.urls]
Homepage = "https://github.com/logos-co/logos-logoscore-py"
Issues = "https://github.com/logos-co/logos-logoscore-py/issues"
"logoscore CLI" = "https://github.com/logos-co/logos-logoscore-cli"
[project.optional-dependencies]
test = ["pytest>=7"]
[tool.hatch.build.targets.wheel]
# Two clients, one distribution. `logoscore` drives the `logoscore` binary,
# `logosctl` drives `logosctl`; they share nothing but this wheel. Both ship
# until one of the two CLIs is retired, and that removal is one entry here
# plus the matching directory under src/ — nothing to untangle.
packages = ["src/logoscore", "src/logosctl"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"