docs(m1): add security model and complete the readme first screen
This commit is contained in:
parent
0e3295f52b
commit
a9153e043e
5 changed files with 338 additions and 8 deletions
82
SECURITY.md
Normal file
82
SECURITY.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue