mooring 0.3.2__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. {mooring-0.3.2 → mooring-0.3.4}/PKG-INFO +3 -1
  2. {mooring-0.3.2 → mooring-0.3.4}/docs/admins/ai-privacy.md +151 -9
  3. {mooring-0.3.2 → mooring-0.3.4}/docs/admins/configuration.md +26 -0
  4. {mooring-0.3.2 → mooring-0.3.4}/docs/users/ai-copilot.md +11 -0
  5. {mooring-0.3.2 → mooring-0.3.4}/docs/users/cli.md +20 -0
  6. {mooring-0.3.2 → mooring-0.3.4}/pyproject.toml +8 -1
  7. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/__init__.py +1 -1
  8. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/base.py +6 -0
  9. mooring-0.3.4/src/mooring/ai/chat.py +391 -0
  10. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/context.py +42 -16
  11. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/copilot.py +12 -0
  12. mooring-0.3.4/src/mooring/ai/ner.py +324 -0
  13. mooring-0.3.4/src/mooring/ai/pii.py +316 -0
  14. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/session.py +34 -1
  15. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/tools.py +15 -2
  16. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/cli.py +273 -1
  17. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/config.py +46 -0
  18. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/config_default.toml +39 -2
  19. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/config_store.py +70 -0
  20. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/hub/server.py +130 -22
  21. mooring-0.3.4/src/mooring/hub/static/chat.html +50 -0
  22. mooring-0.3.4/src/mooring/hub/static/chat.js +995 -0
  23. mooring-0.3.4/src/mooring/hub/static/chat_core.js +177 -0
  24. mooring-0.3.4/src/mooring/hub/static/style.css +427 -0
  25. mooring-0.3.4/tests/js/chat_core.test.js +140 -0
  26. {mooring-0.3.2 → mooring-0.3.4}/tests/test_ai_tools.py +22 -0
  27. {mooring-0.3.2 → mooring-0.3.4}/tests/test_chat_session.py +52 -0
  28. {mooring-0.3.2 → mooring-0.3.4}/tests/test_config_store.py +70 -0
  29. {mooring-0.3.2 → mooring-0.3.4}/tests/test_hub.py +47 -0
  30. mooring-0.3.4/tests/test_ner.py +132 -0
  31. mooring-0.3.4/tests/test_pii.py +543 -0
  32. {mooring-0.3.2 → mooring-0.3.4}/uv.lock +813 -2
  33. mooring-0.3.2/src/mooring/ai/chat.py +0 -153
  34. mooring-0.3.2/src/mooring/hub/static/chat.html +0 -51
  35. mooring-0.3.2/src/mooring/hub/static/chat.js +0 -414
  36. mooring-0.3.2/src/mooring/hub/static/style.css +0 -272
  37. {mooring-0.3.2 → mooring-0.3.4}/.github/workflows/docs.yml +0 -0
  38. {mooring-0.3.2 → mooring-0.3.4}/.github/workflows/release.yml +0 -0
  39. {mooring-0.3.2 → mooring-0.3.4}/.gitignore +0 -0
  40. {mooring-0.3.2 → mooring-0.3.4}/README.md +0 -0
  41. {mooring-0.3.2 → mooring-0.3.4}/docs/admins/build-and-distribute.md +0 -0
  42. {mooring-0.3.2 → mooring-0.3.4}/docs/admins/github-setup.md +0 -0
  43. {mooring-0.3.2 → mooring-0.3.4}/docs/admins/index.md +0 -0
  44. {mooring-0.3.2 → mooring-0.3.4}/docs/assets/images/anchor-mark.svg +0 -0
  45. {mooring-0.3.2 → mooring-0.3.4}/docs/assets/images/favicon.svg +0 -0
  46. {mooring-0.3.2 → mooring-0.3.4}/docs/assets/javascripts/landing.js +0 -0
  47. {mooring-0.3.2 → mooring-0.3.4}/docs/assets/stylesheets/landing.css +0 -0
  48. {mooring-0.3.2 → mooring-0.3.4}/docs/assets/stylesheets/oah-theme.css +0 -0
  49. {mooring-0.3.2 → mooring-0.3.4}/docs/developers/contributing.md +0 -0
  50. {mooring-0.3.2 → mooring-0.3.4}/docs/developers/index.md +0 -0
  51. {mooring-0.3.2 → mooring-0.3.4}/docs/index.md +0 -0
  52. {mooring-0.3.2 → mooring-0.3.4}/docs/users/conflicts.md +0 -0
  53. {mooring-0.3.2 → mooring-0.3.4}/docs/users/daily-workflow.md +0 -0
  54. {mooring-0.3.2 → mooring-0.3.4}/docs/users/index.md +0 -0
  55. {mooring-0.3.2 → mooring-0.3.4}/docs/users/power-bi.md +0 -0
  56. {mooring-0.3.2 → mooring-0.3.4}/overrides/home.html +0 -0
  57. {mooring-0.3.2 → mooring-0.3.4}/scripts/release.ps1 +0 -0
  58. {mooring-0.3.2 → mooring-0.3.4}/scripts/spike_copilot_chat.py +0 -0
  59. {mooring-0.3.2 → mooring-0.3.4}/scripts/spike_live_introspect.py +0 -0
  60. {mooring-0.3.2 → mooring-0.3.4}/scripts/spike_marimo_filewatch.py +0 -0
  61. {mooring-0.3.2 → mooring-0.3.4}/scripts/spike_marimo_http_control.py +0 -0
  62. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/__init__.py +0 -0
  63. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/cellwrite.py +0 -0
  64. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/codeblocks.py +0 -0
  65. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/datadictionary.py +0 -0
  66. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/introspect.py +0 -0
  67. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/locality.py +0 -0
  68. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/prompt.py +0 -0
  69. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/ai/secrets.py +0 -0
  70. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/auth.py +0 -0
  71. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/deletion.py +0 -0
  72. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/editor.py +0 -0
  73. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/githost.py +0 -0
  74. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/github.py +0 -0
  75. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/gitsha.py +0 -0
  76. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/hub/__init__.py +0 -0
  77. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/hub/static/app.js +0 -0
  78. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/hub/static/index.html +0 -0
  79. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/manifest.py +0 -0
  80. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/notebook_template.py +0 -0
  81. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/paths.py +0 -0
  82. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/pbip.py +0 -0
  83. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/pyproject_env.py +0 -0
  84. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/schema.py +0 -0
  85. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/sync.py +0 -0
  86. {mooring-0.3.2 → mooring-0.3.4}/src/mooring/telemetry.py +0 -0
  87. {mooring-0.3.2 → mooring-0.3.4}/tests/conftest.py +0 -0
  88. {mooring-0.3.2 → mooring-0.3.4}/tests/manual_editor_check.py +0 -0
  89. {mooring-0.3.2 → mooring-0.3.4}/tests/test_ai_dict_tools.py +0 -0
  90. {mooring-0.3.2 → mooring-0.3.4}/tests/test_auth.py +0 -0
  91. {mooring-0.3.2 → mooring-0.3.4}/tests/test_cellwrite.py +0 -0
  92. {mooring-0.3.2 → mooring-0.3.4}/tests/test_chat_context.py +0 -0
  93. {mooring-0.3.2 → mooring-0.3.4}/tests/test_cli_delete.py +0 -0
  94. {mooring-0.3.2 → mooring-0.3.4}/tests/test_cli_deps.py +0 -0
  95. {mooring-0.3.2 → mooring-0.3.4}/tests/test_cli_repo.py +0 -0
  96. {mooring-0.3.2 → mooring-0.3.4}/tests/test_codeblocks.py +0 -0
  97. {mooring-0.3.2 → mooring-0.3.4}/tests/test_config.py +0 -0
  98. {mooring-0.3.2 → mooring-0.3.4}/tests/test_context.py +0 -0
  99. {mooring-0.3.2 → mooring-0.3.4}/tests/test_datadictionary.py +0 -0
  100. {mooring-0.3.2 → mooring-0.3.4}/tests/test_deletion.py +0 -0
  101. {mooring-0.3.2 → mooring-0.3.4}/tests/test_editor.py +0 -0
  102. {mooring-0.3.2 → mooring-0.3.4}/tests/test_editor_marimo_ai.py +0 -0
  103. {mooring-0.3.2 → mooring-0.3.4}/tests/test_githost.py +0 -0
  104. {mooring-0.3.2 → mooring-0.3.4}/tests/test_github.py +0 -0
  105. {mooring-0.3.2 → mooring-0.3.4}/tests/test_gitsha.py +0 -0
  106. {mooring-0.3.2 → mooring-0.3.4}/tests/test_introspect.py +0 -0
  107. {mooring-0.3.2 → mooring-0.3.4}/tests/test_locality.py +0 -0
  108. {mooring-0.3.2 → mooring-0.3.4}/tests/test_manifest.py +0 -0
  109. {mooring-0.3.2 → mooring-0.3.4}/tests/test_paths.py +0 -0
  110. {mooring-0.3.2 → mooring-0.3.4}/tests/test_pbip.py +0 -0
  111. {mooring-0.3.2 → mooring-0.3.4}/tests/test_pyproject_env.py +0 -0
  112. {mooring-0.3.2 → mooring-0.3.4}/tests/test_schema.py +0 -0
  113. {mooring-0.3.2 → mooring-0.3.4}/tests/test_secrets.py +0 -0
  114. {mooring-0.3.2 → mooring-0.3.4}/tests/test_sync.py +0 -0
  115. {mooring-0.3.2 → mooring-0.3.4}/tests/test_telemetry.py +0 -0
  116. {mooring-0.3.2 → mooring-0.3.4}/tests/test_truststore.py +0 -0
  117. {mooring-0.3.2 → mooring-0.3.4}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mooring
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Git-free marimo notebook sharing via GitHub
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: keyring
@@ -14,6 +14,8 @@ Requires-Dist: truststore
14
14
  Requires-Dist: uvicorn
15
15
  Provides-Extra: copilot
16
16
  Requires-Dist: github-copilot-sdk>=1.0.1; extra == 'copilot'
17
+ Provides-Extra: pii
18
+ Requires-Dist: gliner>=0.2.13; extra == 'pii'
17
19
  Description-Content-Type: text/markdown
18
20
 
19
21
  # ⚓ mooring
@@ -39,11 +39,15 @@ sees whatever your team wrote into those files.
39
39
 
40
40
  ## The four structural guarantees
41
41
 
42
- 1. **Single choke point.** The context handed to the model is assembled in one
43
- place (`ai/chat.py:build_system_context`) — from the schema text and the
44
- notebook source, plus (only when team context is enabled) the team instructions
45
- and the value-minimised data-dictionary slice. Nothing reaches the model except
46
- through this one function.
42
+ 1. **Single choke point for the system context.** The context handed to the model
43
+ is assembled in one place (`ai/chat.py:build_system_context`) — from the schema
44
+ text and the notebook source, plus (only when team context is enabled) the team
45
+ instructions and the value-minimised data-dictionary slice. Two further egresses
46
+ exist by design and are value-free by construction: your **chat turns**, and the
47
+ agent's **tool reads** (it can re-read the notebook source via
48
+ `mooring_read_notebook_source`, and fetch dataset schemas). The opt-in
49
+ [structured-PII scan](#structured-pii-pre-flight-scan-opt-in-best-effort) runs at
50
+ all of these, not only `build_system_context`.
47
51
  2. **Value-free tools only.** The agent is given mooring's own tools (`ai/tools.py`):
48
52
  list datasets, get a schema, read the notebook source, and *propose* a cell —
49
53
  each value-free by construction. When a data dictionary is configured, three more
@@ -81,9 +85,11 @@ data often lives **outside** it — a network share, a warehouse export, a datab
81
85
  connection, a path built at runtime — and the schema most useful for writing code
82
86
  is frequently a *derived* frame (a join/filter result) that no file holds. To help
83
87
  there, mooring can read the schema of the dataframes **already loaded in your
84
- running kernel**. It is **on by default** and value-free, but like team context
85
- its safety comes from *how it is built*, not from physical impossibility, so it is
86
- documented here in full. Turn it off with `[ai] live_schema = false`.
88
+ running kernel**. It is **on by default**, refreshed on every chat turn (so a frame
89
+ you load after opening the chat is picked up without reopening), and value-free — but,
90
+ like team context, its safety comes from *how it is built*, not from physical
91
+ impossibility, so it is documented here in full. Turn it off with
92
+ `[ai] live_schema = false`.
87
93
 
88
94
  How it stays value-blind (`ai/introspect.py`):
89
95
 
@@ -100,6 +106,11 @@ How it stays value-blind (`ai/introspect.py`):
100
106
  - **Fail-closed on the way back.** The reader (`_parse_frames`) accepts only the
101
107
  `{name, columns: [[str, str]], n_rows: int}` shape and drops everything else, so a
102
108
  value can't ride back on a key mooring doesn't read.
109
+ - **The per-turn refresh adds no new value channel.** The schema is captured at
110
+ chat-open *and* re-probed on each turn through the **same** frozen probe and
111
+ fail-closed reader; a turn re-states the schema only when the kernel changed, and an
112
+ unchanged kernel is not re-sent. The refresh re-states already-value-free schema —
113
+ it opens no path a value could take that the open-time capture did not.
103
114
 
104
115
  Honest caveat: unlike `schema.py` (which physically only ever reads a file header),
105
116
  this probe runs in a namespace that *contains* values. Its value-blindness is the
@@ -145,13 +156,139 @@ Run `mooring ai dictionary check` to see exactly how your files parse — which
145
156
  was detected, how many tables/columns were kept, which keys were dropped, and any
146
157
  secret-scan findings — *before* enabling the feature or sharing the files.
147
158
 
159
+ ## Structured-PII pre-flight scan (opt-in, best-effort)
160
+
161
+ The guarantees above stop the *data* from reaching the model. They cannot stop a
162
+ human from **typing a real value** into a cell or the chat —
163
+ `df.filter(pl.col("pan") == "4012 8888 8888 1881")`, or "why does account
164
+ 4012888888881881 fail?". As a thin extra floor, mooring can scan text on its way
165
+ out for **well-formed structured identifiers** and warn before it leaves. It is
166
+ **off by default** (`[ai.pii] enabled = false`) and, like team context, its safety
167
+ is best-effort, not structural.
168
+
169
+ **What it catches** (precision over recall): checksum-validated **payment cards**
170
+ (Luhn), **IBANs** (mod-97), and **NHS numbers** (mod-11), plus shape-anchored
171
+ **emails** and **UK NINOs**. **What it does not catch, by design:** addresses,
172
+ account narratives, **UK sort codes**, **bank account numbers**, US SSNs, phone
173
+ numbers, dates of birth, IP addresses, or any value split across two messages.
174
+ Person **names** are out of reach of the structured scan too, but can be caught by
175
+ the optional local-NER pass below. **A clean scan is not a value-free guarantee** —
176
+ it is a safety net for the obvious, well-formed cases, and it complements (never
177
+ replaces) the structural value-blindness above.
178
+
179
+ It runs at every egress, and every finding is value-free — a line number and a
180
+ *kind* (`payment card`, `email address`, …), never the matched value:
181
+
182
+ - **Your chat prompt.** With `block_prompt = true` (the default once the feature is
183
+ on), a prompt that looks like it contains a card/IBAN/NHS/email/NINO is **held**;
184
+ you see which kinds tripped it and must click **"Send anyway"** — nothing reaches
185
+ the model until you confirm. (Set `block_prompt = false` for a warn-only advisory.)
186
+ - **The notebook source and its schema.** On opening the copilot you get a one-time,
187
+ value-free banner if the notebook or a dataset schema looks like it contains PII.
188
+ The source is never rewritten (that would break your code). But a schema **column
189
+ name** that is itself a value — the result of a pivot/transpose on a PII key, e.g.
190
+ `df.pivot(on="customer_pan")` — is **withheld** from the schema the model sees.
191
+ - **Team context.** An `instructions.md` carrying a checksum-validated card/IBAN/NHS
192
+ (or a secret) is withheld entirely; a stray email/NINO drops just that line; a
193
+ data-dictionary description that trips the scan is dropped.
194
+
195
+ Run **`mooring ai pii check`** to scan your `context/` files and notebook sources
196
+ **offline** (no Copilot, no network) before enabling the feature — it prints
197
+ `path:line kind` for each finding and never echoes a value. Put `# mooring: pii-ok`
198
+ on a line to retire a reviewed false positive.
199
+
200
+ Configure it under `[ai.pii]`: `enabled` (master switch), `block_prompt`
201
+ (hold-and-confirm vs. a warn-only advisory on the chat prompt), and
202
+ `scan_notebook_source` (the source/schema banner).
203
+
204
+ ## Name detection (opt-in, local NER)
205
+
206
+ A person's name — `where name == "Jane Smith"` — has no checksum or fixed shape, so
207
+ the structured scan above cannot see it. The optional **name pass** (`ai/ner.py`)
208
+ closes that gap with a **local** zero-shot NER model ([GLiNER](https://github.com/urchade/GLiNER)),
209
+ shipped as the `mooring[pii]` extra so the lean install and the frozen `.pyz` stay
210
+ free of the heavy ML stack (torch + transformers). It is **off by default**, only
211
+ acts when `[ai.pii] enabled` is also true, and is **best-effort** (NER both misses
212
+ and false-positives — a clean scan is not proof of no names).
213
+
214
+ Its privacy properties match the structured scan:
215
+
216
+ - **Local only.** The model runs on the analyst's machine; the text is never sent
217
+ anywhere to be scanned. The single network touch is a **one-time model download**
218
+ from Hugging Face on first use — pre-fetch it on a managed/offline network with
219
+ **`mooring ai pii model`**.
220
+ - **Value-free findings.** GLiNER returns the matched name; mooring reads **only**
221
+ the label and character offset, maps it to a line number, and **drops the text**.
222
+ A finding is `(line, "person name")` — never the name — so it logs and streams
223
+ over SSE as safely as the structured kinds. Pinned by `tests/test_ner.py`.
224
+ - **No pickle, pinned.** The default model (`gliner-community/gliner_small-v2.5`) is
225
+ loaded as its **safetensors** `bf16` variant — `mooring ai pii model` fetches *only*
226
+ the safetensors file, never the repo's `pytorch_model.bin`, so nothing is unpickled.
227
+ It is **pinned to a specific commit** (`name_model_revision`) for reproducibility and
228
+ so a security review is against a fixed artifact.
229
+ - **Same egress + UI.** A flagged chat prompt is held with the same "Send anyway"
230
+ confirm; `mooring ai pii check` runs the name pass too (when the model is already
231
+ cached) for the offline lint. At the chat prompt, a configured-but-uninstalled extra
232
+ **fails loud** (a `scan_error` advisory) rather than silently doing nothing; while the
233
+ model is still downloading the name pass is skipped (the message is still structurally
234
+ scanned) and the chat shows a "preparing model" status.
235
+
236
+ Configure under `[ai.pii]`: `detect_names` (on/off), `name_model` / `name_model_revision`
237
+ / `name_model_variant` (which model, pinned commit, and safetensors variant —
238
+ `name_model_variant = ""` loads a repo's default weights file for a model that has no
239
+ variant safetensors), `name_labels` (entity labels to flag), and `name_threshold`
240
+ (confidence cut-off; raise for fewer, safer hits). GLiNER is zero-shot, so `name_labels`
241
+ is not limited to people — add `"organization"` to also flag **business names** (surfaced
242
+ as an `organization` finding); other entity types (e.g. `"address"`) work the same way.
243
+ Capitalised non-person terms make organisation detection more false-positive-prone, so it
244
+ stays out of the default. Install and enable:
245
+
246
+ ```toml
247
+ [ai.pii]
248
+ enabled = true
249
+ detect_names = true
250
+ ```
251
+ ```
252
+ pip install mooring[pii] # or: uvx mooring[pii] (combine extras: mooring[copilot,pii])
253
+ mooring ai pii model # pre-download the model (recommended)
254
+ ```
255
+
256
+ ## Deploying name detection in an institutional / offline environment
257
+
258
+ The model download is the only part of mooring that reaches a non-GitHub host
259
+ (Hugging Face). In a locked-down environment, plan for:
260
+
261
+ - **Firewall allow-list.** Outbound HTTPS is needed to `huggingface.co` **and** the
262
+ file backends — the LFS CDN and the newer **Xet** hosts (`cas-bridge.xethub.hf.co`,
263
+ `*.xethub.hf.co`). Allow-listing only `huggingface.co` passes the metadata fetch and
264
+ then fails on the actual download.
265
+ - **TLS / SSL-intercepting proxy.** mooring enables the **OS trust store** globally
266
+ (`truststore`), so Hugging Face traffic honours your proxy's root CA automatically,
267
+ the same way GitHub traffic does — no separate CA bundle needed in the normal case.
268
+ `REQUESTS_CA_BUNDLE` / `SSL_CERT_FILE` still take precedence if you set them.
269
+ - **Proxy / rate limits.** `HTTPS_PROXY` / `NO_PROXY` are honoured; set an `HF_TOKEN`
270
+ to lift the anonymous-download rate limit (faster, fewer throttles).
271
+ - **Air-gapped (no egress).** Either point at an internal mirror with
272
+ `HF_ENDPOINT=https://<your-hf-proxy>` (e.g. Artifactory/Nexus), **or** provision the
273
+ cache out-of-band: run `mooring ai pii model` on a connected machine, copy
274
+ `~/.cache/huggingface` (or set a shared `HF_HOME`) to the target machines, and set
275
+ `HF_HUB_OFFLINE=1`. Relocate the cache with `HF_HOME` if the user profile is small
276
+ or roaming.
277
+ - **Model governance.** The weights are a third-party artifact. The pinned default is
278
+ safetensors (no code-execution-on-load risk that a pickle `pytorch_model.bin` carries),
279
+ loaded locally; review the pinned `name_model` + `name_model_revision` through your
280
+ model-risk process, and re-pin a new revision only after review.
281
+
148
282
  ## The one thing to watch
149
283
 
150
284
  Anything **you type into a cell or the chat** is, by definition, visible to the
151
285
  assistant. If you hard-code a real value into a cell —
152
286
  `df.filter(pl.col("ssn") == "123-45-6789")` — that literal is part of the source
153
287
  the assistant can read. The chat reminds you of this; **never paste real values**.
154
- A future release will add an automatic PII/redaction guard on outbound text.
288
+ The opt-in [structured-PII scan](#structured-pii-pre-flight-scan-opt-in-best-effort)
289
+ above catches *well-formed* cards/IBANs/NHS numbers/emails/NINOs as a safety net,
290
+ but it cannot catch a name, a sort code, an account number, or a value typed into
291
+ prose — so the rule stands regardless.
155
292
 
156
293
  ## Verifying it yourself
157
294
 
@@ -177,3 +314,8 @@ The copilot needs the optional extra (`pip install mooring[copilot]` /
177
314
  `uvx mooring[copilot]`), a GitHub Copilot licence (`mooring ai login`), and your
178
315
  organisation's Copilot **CLI/agent policy** enabled. See
179
316
  [Configuration](configuration.md) for the `[ai]` settings.
317
+
318
+ Optional **name detection** (the structured-PII guard's NER pass) needs the
319
+ separate `mooring[pii]` extra (`pip install mooring[pii]`); without it the guard
320
+ still does its stdlib structured-PII scan. See
321
+ [Name detection](#name-detection-opt-in-local-ner).
@@ -189,6 +189,32 @@ a fallback when no OS credential store is available; normally it's in the
189
189
  credential store instead. Run `selftest` to print the exact paths on a given
190
190
  machine.
191
191
 
192
+ ## Editing the user config from the command line
193
+
194
+ Rather than hand-editing the file, `mooring config` reads and writes it by
195
+ **dotted key** (`section.subsection.key`). It touches only the user file and
196
+ leaves every other setting in place:
197
+
198
+ ```
199
+ mooring config set ai.pii.enabled true # bool
200
+ mooring config set ai.pii.name_threshold 0.6 # number
201
+ mooring config set ai.pii.name_labels person name organization # several tokens = a list
202
+ mooring config get ai.pii.enabled # print the effective value
203
+ mooring config unset ai.pii.enabled # remove the key (revert to the default)
204
+ mooring config list # print the whole effective config
205
+ mooring config path # print the config.toml location
206
+ ```
207
+
208
+ Value typing is automatic: `true`/`false` become booleans, `5`/`0.6` become
209
+ numbers, **several tokens become a string list**, and anything else (a path or
210
+ model id like `urchade/gliner_multi_pii-v1`) stays a string. To force a string
211
+ that looks numeric, quote it as a TOML literal, e.g. `set some.key '"123"'`.
212
+
213
+ `get` and `list` show the **effective** value (packaged default merged with your
214
+ file); they do **not** reflect a one-run [environment-variable](#environment-variables)
215
+ override. This works for any key, including the `[ai]` / `[ai.pii]` settings
216
+ documented in [AI privacy](ai-privacy.md).
217
+
192
218
  ## The runtime setup form
193
219
 
194
220
  If a build ships **without** `client_id` / `owner` / `repo`, the hub shows a
@@ -41,6 +41,13 @@ Keep both tabs side by side: chat on one, the marimo notebook on the other.
41
41
  Anything you type into a cell or the chat is visible to the assistant. Refer
42
42
  to columns by name — don't paste actual data values.
43
43
 
44
+ If your admin has turned on the **structured-PII scan**, a message that looks
45
+ like it contains a payment card, IBAN, NHS number, email, or UK NINO is **held**
46
+ and you'll see a **"Send anyway"** prompt — nothing is sent until you confirm.
47
+ It's a best-effort safety net: it won't catch names, sort codes, or account
48
+ numbers, so the rule still stands. See
49
+ [the privacy page](../admins/ai-privacy.md#structured-pii-pre-flight-scan-opt-in-best-effort).
50
+
44
51
  ## Team context (optional)
45
52
 
46
53
  If your admin enables it (`[ai] context = true`), the copilot also reads a
@@ -72,4 +79,8 @@ your files parse and to catch anything sensitive *before* you share them.
72
79
  choice is remembered. (Some models — and "Auto" — have no effort setting.)
73
80
  - The assistant can read your notebook's current code and ask for a dataset's
74
81
  schema on its own; you don't need to paste either in.
82
+ - It also sees the schema of dataframes already loaded in the running notebook —
83
+ and this is **refreshed every time you send a message**, so if you load a new
84
+ dataframe mid-chat, just ask your next question; there's no need to reopen the
85
+ chat. (It sees column names and types only, never your data values.)
75
86
  - It writes Polars (`pl`) by default, matching mooring's bundled notebook stack.
@@ -36,6 +36,8 @@ python mooring.pyz deps remove polars
36
36
  python mooring.pyz deps list
37
37
  python mooring.pyz deps lock
38
38
  python mooring.pyz build-requirements [-o FILE]
39
+ python mooring.pyz config set <key> <value...> | config get <key>
40
+ python mooring.pyz config unset <key> | config list | config path
39
41
  python mooring.pyz selftest
40
42
  python mooring.pyz version
41
43
  ```
@@ -178,6 +180,24 @@ afterwards to remove it from the team repo for everyone; a notebook you never
178
180
  shared just disappears. You are asked to confirm first — pass `-y`/`--yes` to
179
181
  skip the prompt (required when running non-interactively, e.g. from a script).
180
182
 
183
+ ### `config`
184
+
185
+ Read and edit your user `config.toml` by **dotted key**, without hand-editing the
186
+ file (every other setting is preserved):
187
+
188
+ ```
189
+ python mooring.pyz config set ai.pii.enabled true
190
+ python mooring.pyz config set ai.pii.name_labels person name organization
191
+ python mooring.pyz config get ai.pii.enabled # effective value
192
+ python mooring.pyz config unset ai.pii.enabled # revert to the default
193
+ python mooring.pyz config list # whole effective config
194
+ python mooring.pyz config path # config.toml location
195
+ ```
196
+
197
+ `true`/`false` become booleans and numbers are parsed; several tokens become a
198
+ list; anything else stays a string. See
199
+ [Configuration → editing from the command line](../admins/configuration.md#editing-the-user-config-from-the-command-line).
200
+
181
201
  ### `selftest`
182
202
 
183
203
  Verify the bundled environment and print a diagnostic snapshot of the machine:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mooring"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "Git-free marimo notebook sharing via GitHub"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -28,6 +28,13 @@ dependencies = [
28
28
  copilot = [
29
29
  "github-copilot-sdk>=1.0.1",
30
30
  ]
31
+ # Optional NER name detection for the outbound-PII guard (Phase 2). GLiNER pulls
32
+ # torch + transformers (hundreds of MB), so it ships as an extra — without it the
33
+ # guard still does its stdlib structured-PII scan. Install with
34
+ # `pip install mooring[pii]`. Pre-fetch the model with `mooring ai pii model`.
35
+ pii = [
36
+ "gliner>=0.2.13",
37
+ ]
31
38
 
32
39
  [project.scripts]
33
40
  mooring = "mooring.cli:main"
@@ -1,3 +1,3 @@
1
1
  """Mooring: git-free marimo notebook sharing via GitHub."""
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.3.4"
@@ -65,6 +65,12 @@ class AIProvider(Protocol):
65
65
  model: str | None = None,
66
66
  reasoning_effort: str | None = None,
67
67
  dictionary=None,
68
+ pii_enabled: bool = False,
69
+ pii_block: bool = True,
70
+ pii_names: bool = False,
71
+ pii_name_labels: tuple[str, ...] | None = None,
72
+ pii_name_threshold: float = 0.7,
73
+ pii_name_model: str | None = None,
68
74
  ):
69
75
  """Open a long-lived, streaming chat session (a ``ChatBroadcaster``).
70
76