# 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.py` 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.