82 lines
4.3 KiB
Markdown
82 lines
4.3 KiB
Markdown
# 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.
|