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