jobbsok/SECURITY.md

4.3 KiB

Security model

This plugin runs on the operator's own machine, reads job listings written by strangers, and holds a job search that is nobody else's business. The design follows from those three facts. This document states what the plugin will not do, where the boundaries actually sit, and which risks the design accepts rather than solves.

It is written for M1. Sections marked not live yet describe a surface that lands at M3 or M6; they are here because the boundary is designed now and building against an undocumented boundary is how one gets moved.

The four non-negotiables

Read-only against external sites. The plugin never submits a form, never sends a message, never applies for anything. It attaches to a browser the operator already opened and reads what is on the screen. At M3 that is enforced by a deny list and a PreToolUse hook, not by convention alone (not live yet).

No credential handling. The plugin stores no password, no token and no cookie. The browser it reads through is authenticated because the operator signed in by hand, in their own browser session. Mail access, if it is ever configured, is read-only IMAP whose credentials come from environment variables the plugin never writes and never logs.

All data is local. The workspace lives outside this repository, is never committed, and is never transmitted anywhere. There is no telemetry, no analytics, no crash reporting and no phone-home. Nothing runs on a schedule: the plugin does something when the operator asks it to and at no other time.

Untrusted content stays untrusted. A job listing and an email body are input written by someone else, and both routinely contain text shaped like an instruction. Every such body passes scripts/guard_ingest.py, which wraps a pinned release of the ingestion guard, before it is written to the workspace. The trust boundary is the write, not the read: content that has not passed the guard may be looked at and reasoned about, but it does not become a file. (The guard pipeline lands at M3.)

Where the boundaries sit

Boundary Enforced by Live at
Workspace containment jobbsok_lib.paths.safe_join, which resolves symlinks before comparing M1
Explicit workspace, never a guessed one jobbsok_lib.paths.workspace_root; every MCP tool requires it M1
Interpreter floor scripts/jobbsok_tools_launch.sh refuses anything below Python 3.10 M1
Build identity BUILD_STAMP, so a cached upload cannot pass for a fresh one M1
Ingestion scripts/guard_ingest.py at the point of the write M3
Browser read-only deny list plus a PreToolUse hook M3
Profile mutation proposed as a diff, applied only by a human M6

Accepted risks

The host MCP server is unsandboxed. jobbsok-tools runs as the operator, with the operator's file system. That is what makes it useful and it is not mitigated away. What is mitigated is the path: every tool takes an explicit workspace, and every path is resolved and checked against that root before it is opened, so a caller-supplied path cannot walk out of the workspace by traversal or through a symlink.

Decision logs are append-only and are never pruned automatically. beslutninger.jsonl accumulates. A correction is a new entry, never an edit, because a log that can be rewritten is not evidence of anything. Retention is proposed to the operator at M6 and applied only on approval.

The ingestion guard is installed from a self-hosted host. If that host is unreachable, the guarded ingestion path cannot be built. The pin is an exact tag and is never substituted for a different one, and the source is never vendored into this repository.

This repository has a public remote. Everything committed here is published. tests/test_public_surface.py scans every tracked file on every run for absolute home paths and for addresses outside .example, and tests/test_fixture_hygiene.py scans the fixture corpus for operator content. Both report the number of files they read, because a scan that says it found nothing without saying what it looked at has said nothing.

Reporting

This is a solo-maintained, fork-and-own repository. Issues are welcome as signals; pull requests are not accepted. For anything sensitive, open an issue saying only that you have something to report, without the details.