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>
203 lines
8.3 KiB
Python
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)
|
|
)
|