jobbsok/tests/test_public_surface.py
Kjell Tore Guttormsen dcd3eae534 feat(port): move five bash entry points to Python and drop bash from .mcp.json
jobbsok could not start on stock Windows. `.mcp.json` named `bash` as the
command, and the five entry points behind it reached for grep, sed, find, awk,
zip and unzip. `bootstrap.sh` built the virtualenv, so a Windows adopter could
not even reach an interpreter. Two adopters are waiting and neither is
guaranteed to be on macOS, so this is the install, not a rough edge.

The Python layer was already clean -- no /tmp, no /usr, no os.uname, no home
directory assumption -- so only the shell layer moved. Behaviour is carried
over unchanged; the deliberate exceptions are listed in docs/.

THE ONE OPEN DECISION, AND WHY IT WAS FORCED

How does .mcp.json start an interpreter without a POSIX shell, when it is
called python3 on macOS and python or py on Windows? Measured against the
installed CLI, not assumed:

  - The plugin mcpServers stdio schema has NO platform-conditional form. A
    config carrying invented windows/darwin/platform keys was accepted and the
    keys were silently discarded -- it fails quietly, not loudly.
  - ${VAR:-default} IS expanded, in command, args and env.
  - ${VAR} without a default is not safe: unset, it is passed through
    unexpanded, so the spawn would try to run a program named ${VAR}.
  - Windows spawns with shell:false, so a .py path as command is out.
  - No single literal works. On this Mac, python and py are not on PATH.

So the default form is the only lever the schema offers:
"${JOBBSOK_LAUNCH_PYTHON:-python3}". macOS and Linux keep working with nothing
set; Windows sets one variable and needs no shell.

A SECOND VARIABLE, NOT A REUSE OF JOBBSOK_PYTHON

JOBBSOK_PYTHON names the interpreter to SERVE on: the launcher treats it as an
explicit operator choice, so it wins over the bootstrapped virtualenv. A
Windows adopter setting it merely to spell `python` would silently bypass that
virtualenv and serve WITHOUT the ingestion guard. JOBBSOK_LAUNCH_PYTHON only
says how to start the launcher. A test asserts the two never collapse into one.

O4 IS LEFT STANDING

The launcher still gates on the interpreter's version rather than on the guard
being importable. That is the shell version's semantics carried over on
purpose: harmless while nothing writes, a defect from M2, and an M2 decision.

VERIFY

  - pytest tests/                      -> 124 passed, exit 0 (was 112)
  - grep -c '"command": "bash"' .mcp.json -> 0
  - git ls-files 'scripts/*.sh'        -> 0
  - README install block names Windows, and neither WSL nor Git Bash
  - server started end to end exactly as .mcp.json expands, and answered
    initialize and tools/list
  - the ported probe checker reproduces the shell version's output and exits 0

NOT MEASURED, AND NOT ASSUMED

Nothing here has ever run on Windows. Whether Cowork on Windows bridges to a
host-side stdio MCP as it does on this Mac is unmeasured -- docs/cowork-probe.md
covered macOS only. docs/cross-platform-port.md says what a Windows probe would
have to measure, and records two findings left deliberately untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:02:03 +02:00

203 lines
8.3 KiB
Python

"""What this repository publishes, and what it must never publish (plan Step 14).
The remote is PUBLIC. That single fact decides both halves of this file.
The first half is presentation: a README first screen that says what the plugin
is, an install block that covers both surfaces it actually runs on -- Claude
Code and Claude Cowork, which install it in completely different ways -- and a
`SECURITY.md` that states the trust model instead of implying it.
The second half is the one that matters more, and it is deliberately wider than
the two files this step writes. The scan reads **every tracked file**, because
the leak this repository is exposed to does not live in the README. It lives in
`docs/cowork-probe.md`, which is committed here and is appended to by six later
steps with measured facts from the operator's own machine and browser (risk
H1). A two-file scan would never look at it. The first run of this scan found
a real job-ad identifier there, published, which is the whole argument for
scanning wide compressed into one finding.
Three deliberate scopings, stated rather than smuggled, because a rule that
cannot be stated is a rule that will be quietly widened later:
1. **An absolute home path means a literal one.** `/Users/<name>/` and
`/home/<name>/` are refused; `$HOME/Library/...` is not, because that is the
correct way to write the same path and `scripts/cowork_probe_check.py` has
to name a macOS directory to read a log out of it.
2. **Infrastructure hosts are allowed by name.** The Forgejo host this
repository lives on and the badge service in the README are addresses the
published repository cannot function without. Every other host in a URL that
points at a resource must be under `.example` (RFC 2606).
3. **`tests/test_fixture_hygiene.py` is exempt from the URL rule**, and only
from that one. It seeds a synthetic job-board URL as a canary to prove its
own scan can find, and a scanner cannot scan another scanner's canary
without one of the two being wrong.
The scan reports its denominator on every run, and proves it can find before it
reports that it found nothing. "Found nothing" is a measurement; without a
denominator and a positive control it is indistinguishable from a scan whose
patterns were wrong.
Style note: this file follows tests/test_fixture_hygiene.py.
"""
import os
import re
import subprocess
import warnings
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
README = os.path.join(REPO, "README.md")
SECURITY = os.path.join(REPO, "SECURITY.md")
class OffentligFlateSkannet(UserWarning):
"""Carries the denominator into the run's output, where a reader sees it."""
#: A literal home directory with a user name in it. Written as a pattern rather
#: than a literal so this file does not match itself.
HJEMMESTI = re.compile(r"/(?:Users|home)/[A-Za-z0-9._-]+/")
#: A URL pointing at a resource -- with a path, so a bare domain named in prose
#: is not a finding. Both spellings the corpus uses are covered.
ADRESSE = re.compile(r"(?:https?://|\bwww\.)([A-Za-z0-9.-]+\.[A-Za-z]{2,})(/[^\s`'\")\]]*)")
#: Hosts the published repository cannot function without.
INFRASTRUKTUR = ("git.fromaitochitta.com", "img.shields.io")
#: Exempt from the URL rule only, and for the reason in this module's docstring.
URL_UNNTAK = ("tests/test_fixture_hygiene.py",)
#: The invariants that are live at M1. M3 and M6 extend the list as their
#: surfaces land; these four are true from the first commit.
IKKE_FORHANDLINGSBART = (
"read-only",
"credential",
"local",
"guard",
)
def sporede_filer():
ut = subprocess.run(
["git", "-C", REPO, "ls-files"], capture_output=True, text=True
)
assert ut.returncode == 0, ut.stderr
return [p for p in ut.stdout.split("\n") if p]
def skann(rot, filer):
"""Return (findings, files read, bytes read) for ``filer`` under ``rot``."""
funn = []
lest = 0
bytes_lest = 0
for relativ in filer:
full = os.path.join(rot, relativ)
if not os.path.isfile(full):
continue
try:
with open(full, "r", encoding="utf-8") as handle:
tekst = handle.read()
except UnicodeDecodeError:
continue
lest += 1
bytes_lest += len(tekst.encode("utf-8"))
for treff in HJEMMESTI.finditer(tekst):
funn.append((relativ, "absolutt hjemmesti", treff.group(0)))
if relativ in URL_UNNTAK:
continue
for vert, _sti in ADRESSE.findall(tekst):
if vert in INFRASTRUKTUR:
continue
if vert.endswith(".example"):
continue
funn.append((relativ, "adresse utenfor .example", vert))
return funn, lest, bytes_lest
def test_the_readme_install_block_covers_both_surfaces():
with open(README, "r", encoding="utf-8") as handle:
tekst = handle.read()
assert "## Install" in tekst, "the README has no install section"
installer = tekst.split("## Install", 1)[1]
installer = installer.split("\n## ", 1)[0]
assert "Claude Code" in installer, (
"the install block does not name Claude Code, the development surface"
)
assert "Cowork" in installer, (
"the install block does not name Cowork, which installs by uploading an "
"archive and not by a marketplace command -- a reader following the "
"Claude Code lines there gets nowhere"
)
assert "scripts/package_plugin.py" in installer, (
"the Cowork route needs the packaging command; never a recursive zip "
"of the repository root"
)
assert "scripts/bootstrap.py" in installer, (
"without the bootstrap there is no environment for the host MCP server "
"to run on in an installed copy"
)
# The honesty clause: the guard is installed from a self-hosted host a
# third party may not reach. A public install block that fails at its first
# step for everyone but its author has to say so.
assert "git.fromaitochitta.com" in tekst
assert "## Non-goals" in tekst, "the README states no non-goals"
def test_security_md_states_the_trust_model():
assert os.path.isfile(SECURITY), "SECURITY.md does not exist"
with open(SECURITY, "r", encoding="utf-8") as handle:
tekst = handle.read()
lav = tekst.lower()
for begrep in IKKE_FORHANDLINGSBART:
assert begrep in lav, (
"SECURITY.md never mentions %r; it is one of the four invariants "
"that are live at M1" % begrep
)
# The trust boundary is the write, not the read. Saying only "we use a
# guard" leaves the reader guessing where it sits.
assert "guard_ingest.py" in tekst
assert "M3" in tekst or "M6" in tekst, (
"SECURITY.md must say which surfaces are not live yet, or it reads as "
"a description of a finished system"
)
def test_no_tracked_file_carries_a_home_path_or_a_real_address(tmp_path):
# Prove the scan can find before believing that it found nothing.
#
# The canary is assembled from parts rather than written out, and that is
# not fussiness: this file is tracked, so the scan reads it too. A literal
# canary here would be a finding in the scanner's own source -- which is
# exactly what the M1 gate caught the first time this file was committed.
# Building it at runtime keeps the scanner subject to its own rule instead
# of exempting itself from it.
kanari = tmp_path / "kanari.md"
kanari.write_text(
"Se /%s/enperson/hemmelig/mappe/ og https://ekte-%s.no/annonse/12345\n"
% ("Users", "jobbportal"),
encoding="utf-8",
)
kanarifunn, kanarilest, _ = skann(str(tmp_path), ["kanari.md"])
assert kanarilest == 1
assert len(kanarifunn) == 2, (
"the scan cannot find what it exists to find; it reported %r"
% (kanarifunn,)
)
filer = sporede_filer()
funn, lest, bytes_lest = skann(REPO, filer)
warnings.warn(
"public surface scan: %d tracked files, %d bytes read; %d finding(s)"
% (lest, bytes_lest, len(funn)),
OffentligFlateSkannet,
)
assert lest > 0, "the scan read no files; git ls-files returned %d paths" % len(filer)
assert funn == [], (
"tracked files carry paths or addresses that must not be published:\n%s"
% "\n".join(" %s: %s -- %s" % rad for rad in funn)
)