Add Write Safety wiki page
1 parent
c64158da94
commit
f3c96e25ed
1 file changed
+95
+95
@@ -0,0 +1,95 @@
|
||||
# Write Safety
|
||||
|
||||
familiar began "read-only by design" and is now **read + write** — but writing is gated behind a single, deliberately strict safety layer. This page explains exactly when a write happens and what stops it.
|
||||
|
||||
## The three conditions
|
||||
|
||||
A write only happens when **all** of these hold:
|
||||
|
||||
1. The tool is called with **`confirm=true`** (otherwise it returns a dry-run preview and changes nothing).
|
||||
2. Writes are **enabled globally** (on by default; opt out with `familiar writes disable`).
|
||||
3. The action runs through **`writes.guard`** — the single choke point every mutating tool must use.
|
||||
|
||||
Every executed write is appended to an **audit log** on disk.
|
||||
|
||||
## The gate: `writes.guard`
|
||||
|
||||
[`writes.py`](https://git.hiddenden.cafe/Hiddenden/familiar/src/branch/main/src/familiar/writes.py) exposes one function that every write tool calls:
|
||||
|
||||
```python
|
||||
async def guard(action, params, confirm, execute):
|
||||
# confirm=False -> dry-run preview, execute nothing
|
||||
# confirm=True, writes off -> raise WriteDisabled
|
||||
# confirm=True, writes on -> execute(), append to audit log, return result
|
||||
```
|
||||
|
||||
### `confirm=false` → dry-run preview
|
||||
|
||||
The default. Nothing is changed. The tool returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"dry_run": true,
|
||||
"action": "set_waypoint",
|
||||
"params": { "...": "..." },
|
||||
"note": "Nothing was changed. Pass confirm=true to execute (writes must be enabled)."
|
||||
}
|
||||
```
|
||||
|
||||
This is how the AI can *show you what it would do* before you commit. You confirm by asking it to run the same action with `confirm=true`.
|
||||
|
||||
### `confirm=true` but writes disabled → `WriteDisabled`
|
||||
|
||||
If you've globally turned writes off, a confirmed write still does nothing and raises `WriteDisabled` telling you to run `familiar writes enable` to re-allow it.
|
||||
|
||||
### `confirm=true` and writes enabled → execute + audit
|
||||
|
||||
The action runs, and a line is appended to the audit log (see below).
|
||||
|
||||
## The global toggle
|
||||
|
||||
Writes are **on by default**. The toggle is controlled three ways, in precedence order:
|
||||
|
||||
1. **Environment variable** `FAMILIAR_ENABLE_WRITES` — if set, it wins (`1/true/yes/on` = enabled).
|
||||
2. **On-disk flag file** `writes_disabled` in the data dir — created/removed by the CLI.
|
||||
3. **Default** — enabled (no flag file present).
|
||||
|
||||
CLI:
|
||||
|
||||
```bash
|
||||
uv run familiar writes # show current state
|
||||
uv run familiar writes disable # create the flag → block all writes
|
||||
uv run familiar writes enable # remove the flag → re-allow writes
|
||||
```
|
||||
|
||||
The `write_status` MCP tool reports the current state to the AI without touching the toggle.
|
||||
|
||||
## The audit log
|
||||
|
||||
Each executed write appends one JSON line to `audit.log` in the data dir:
|
||||
|
||||
```json
|
||||
{"ts": 1719600000.0, "action": "send_mail", "params": { "...": "..." }}
|
||||
```
|
||||
|
||||
Dry-runs and blocked (disabled) writes are **not** logged — only real, executed writes are. The log lives alongside the encrypted token store (see **[Configuration](Configuration)** for the path).
|
||||
|
||||
## Which tools are writes?
|
||||
|
||||
Every tool marked **WRITE** in the **[Tool Reference](Tool-Reference)**:
|
||||
|
||||
- **Comms:** `send_mail`, `set_mail_read`, `respond_calendar_event`
|
||||
- **Contacts:** `add_contacts`, `remove_contacts`
|
||||
- **Fittings:** `create_fitting`, `delete_fitting`
|
||||
- **Fleet:** `invite_to_fleet`, `update_fleet`
|
||||
- **UI:** `set_waypoint`, `open_window`
|
||||
|
||||
These are powered by the OAuth **write scopes** granted at login (see **[Authentication & Scopes](Authentication-and-Scopes)**).
|
||||
|
||||
## For contributors
|
||||
|
||||
When adding a write tool:
|
||||
|
||||
- Give it a **required** `character` and a `confirm: bool = False` parameter.
|
||||
- Route the mutation through **`writes.guard`** — never call a mutating ESI endpoint directly.
|
||||
- Pass a clear `action` string and the user-facing `params` so the dry-run preview and audit line are meaningful.
|
||||
Reference in new issue
Block a user