feat(m2): expose status and daily tools through jobbsok-tools

This commit is contained in:
Kjell Tore Guttormsen 2026-09-05 22:09:17 +02:00
commit c7db9f16a0
4 changed files with 578 additions and 19 deletions

View file

@ -44,11 +44,31 @@ Two environment variables, and they are NOT the same one:
spelling problem above: a Windows adopter setting it
merely to say `python` would serve without the guard.
The gate is on the VERSION, not on the guard being importable. That is the
shell version's semantics carried over unchanged, and it is a known weakness
(O4 in docs/cowork-probe.md): `selvsjekk` can report `guard_versjon: null`
while the server runs happily. It is harmless while nothing writes and becomes
a defect in M2. Fixing it is an M2 decision, not a porting one.
Two gates, in this order (O4, decided in docs/cowork-probe.md and implemented
at plan Step 24):
1. VERSION. The interpreter reports at least 3.10, or it is not used.
2. THE GUARD. The interpreter can import `llm_ingestion_guard`, or the
launcher refuses to serve at all and says which command installs it.
Until M2 there was only the first, and the measured consequence is the reason
for the second: the M1 probe found the server running happily on the host's
3.14.0 with `selvsjekk` reporting `guard_versjon: null`, because nothing had
ever asked. That cost nothing while no tool wrote anything. From M2 the case
folder and the decision log persist employer names, titles, URLs and notes
that came out of a listing, and the guard is the boundary those writes are
supposed to cross.
Refusing is cheap here, and that is the deciding argument rather than a
consolation: every skill in this plugin degrades to manual paste with
`jobbsok-tools` absent. A launcher that refuses removes a connector; it does
not strand the operator. Failing open would persist untrusted text through the
one boundary this plugin exists to hold.
The guard gate costs one extra interpreter start-up at launch, which is paid
once per session against Cowork's MCP start-up timeout. It is a real cost and
it is worth naming; it is not a reason to check something cheaper that answers
a different question.
"""
import os
@ -118,6 +138,25 @@ def version_ok(tolk):
)
#: The ingestion guard, by the name the interpreter has to be able to import.
#: build-brief calls this the trust boundary, and the boundary is the write.
GUARD_MODUL = "llm_ingestion_guard"
#: What fixes a failed guard gate. Named in the refusal, because a refusal
#: that does not say what to run is a dead end.
BOOTSTRAP = "python scripts/bootstrap.py"
def guard_ok(tolk):
"""True when ``tolk`` can import the ingestion guard. Runs it; does not
look for a file, because what matters is that interpreter's own path."""
return 0 == subprocess.call(
[tolk, "-c", "import " + GUARD_MODUL],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def version_of(tolk):
"""The version ``tolk`` reports, or "ukjent" when it will not say."""
try:
@ -206,9 +245,11 @@ def resolve_interpreter(env, plugin_root, which=None, gate=None):
return None
def main(argv, env=None):
def main(argv, env=None, guard=None):
if env is None:
env = os.environ
if guard is None:
guard = guard_ok
plugin_root = env.get("CLAUDE_PLUGIN_ROOT") or os.path.dirname(SCRIPT_DIR)
server = os.path.join(plugin_root, "scripts", "jobbsok_tools.py")
@ -243,6 +284,23 @@ def main(argv, env=None):
sys.stderr.write("jobbsok-tools: server not found at %s\n" % server)
return 1
# Gate two. Last, because it is the expensive one and there is no point
# probing an interpreter that already failed the floor.
if not guard(tolk):
sys.stderr.write(
"jobbsok-tools: %s cannot import %s.\n" % (tolk, GUARD_MODUL)
)
sys.stderr.write(
"jobbsok-tools: run '%s' against the installed plugin; the guard "
"is installed with it.\n" % BOOTSTRAP
)
sys.stderr.write(
"jobbsok-tools: refusing to start rather than serving tools that "
"write without the ingestion guard. Every skill still works by "
"manual paste.\n"
)
return 1
# POSIX replaces this process, as the shell version's `exec` did: the MCP
# client's child stays the process it spawned, and killing it kills the
# server. Windows has no such replacement -- os.execv there starts a new