mostlyright-data 0.20.0__tar.gz → 0.20.2__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.
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/PKG-INFO +13 -1
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/README.md +12 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/pyproject.toml +1 -1
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/skills/mr-data-build/SKILL.md +23 -7
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/recipe.py +53 -45
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/commands/auth.py +83 -18
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/commands/login.py +5 -3
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/commands/whoami.py +1 -1
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/credential_native.py +30 -15
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/credential_store.py +1 -7
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/credentials.py +15 -15
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/login.py +15 -1
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/remediation.py +38 -2
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/.gitignore +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/scripts/hatch_build.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/skills/mr-data-build/agents/openai.yaml +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/__init__.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/canonical.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/formats.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/key_seam.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/page_coverage.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/session_probes.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/skill_assets.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/table_manifest.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/__init__.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/acquire.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/activity.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/approvals.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/categories.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/commands.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/download.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/narrative.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/parity.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/probe.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/propose.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/research.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/router.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/runs.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/session.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/stream.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/transport.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/__init__.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/attendance.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/clarification.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
- {mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mostlyright-data
|
|
3
|
-
Version: 0.20.
|
|
3
|
+
Version: 0.20.2
|
|
4
4
|
Summary: Mostly Right hosted CLI for reviewed datasets
|
|
5
5
|
Project-URL: Homepage, https://mostlyright.md/
|
|
6
6
|
Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
|
|
@@ -48,6 +48,18 @@ mr-data whoami
|
|
|
48
48
|
|
|
49
49
|
`login` stores a device key on the current machine. `whoami` checks that key.
|
|
50
50
|
|
|
51
|
+
New macOS/Linux logins use `~/.mostlyright/credentials.secure`, an unencrypted file restricted to
|
|
52
|
+
your OS account (0600 inside a 0700 directory). Processes running as you and backups can read it.
|
|
53
|
+
Older file-based logins migrate to this file without requesting Keychain access. Windows keeps
|
|
54
|
+
DPAPI encryption as its default.
|
|
55
|
+
Existing Keychain, DPAPI, or Secret Service logins keep their selected store; an unavailable store
|
|
56
|
+
never causes an automatic switch to file storage.
|
|
57
|
+
|
|
58
|
+
If an existing macOS login requests Keychain access, it is requesting the `md.mostlyright.cli`
|
|
59
|
+
device credential. To switch that login to file storage, restore access to its Keychain item,
|
|
60
|
+
run `mr-data auth logout` successfully, then run `mr-data login`. The `--credential-store secure-file`
|
|
61
|
+
option does not move an existing native-store login.
|
|
62
|
+
|
|
51
63
|
## Check the installation
|
|
52
64
|
|
|
53
65
|
```bash
|
|
@@ -36,6 +36,18 @@ mr-data whoami
|
|
|
36
36
|
|
|
37
37
|
`login` stores a device key on the current machine. `whoami` checks that key.
|
|
38
38
|
|
|
39
|
+
New macOS/Linux logins use `~/.mostlyright/credentials.secure`, an unencrypted file restricted to
|
|
40
|
+
your OS account (0600 inside a 0700 directory). Processes running as you and backups can read it.
|
|
41
|
+
Older file-based logins migrate to this file without requesting Keychain access. Windows keeps
|
|
42
|
+
DPAPI encryption as its default.
|
|
43
|
+
Existing Keychain, DPAPI, or Secret Service logins keep their selected store; an unavailable store
|
|
44
|
+
never causes an automatic switch to file storage.
|
|
45
|
+
|
|
46
|
+
If an existing macOS login requests Keychain access, it is requesting the `md.mostlyright.cli`
|
|
47
|
+
device credential. To switch that login to file storage, restore access to its Keychain item,
|
|
48
|
+
run `mr-data auth logout` successfully, then run `mr-data login`. The `--credential-store secure-file`
|
|
49
|
+
option does not move an existing native-store login.
|
|
50
|
+
|
|
39
51
|
## Check the installation
|
|
40
52
|
|
|
41
53
|
```bash
|
|
@@ -252,6 +252,13 @@ machine that is only using the product installs from `pip` instead.
|
|
|
252
252
|
|
|
253
253
|
## Cloud authentication preflight
|
|
254
254
|
|
|
255
|
+
New and legacy file-based logins on macOS/Linux use an owner-only, unencrypted private file.
|
|
256
|
+
Windows keeps DPAPI encryption by default. Existing native-store logins retain their backend. Never bypass a Keychain/DPAPI/Secret Service refusal by
|
|
257
|
+
selecting secure-file: that flag cannot migrate an existing native-store login. Restore access,
|
|
258
|
+
then use a successful remote-first logout followed by login when the user wants file storage.
|
|
259
|
+
An interrupted sign-in can retain a pending record; follow its recovery instruction instead of
|
|
260
|
+
repeatedly starting another login.
|
|
261
|
+
|
|
255
262
|
Authentication begins only when the next command will contact the Mostly Right cloud. Immediately
|
|
256
263
|
before hosted `mr-data dataset create` or `mr-data recipe`, run
|
|
257
264
|
`mr-data whoami --json` and read its `status`, and it intentionally does not report
|
|
@@ -933,16 +940,25 @@ finished work rather than a shape test. Where a source was never opened, and its
|
|
|
933
940
|
a guess, two thousand rows a source is the fallback: it exercises every column, every cast and
|
|
934
941
|
every declared check, and it comes back while the person who asked is still watching the page.
|
|
935
942
|
|
|
936
|
-
**What bounds a source is how much it DECODES, not how much it weighs.**
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
943
|
+
**What bounds a source is how much it DECODES, not how much it weighs.** The two come apart
|
|
944
|
+
constantly: a 10.4 MB document projected to a few columns decodes to a few hundred kilobytes,
|
|
945
|
+
while an 8.6 MB table read whole decodes to 8.6 MB. So the fetched size predicts nothing on its
|
|
946
|
+
own, and a source over either bound is refused with `READER_BUDGET`, which names both sizes.
|
|
947
|
+
|
|
948
|
+
**How far a source may fetch depends on how it will be read.** A Reader-pinned source, and an
|
|
949
|
+
unpinned `csv` one, fetch up to 256 MiB. An unpinned `ndjson` source fetches up to what one
|
|
950
|
+
confined request carries, about 36 MB. An unpinned `json` or `parquet` source keeps 16 MiB,
|
|
951
|
+
because neither has a row-at-a-time reader and both are read whole inside the clean room —
|
|
952
|
+
pinning a Reader is the way past that. What a source may DECODE to is its Reader family's output
|
|
953
|
+
budget, narrowed to at most 1 GiB, because the whole decoded relation is held in memory while it
|
|
954
|
+
is classified and sealed. A wide source is still best narrowed by projection, which is what
|
|
955
|
+
shrinks the decoded relation; a long source can still be split.
|
|
941
956
|
|
|
942
957
|
`limits.max_source_bytes` states where fetching STOPS rather than how big the source is, so
|
|
943
958
|
`mr-data recipe` does not refuse a large one. It registers the document and puts one sentence on
|
|
944
|
-
the receipt under `warnings` when the declared number is larger than
|
|
945
|
-
that as "go and check this one", not as a
|
|
959
|
+
the receipt under `warnings` when the declared number is larger than a fetch can reach, and the
|
|
960
|
+
sentence names the formats that stop earlier. Read that as "go and check this one", not as a
|
|
961
|
+
failure.
|
|
946
962
|
|
|
947
963
|
**Which remedy applies depends on which way the source is too big, and lowering the ceiling is
|
|
948
964
|
never it** — that does not fetch less data, it truncates the sample or refuses the full run. A WIDE
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/recipe.py
RENAMED
|
@@ -67,26 +67,20 @@ REQUIRED_DOCUMENT_MEMBERS: tuple[str, ...] = (
|
|
|
67
67
|
"timezone",
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
#: The declared ``limits.max_source_bytes`` above which one source is worth a word, in bytes
|
|
71
|
-
#:
|
|
72
|
-
#:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
#:
|
|
76
|
-
#:
|
|
77
|
-
#:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#:
|
|
81
|
-
#:
|
|
82
|
-
#:
|
|
83
|
-
|
|
84
|
-
#:
|
|
85
|
-
#: So this stays a warning about a DECLARED ceiling being implausibly large, which is worth one
|
|
86
|
-
#: sentence, and it no longer promises to predict a failure it cannot see. It is set at the
|
|
87
|
-
#: backend's own output ceiling: a document declaring more than one decode can return is a
|
|
88
|
-
#: document whose author has not measured the source, whatever the source turns out to weigh.
|
|
89
|
-
READER_SOURCE_BYTE_CEILING = 6_845_533
|
|
70
|
+
#: The declared ``limits.max_source_bytes`` above which one source is worth a word, in bytes --
|
|
71
|
+
#: for a source that is read through descriptors, which is a Reader-pinned one or an unpinned
|
|
72
|
+
#: ``csv`` one. That is the hosted fetch cap.
|
|
73
|
+
READER_SOURCE_BYTE_CEILING = 268_435_456
|
|
74
|
+
|
|
75
|
+
#: The same ceiling for an unpinned ``ndjson`` source. It has an inline streaming pass and no
|
|
76
|
+
#: descriptor-backed one, so its bytes travel inside one confined request and what bounds the
|
|
77
|
+
#: fetch is what that request can carry.
|
|
78
|
+
NDJSON_SOURCE_BYTE_CEILING = 37_745_664
|
|
79
|
+
|
|
80
|
+
#: And for an unpinned ``json`` or ``parquet`` source, which has no streaming pass at all: both
|
|
81
|
+
#: are read whole inside the clean room, so the fetch keeps the drained read's tighter bound.
|
|
82
|
+
#: Pinning a Reader is what moves a source off this ceiling.
|
|
83
|
+
DRAINED_SOURCE_BYTE_CEILING = 16_777_216
|
|
90
84
|
|
|
91
85
|
#: How many undescribed columns the warning names before it counts the rest. A table plan admits
|
|
92
86
|
#: 512 columns, and a sentence that spelt every one of them out would be the wall the `numbered`
|
|
@@ -194,24 +188,16 @@ def check_document(document: dict[str, Any]) -> None:
|
|
|
194
188
|
|
|
195
189
|
|
|
196
190
|
def source_byte_warnings(sources: Any) -> list[str]:
|
|
197
|
-
"""One sentence per source whose declared ceiling is larger than
|
|
191
|
+
"""One sentence per source whose declared ceiling is larger than the hosted lane fetches.
|
|
198
192
|
|
|
199
193
|
⚠ WARNINGS, AND NOT A GATE, FOR THE REASON :data:`READER_SOURCE_BYTE_CEILING` GIVES: the
|
|
200
194
|
declared number is where fetching STOPS, not how big the source is. A gate over it refuses
|
|
201
|
-
recipes that run
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
⚠ THE LARGEST SOURCE DECIDES, NOT THE SUM. One Reader opens one source at a time inside one
|
|
206
|
-
Clean room, so eight 2 MiB sources are worth nothing and one 6 MiB source is worth a sentence.
|
|
207
|
-
A warning over the total would shout at recipes that run and stay quiet about the one that
|
|
208
|
-
will not.
|
|
195
|
+
recipes that run and still admits a source that declares 1 MiB and delivers 6. So nothing here
|
|
196
|
+
decides anything; it tells the agent that wrote the number that the hosted lane will not honour
|
|
197
|
+
it.
|
|
209
198
|
|
|
210
|
-
⚠
|
|
211
|
-
|
|
212
|
-
truncates the fetch rather than decoding less. Narrowing what the reader projects is the answer
|
|
213
|
-
when the source is wide; splitting the source is the answer when it is long. Splitting a wide
|
|
214
|
-
source does nothing, which is why the two cases are named separately.
|
|
199
|
+
⚠ THE LARGEST SOURCE DECIDES, NOT THE SUM. One source is fetched at a time, so the sentence is
|
|
200
|
+
about the one that overstates its ceiling rather than about the total.
|
|
215
201
|
|
|
216
202
|
A declared ceiling that is not a whole number of bytes is left alone: the document admits no
|
|
217
203
|
fractional number at all, ``read_document`` has already refused one, and re-deciding here what
|
|
@@ -228,25 +214,45 @@ def source_byte_warnings(sources: Any) -> list[str]:
|
|
|
228
214
|
if not isinstance(limits, Mapping):
|
|
229
215
|
continue
|
|
230
216
|
declared = limits.get("max_source_bytes")
|
|
231
|
-
if type(declared) is not int
|
|
217
|
+
if type(declared) is not int:
|
|
218
|
+
continue
|
|
219
|
+
ceiling, why = _source_byte_ceiling(source)
|
|
220
|
+
if declared <= ceiling:
|
|
232
221
|
continue
|
|
233
222
|
named = source.get("name")
|
|
234
223
|
where = f"{named!r}" if isinstance(named, str) and named else f"sources[{index}]"
|
|
235
224
|
said.append(
|
|
236
|
-
f"source {where} declares limits.max_source_bytes of {declared:,} bytes,
|
|
237
|
-
f"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"ceiling does not help, because it bounds the fetch and truncates rather than "
|
|
244
|
-
"decoding less. Narrow the projection, or split the source and union the parts in a "
|
|
245
|
-
"first transform step"
|
|
225
|
+
f"source {where} declares limits.max_source_bytes of {declared:,} bytes, and {why} "
|
|
226
|
+
f"stops at {ceiling:,}. That is a ceiling on fetching rather than a measurement of "
|
|
227
|
+
"the source, so this registers and runs, and the fetch stops there whatever the "
|
|
228
|
+
"document says. An unpinned source that answers ndjson stops at "
|
|
229
|
+
f"{NDJSON_SOURCE_BYTE_CEILING:,} and one that answers json or parquet at "
|
|
230
|
+
f"{DRAINED_SOURCE_BYTE_CEILING:,}, and pinning a Reader is what moves it off "
|
|
231
|
+
"those. Declare what the source actually weighs"
|
|
246
232
|
)
|
|
247
233
|
return said
|
|
248
234
|
|
|
249
235
|
|
|
236
|
+
def _source_byte_ceiling(source: Mapping[str, Any]) -> tuple[int, str]:
|
|
237
|
+
"""The fetch ceiling one declared source is held to, and the phrase that names it.
|
|
238
|
+
|
|
239
|
+
A document declares a Reader pin on ``connector.parameters`` and declares no wire format at
|
|
240
|
+
all: the format is what the response turns out to be. So the one thing this client can read
|
|
241
|
+
off the document is whether the source is pinned. A pinned source fetches to the hosted cap
|
|
242
|
+
whatever its wire format. An unpinned source is held to the same cap here, and the sentence
|
|
243
|
+
says which formats stop earlier, because that is decided by bytes this client has not seen.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
connector = source.get("connector")
|
|
247
|
+
parameters = connector.get("parameters") if isinstance(connector, Mapping) else None
|
|
248
|
+
if isinstance(parameters, list) and any(
|
|
249
|
+
isinstance(parameter, Mapping) and parameter.get("name") == "reader.family_id"
|
|
250
|
+
for parameter in parameters
|
|
251
|
+
):
|
|
252
|
+
return READER_SOURCE_BYTE_CEILING, "a Reader-pinned source's fetch"
|
|
253
|
+
return READER_SOURCE_BYTE_CEILING, "an unpinned source's fetch"
|
|
254
|
+
|
|
255
|
+
|
|
250
256
|
def columns_without_description(table: Any) -> list[str]:
|
|
251
257
|
"""The declared columns carrying no description, in the order the document declares them.
|
|
252
258
|
|
|
@@ -469,8 +475,10 @@ def recipe(args: argparse.Namespace, *, client: StudioV4Client | None = None) ->
|
|
|
469
475
|
|
|
470
476
|
__all__ = [
|
|
471
477
|
"COMMAND_HELP",
|
|
478
|
+
"DRAINED_SOURCE_BYTE_CEILING",
|
|
472
479
|
"MAX_DOCUMENT_BYTES",
|
|
473
480
|
"NAMED_UNDESCRIBED_COLUMNS",
|
|
481
|
+
"NDJSON_SOURCE_BYTE_CEILING",
|
|
474
482
|
"READER_SOURCE_BYTE_CEILING",
|
|
475
483
|
"REGISTER_SCHEMA",
|
|
476
484
|
"REQUIRED_DOCUMENT_MEMBERS",
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/commands/auth.py
RENAMED
|
@@ -48,7 +48,9 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
48
48
|
dest="auth_command", required=True, help="device-authority operation"
|
|
49
49
|
)
|
|
50
50
|
nested_json = argparse.ArgumentParser(add_help=False)
|
|
51
|
-
nested_json.add_argument(
|
|
51
|
+
nested_json.add_argument(
|
|
52
|
+
"--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS
|
|
53
|
+
)
|
|
52
54
|
|
|
53
55
|
status = subcommands.add_parser(
|
|
54
56
|
"status", parents=[nested_json], help="validate the effective device credential"
|
|
@@ -56,7 +58,7 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
56
58
|
status.add_argument(
|
|
57
59
|
"--credential-store",
|
|
58
60
|
choices=("secure-file",),
|
|
59
|
-
help="
|
|
61
|
+
help="select private-file storage for legacy credential migration",
|
|
60
62
|
)
|
|
61
63
|
status.set_defaults(auth_operation="status")
|
|
62
64
|
|
|
@@ -95,7 +97,9 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
95
97
|
)
|
|
96
98
|
login.add_argument("--device-name", help="what to call this device in Cloud")
|
|
97
99
|
login.add_argument(
|
|
98
|
-
"--credential-store",
|
|
100
|
+
"--credential-store",
|
|
101
|
+
choices=("secure-file",),
|
|
102
|
+
help="select private-file storage; cannot change an existing native store",
|
|
99
103
|
)
|
|
100
104
|
login.set_defaults(auth_operation="login")
|
|
101
105
|
rotate = subcommands.add_parser(
|
|
@@ -103,7 +107,9 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
103
107
|
)
|
|
104
108
|
rotate.add_argument("--device-name", help="what to call the replacement device")
|
|
105
109
|
rotate.add_argument(
|
|
106
|
-
"--credential-store",
|
|
110
|
+
"--credential-store",
|
|
111
|
+
choices=("secure-file",),
|
|
112
|
+
help="select private-file storage; cannot change an existing native store",
|
|
107
113
|
)
|
|
108
114
|
rotate.set_defaults(auth_operation="rotate")
|
|
109
115
|
logout = subcommands.add_parser(
|
|
@@ -117,7 +123,9 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
117
123
|
help="remove local state without claiming remote revocation",
|
|
118
124
|
)
|
|
119
125
|
logout.add_argument(
|
|
120
|
-
"--credential-store",
|
|
126
|
+
"--credential-store",
|
|
127
|
+
choices=("secure-file",),
|
|
128
|
+
help="select private-file storage; cannot change an existing native store",
|
|
121
129
|
)
|
|
122
130
|
logout.set_defaults(auth_operation="logout")
|
|
123
131
|
recover = subcommands.add_parser(
|
|
@@ -131,7 +139,9 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
131
139
|
help="clear only a stale login lease; remote mint remains unproved",
|
|
132
140
|
)
|
|
133
141
|
recover.add_argument(
|
|
134
|
-
"--credential-store",
|
|
142
|
+
"--credential-store",
|
|
143
|
+
choices=("secure-file",),
|
|
144
|
+
help="select private-file storage; cannot change an existing native store",
|
|
135
145
|
)
|
|
136
146
|
recover.set_defaults(auth_operation="recover")
|
|
137
147
|
|
|
@@ -145,9 +155,17 @@ def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
145
155
|
if operation == "devices_revoke":
|
|
146
156
|
return device_revoke_payload(args.key_id)
|
|
147
157
|
if operation == "login":
|
|
148
|
-
return login_payload(
|
|
158
|
+
return login_payload(
|
|
159
|
+
device_name=args.device_name,
|
|
160
|
+
credential_store=args.credential_store,
|
|
161
|
+
announce=not args.json,
|
|
162
|
+
)
|
|
149
163
|
if operation == "rotate":
|
|
150
|
-
return rotate_payload(
|
|
164
|
+
return rotate_payload(
|
|
165
|
+
device_name=args.device_name,
|
|
166
|
+
credential_store=args.credential_store,
|
|
167
|
+
announce=not args.json,
|
|
168
|
+
)
|
|
151
169
|
if operation == "logout":
|
|
152
170
|
return logout_payload(local_only=args.local_only, credential_store=args.credential_store)
|
|
153
171
|
if operation == "recover":
|
|
@@ -491,6 +509,19 @@ def recover_payload(
|
|
|
491
509
|
with credential_lock(store.config_dir):
|
|
492
510
|
envelope = store.load_under_held_lock()
|
|
493
511
|
if envelope is None:
|
|
512
|
+
if local_only:
|
|
513
|
+
# Resume a crash after deleting an empty pending store but before
|
|
514
|
+
# removing its selector. Never claim remote cleanup from local absence.
|
|
515
|
+
remove_credential_manifest_under_held_lock(path)
|
|
516
|
+
return _with_environment_authority(
|
|
517
|
+
{
|
|
518
|
+
"schema_version": _RECOVERY_SCHEMA,
|
|
519
|
+
"status": "local_recovery_completed",
|
|
520
|
+
"remote_revocation": "unproved",
|
|
521
|
+
"local_state": "removed",
|
|
522
|
+
"mint_outcome": "unproved",
|
|
523
|
+
}
|
|
524
|
+
)
|
|
494
525
|
return _mutation_error(
|
|
495
526
|
_RECOVERY_SCHEMA,
|
|
496
527
|
status="refused",
|
|
@@ -516,16 +547,37 @@ def recover_payload(
|
|
|
516
547
|
)
|
|
517
548
|
if envelope.state == "login_pending":
|
|
518
549
|
if not local_only:
|
|
519
|
-
return
|
|
520
|
-
|
|
550
|
+
return {
|
|
551
|
+
**_mutation_error(
|
|
552
|
+
_RECOVERY_SCHEMA, status="refused", local_state="pending"
|
|
553
|
+
),
|
|
554
|
+
"next_action": "Finish or stop the previous sign-in. If it has stopped, "
|
|
555
|
+
"run mr-data auth recover --local-only, then retry logout or login. "
|
|
556
|
+
"Local recovery does not prove remote revocation.",
|
|
557
|
+
}
|
|
558
|
+
# The outer lock already owns this generation. Calling the standalone
|
|
559
|
+
# recovery helper here would try to acquire the same non-reentrant lock.
|
|
560
|
+
if envelope.active is None:
|
|
561
|
+
# Keep the selector retryable if the native delete refuses access.
|
|
562
|
+
store.delete_under_held_lock(
|
|
563
|
+
expected_generation=envelope.generation,
|
|
564
|
+
expected_states=frozenset({"login_pending"}),
|
|
565
|
+
)
|
|
566
|
+
remove_credential_manifest_under_held_lock(path)
|
|
567
|
+
else:
|
|
568
|
+
store.replace_under_held_lock(
|
|
569
|
+
CredentialEnvelope.active_envelope(
|
|
570
|
+
generation=envelope.generation + 1, active=envelope.active
|
|
571
|
+
),
|
|
572
|
+
expected_generation=envelope.generation,
|
|
573
|
+
expected_states=frozenset({"login_pending"}),
|
|
521
574
|
)
|
|
522
|
-
store.recover_login_lease_local_only()
|
|
523
575
|
return _with_environment_authority(
|
|
524
576
|
{
|
|
525
577
|
"schema_version": _RECOVERY_SCHEMA,
|
|
526
578
|
"status": "local_recovery_completed",
|
|
527
579
|
"remote_revocation": "unproved",
|
|
528
|
-
"local_state": "
|
|
580
|
+
"local_state": "active" if envelope.active is not None else "removed",
|
|
529
581
|
"mint_outcome": "unproved",
|
|
530
582
|
}
|
|
531
583
|
)
|
|
@@ -652,16 +704,29 @@ def _cleanup_environment_revoked_stored_target(
|
|
|
652
704
|
return "pending"
|
|
653
705
|
|
|
654
706
|
|
|
655
|
-
def login_payload(
|
|
656
|
-
|
|
707
|
+
def login_payload(
|
|
708
|
+
*, device_name: str | None, credential_store: str | None, announce: bool = False
|
|
709
|
+
) -> dict[str, Any]:
|
|
710
|
+
from mostlyright.data_harness.ux.login import announce_device_code, run_login
|
|
657
711
|
|
|
658
|
-
return run_login(
|
|
712
|
+
return run_login(
|
|
713
|
+
device_name=device_name,
|
|
714
|
+
credential_store=credential_store,
|
|
715
|
+
announce=announce_device_code if announce else None,
|
|
716
|
+
)
|
|
659
717
|
|
|
660
718
|
|
|
661
|
-
def rotate_payload(
|
|
662
|
-
|
|
719
|
+
def rotate_payload(
|
|
720
|
+
*, device_name: str | None, credential_store: str | None, announce: bool = False
|
|
721
|
+
) -> dict[str, Any]:
|
|
722
|
+
from mostlyright.data_harness.ux.login import announce_device_code, run_login
|
|
663
723
|
|
|
664
|
-
return run_login(
|
|
724
|
+
return run_login(
|
|
725
|
+
device_name=device_name,
|
|
726
|
+
force=True,
|
|
727
|
+
credential_store=credential_store,
|
|
728
|
+
announce=announce_device_code if announce else None,
|
|
729
|
+
)
|
|
665
730
|
|
|
666
731
|
|
|
667
732
|
def tokens_payload(operation: str) -> dict[str, Any]:
|
|
@@ -22,8 +22,10 @@ from mostlyright.data_harness.ux.login import CLOUD_URL_ENV, announce_device_cod
|
|
|
22
22
|
|
|
23
23
|
DESCRIPTION = (
|
|
24
24
|
"Sign in to the Mostly Right cloud from this machine. Prints a short code and opens a browser "
|
|
25
|
-
"to approve it there
|
|
26
|
-
|
|
25
|
+
"to approve it there. New macOS/Linux logins use ~/.mostlyright/credentials.secure, an "
|
|
26
|
+
"unencrypted file restricted to your OS account (0600). Windows uses DPAPI encryption; "
|
|
27
|
+
"existing logins keep their selected store. "
|
|
28
|
+
f"The raw credential is never printed; only its prefix and last four characters "
|
|
27
29
|
f"are. Set {CLOUD_URL_ENV} to sign in against a different cloud."
|
|
28
30
|
)
|
|
29
31
|
|
|
@@ -42,7 +44,7 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
42
44
|
parser.add_argument(
|
|
43
45
|
"--credential-store",
|
|
44
46
|
choices=("secure-file",),
|
|
45
|
-
help="
|
|
47
|
+
help="use the private-file store (default on macOS/Linux; existing stores are preserved)",
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
|
|
@@ -18,7 +18,7 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
18
18
|
parser.add_argument(
|
|
19
19
|
"--credential-store",
|
|
20
20
|
choices=("secure-file",),
|
|
21
|
-
help="
|
|
21
|
+
help="select private-file storage for legacy credential migration",
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
|
|
@@ -56,8 +56,8 @@ class MacOSKeychainBackend:
|
|
|
56
56
|
if payload is not None and len(payload) > MAX_CREDENTIAL_ENVELOPE_BYTES:
|
|
57
57
|
raise ValueError("oversized item")
|
|
58
58
|
return payload
|
|
59
|
-
except Exception:
|
|
60
|
-
raise
|
|
59
|
+
except Exception as error:
|
|
60
|
+
raise _keychain_refusal("read", error) from None
|
|
61
61
|
|
|
62
62
|
def write(self, payload: bytes) -> None:
|
|
63
63
|
if len(payload) > MAX_CREDENTIAL_ENVELOPE_BYTES:
|
|
@@ -67,16 +67,16 @@ class MacOSKeychainBackend:
|
|
|
67
67
|
mutable = bytearray(payload)
|
|
68
68
|
try:
|
|
69
69
|
self._ffi.write(mutable, service=_SERVICE, account=_ACCOUNT)
|
|
70
|
-
except Exception:
|
|
71
|
-
raise
|
|
70
|
+
except Exception as error:
|
|
71
|
+
raise _keychain_refusal("write", error) from None
|
|
72
72
|
finally:
|
|
73
73
|
_zero(mutable)
|
|
74
74
|
|
|
75
75
|
def delete(self) -> None:
|
|
76
76
|
try:
|
|
77
77
|
self._ffi.delete(service=_SERVICE, account=_ACCOUNT)
|
|
78
|
-
except Exception:
|
|
79
|
-
raise
|
|
78
|
+
except Exception as error:
|
|
79
|
+
raise _keychain_refusal("delete", error) from None
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
class DPAPIBackend:
|
|
@@ -91,7 +91,6 @@ class DPAPIBackend:
|
|
|
91
91
|
# keeps replace/fsync/link checks identical without making secure-file a fallback.
|
|
92
92
|
self._blob = SecureFileBackend(
|
|
93
93
|
config_dir,
|
|
94
|
-
explicit=True,
|
|
95
94
|
filename="credentials.dpapi",
|
|
96
95
|
)
|
|
97
96
|
except Exception:
|
|
@@ -248,8 +247,8 @@ def native_backend(
|
|
|
248
247
|
if executable is None:
|
|
249
248
|
raise CredentialStoreError(
|
|
250
249
|
"CREDENTIAL_STORE_UNAVAILABLE",
|
|
251
|
-
"Secret Service
|
|
252
|
-
"
|
|
250
|
+
"Secret Service is unavailable; restore secret-tool and the desktop "
|
|
251
|
+
"secret service to access the existing login",
|
|
253
252
|
)
|
|
254
253
|
return SecretServiceBackend(executable=executable)
|
|
255
254
|
except CredentialStoreError:
|
|
@@ -258,8 +257,7 @@ def native_backend(
|
|
|
258
257
|
pass
|
|
259
258
|
raise CredentialStoreError(
|
|
260
259
|
"CREDENTIAL_STORE_UNAVAILABLE",
|
|
261
|
-
"native protected storage is unavailable
|
|
262
|
-
"--credential-store secure-file to use an owner-controlled file",
|
|
260
|
+
"the existing login uses native protected storage that is unavailable on this system",
|
|
263
261
|
)
|
|
264
262
|
|
|
265
263
|
|
|
@@ -270,6 +268,21 @@ def _native_refusal(adapter: str, outcome: str) -> CredentialStoreError:
|
|
|
270
268
|
)
|
|
271
269
|
|
|
272
270
|
|
|
271
|
+
class _KeychainStatusError(OSError):
|
|
272
|
+
"""Carry only the numeric Security.framework result, never native error text."""
|
|
273
|
+
|
|
274
|
+
def __init__(self, status: int) -> None:
|
|
275
|
+
self.status = status
|
|
276
|
+
super().__init__("Keychain operation failed")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _keychain_refusal(operation: str, error: Exception) -> CredentialStoreError:
|
|
280
|
+
detail = f"Keychain {operation} failed"
|
|
281
|
+
if isinstance(error, _KeychainStatusError):
|
|
282
|
+
detail += f" (OSStatus {error.status})"
|
|
283
|
+
return CredentialStoreError("CREDENTIAL_STORE_UNAVAILABLE", detail)
|
|
284
|
+
|
|
285
|
+
|
|
273
286
|
def _zero(value: bytearray) -> None:
|
|
274
287
|
for index in range(len(value)):
|
|
275
288
|
value[index] = 0
|
|
@@ -491,8 +504,10 @@ class _MacOSSecurityFramework:
|
|
|
491
504
|
status = self._security.SecItemCopyMatching(rich_query, ctypes.byref(result))
|
|
492
505
|
if status == self._NOT_FOUND:
|
|
493
506
|
return None
|
|
494
|
-
if status != self._SUCCESS
|
|
495
|
-
raise
|
|
507
|
+
if status != self._SUCCESS:
|
|
508
|
+
raise _KeychainStatusError(status)
|
|
509
|
+
if not result.value:
|
|
510
|
+
raise OSError("SecItemCopyMatching returned no data")
|
|
496
511
|
length = self._core.CFDataGetLength(result)
|
|
497
512
|
if length < 0 or length > MAX_CREDENTIAL_ENVELOPE_BYTES:
|
|
498
513
|
raise OSError("invalid Keychain item size")
|
|
@@ -525,7 +540,7 @@ class _MacOSSecurityFramework:
|
|
|
525
540
|
if status == self._DUPLICATE:
|
|
526
541
|
status = self._security.SecItemUpdate(query, attributes)
|
|
527
542
|
if status != self._SUCCESS:
|
|
528
|
-
raise
|
|
543
|
+
raise _KeychainStatusError(status)
|
|
529
544
|
finally:
|
|
530
545
|
self._core.CFRelease(attributes)
|
|
531
546
|
self._core.CFRelease(data)
|
|
@@ -537,7 +552,7 @@ class _MacOSSecurityFramework:
|
|
|
537
552
|
try:
|
|
538
553
|
status = self._security.SecItemDelete(query)
|
|
539
554
|
if status not in {self._SUCCESS, self._NOT_FOUND}:
|
|
540
|
-
raise
|
|
555
|
+
raise _KeychainStatusError(status)
|
|
541
556
|
finally:
|
|
542
557
|
self._core.CFRelease(query)
|
|
543
558
|
self._release_all(owned)
|
|
@@ -324,7 +324,7 @@ class CredentialBackend(Protocol):
|
|
|
324
324
|
|
|
325
325
|
|
|
326
326
|
class SecureFileBackend:
|
|
327
|
-
"""
|
|
327
|
+
"""Atomic owner-only file backend, selected by policy or a saved manifest."""
|
|
328
328
|
|
|
329
329
|
name = "secure-file"
|
|
330
330
|
|
|
@@ -332,14 +332,8 @@ class SecureFileBackend:
|
|
|
332
332
|
self,
|
|
333
333
|
config_dir: Path,
|
|
334
334
|
*,
|
|
335
|
-
explicit: bool,
|
|
336
335
|
filename: str = SECURE_FILE_NAME,
|
|
337
336
|
) -> None:
|
|
338
|
-
if explicit is not True:
|
|
339
|
-
raise CredentialStoreError(
|
|
340
|
-
"CREDENTIAL_STORE_EXPLICIT_REQUIRED",
|
|
341
|
-
"secure-file storage requires explicit selection",
|
|
342
|
-
)
|
|
343
337
|
self.config_dir = Path(config_dir)
|
|
344
338
|
if not filename or filename in {".", ".."} or Path(filename).name != filename:
|
|
345
339
|
raise CredentialStoreError(
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/credentials.py
RENAMED
|
@@ -11,7 +11,8 @@ import contextlib
|
|
|
11
11
|
import os
|
|
12
12
|
import secrets
|
|
13
13
|
import stat
|
|
14
|
-
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable, Mapping
|
|
15
16
|
from dataclasses import dataclass
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any
|
|
@@ -175,12 +176,12 @@ def store_credentials(
|
|
|
175
176
|
)
|
|
176
177
|
control = _read_control(path)
|
|
177
178
|
if control is None:
|
|
178
|
-
backend = _backend_for(directory, selector or
|
|
179
|
+
backend = _backend_for(directory, selector or _default_backend_name())
|
|
179
180
|
current = None
|
|
180
181
|
else:
|
|
181
182
|
kind, value = _parse_control(control, path)
|
|
182
183
|
if kind == "legacy":
|
|
183
|
-
backend = _backend_for(directory, selector or
|
|
184
|
+
backend = _backend_for(directory, selector or _default_backend_name())
|
|
184
185
|
current = None
|
|
185
186
|
else:
|
|
186
187
|
backend_name = value["backend"]
|
|
@@ -232,7 +233,7 @@ def read_credentials(
|
|
|
232
233
|
return _active_credentials(envelope)
|
|
233
234
|
|
|
234
235
|
legacy = _legacy_credentials(value, path)
|
|
235
|
-
backend = _backend_for(directory, selector or
|
|
236
|
+
backend = _backend_for(directory, selector or _default_backend_name())
|
|
236
237
|
envelope = CredentialEnvelope.active_envelope(
|
|
237
238
|
generation=1,
|
|
238
239
|
active=legacy._protected(),
|
|
@@ -295,6 +296,7 @@ def credential_transaction_store(
|
|
|
295
296
|
*,
|
|
296
297
|
config_dir: Path | None = None,
|
|
297
298
|
credential_store: str | None = None,
|
|
299
|
+
announce_store: Callable[[str], None] | None = None,
|
|
298
300
|
) -> tuple[CredentialStore, Path] | None:
|
|
299
301
|
"""Return the selected protected store and its non-secret manifest path.
|
|
300
302
|
|
|
@@ -308,7 +310,7 @@ def credential_transaction_store(
|
|
|
308
310
|
path = directory / CREDENTIALS_FILENAME
|
|
309
311
|
selector = _selector(credential_store)
|
|
310
312
|
if not os.path.lexists(path):
|
|
311
|
-
backend = _backend_for(directory, selector or
|
|
313
|
+
backend = _backend_for(directory, selector or _default_backend_name())
|
|
312
314
|
return CredentialStore(directory, backend), path
|
|
313
315
|
raw = _read_control(path)
|
|
314
316
|
if raw is None:
|
|
@@ -325,6 +327,8 @@ def credential_transaction_store(
|
|
|
325
327
|
raise LoginError("LOGIN_CREDENTIALS_UNREADABLE", "the stored credential is unreadable")
|
|
326
328
|
backend_name = value["backend"]
|
|
327
329
|
_require_selector_agrees(selector, backend_name)
|
|
330
|
+
if announce_store is not None and backend_name != "secure-file":
|
|
331
|
+
announce_store(backend_name)
|
|
328
332
|
return CredentialStore(directory, _backend_for(directory, backend_name)), path
|
|
329
333
|
|
|
330
334
|
|
|
@@ -388,17 +392,16 @@ def _selector(value: str | None) -> str | None:
|
|
|
388
392
|
return value
|
|
389
393
|
|
|
390
394
|
|
|
391
|
-
def
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
raise login_store_error(error) from None
|
|
395
|
+
def _default_backend_name() -> str:
|
|
396
|
+
# DPAPI is non-interactive and encrypts the Windows blob; its file ACL gate proves
|
|
397
|
+
# allowed writers, not absence of inherited readers, so plaintext is not the default there.
|
|
398
|
+
return "dpapi" if sys.platform == "win32" else "secure-file"
|
|
396
399
|
|
|
397
400
|
|
|
398
401
|
def _backend_for(directory: Path, backend_name: str) -> CredentialBackend:
|
|
399
402
|
if backend_name == "secure-file":
|
|
400
403
|
try:
|
|
401
|
-
return SecureFileBackend(directory
|
|
404
|
+
return SecureFileBackend(directory)
|
|
402
405
|
except CredentialStoreError as error:
|
|
403
406
|
raise login_store_error(error) from None
|
|
404
407
|
try:
|
|
@@ -594,10 +597,7 @@ def _install_manifest(path: Path, backend_name: str) -> None:
|
|
|
594
597
|
def login_store_error(error: CredentialStoreError) -> LoginError:
|
|
595
598
|
suffix = error.code.removeprefix("CREDENTIAL_")
|
|
596
599
|
details = {
|
|
597
|
-
"CREDENTIAL_STORE_UNAVAILABLE":
|
|
598
|
-
"native protected storage is unavailable; explicitly select "
|
|
599
|
-
"--credential-store secure-file to use an owner-controlled file"
|
|
600
|
-
),
|
|
600
|
+
"CREDENTIAL_STORE_UNAVAILABLE": error.detail or "the credential store is unavailable",
|
|
601
601
|
"CREDENTIAL_STORE_WRITE_FAILED": "the protected credential could not be written",
|
|
602
602
|
"CREDENTIAL_STORE_READBACK_FAILED": (
|
|
603
603
|
"the protected credential could not be verified after writing"
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/login.py
RENAMED
|
@@ -472,6 +472,18 @@ def announce_device_code(authorization: DeviceAuthorization) -> None:
|
|
|
472
472
|
print("Waiting for approval...", file=sys.stderr, flush=True)
|
|
473
473
|
|
|
474
474
|
|
|
475
|
+
def announce_credential_store(backend: str) -> None:
|
|
476
|
+
"""Explain an existing native login before the OS can request access to it."""
|
|
477
|
+
|
|
478
|
+
if backend == "keychain":
|
|
479
|
+
print(
|
|
480
|
+
"This login uses macOS Keychain. macOS may ask to access mr-data's own device "
|
|
481
|
+
"credential (md.mostlyright.cli); no other saved passwords are requested.",
|
|
482
|
+
file=sys.stderr,
|
|
483
|
+
flush=True,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
475
487
|
def run_login(
|
|
476
488
|
*,
|
|
477
489
|
cloud_url: str | None = None,
|
|
@@ -508,7 +520,9 @@ def run_login(
|
|
|
508
520
|
refuse_existing_credentials(config_dir=config_dir)
|
|
509
521
|
try:
|
|
510
522
|
opened = credential_transaction_store(
|
|
511
|
-
config_dir=config_dir,
|
|
523
|
+
config_dir=config_dir,
|
|
524
|
+
credential_store=credential_store,
|
|
525
|
+
announce_store=announce_credential_store if announce is not None else None,
|
|
512
526
|
)
|
|
513
527
|
except LoginError:
|
|
514
528
|
raise
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/remediation.py
RENAMED
|
@@ -11,7 +11,7 @@ neither affordable nor honest:
|
|
|
11
11
|
* **Families.** Every code prefix is assigned to a family -- 224 of them, plus 18 compound
|
|
12
12
|
prefixes, across 38 families. A family is one paragraph: what this class of failure is about,
|
|
13
13
|
where to look, and what to do. Every prefix has one, so no code can fail to say something useful.
|
|
14
|
-
* **Curated codes.** The
|
|
14
|
+
* **Curated codes.** The 107 failures a person actually meets get their own exact sentences, and
|
|
15
15
|
where one command is the whole answer, the command itself.
|
|
16
16
|
|
|
17
17
|
Those four counts are not a note somebody remembered to update:
|
|
@@ -436,7 +436,8 @@ FAMILIES: dict[str, Family] = {
|
|
|
436
436
|
"on this machine.",
|
|
437
437
|
"The message names the step that stopped: starting sign-in, waiting for browser "
|
|
438
438
|
"approval, or storing the credential.",
|
|
439
|
-
"A stopped sign-in
|
|
439
|
+
"A stopped sign-in may retain the previous login or a pending sign-in record. "
|
|
440
|
+
"Follow the storage or recovery instruction in the message before retrying.",
|
|
440
441
|
)
|
|
441
442
|
),
|
|
442
443
|
# Catalog facts and provenance require remediation distinct from Recipe source files.
|
|
@@ -1770,6 +1771,41 @@ CODES: dict[str, Remediation] = {
|
|
|
1770
1771
|
"mr-data login --force.",
|
|
1771
1772
|
),
|
|
1772
1773
|
),
|
|
1774
|
+
"LOGIN_CREDENTIAL_STORE_UNAVAILABLE": Remediation(
|
|
1775
|
+
lines=(
|
|
1776
|
+
"The selected credential store could not be accessed. The message names the "
|
|
1777
|
+
"backend and operation when available.",
|
|
1778
|
+
"For an existing macOS Keychain login, unlock your login keychain and allow access "
|
|
1779
|
+
"to the md.mostlyright.cli item, then retry the original command. mr-data requests "
|
|
1780
|
+
"its own device credential, not other saved passwords.",
|
|
1781
|
+
"The secure-file flag cannot switch an existing Keychain login. To move to file "
|
|
1782
|
+
"storage, restore access and run mr-data auth logout successfully, then mr-data login.",
|
|
1783
|
+
),
|
|
1784
|
+
),
|
|
1785
|
+
"LOGIN_CREDENTIAL_STORE_CONFLICT": Remediation(
|
|
1786
|
+
lines=(
|
|
1787
|
+
"The requested storage differs from the existing login's storage. Nothing was "
|
|
1788
|
+
"moved to another store.",
|
|
1789
|
+
"Retry without --credential-store to use the existing store. On macOS/Linux, to use "
|
|
1790
|
+
"private-file storage instead, first run mr-data auth logout successfully, then "
|
|
1791
|
+
"mr-data login.",
|
|
1792
|
+
),
|
|
1793
|
+
),
|
|
1794
|
+
"LOGIN_CREDENTIAL_LOGIN_PENDING": Remediation(
|
|
1795
|
+
lines=(
|
|
1796
|
+
"A previous device sign-in is still recorded as pending. It may still be running.",
|
|
1797
|
+
"Finish or stop that sign-in first. If it has stopped, run "
|
|
1798
|
+
"mr-data auth recover --local-only to clear its pending record before retrying. "
|
|
1799
|
+
"This does not prove that any remotely issued device key was revoked.",
|
|
1800
|
+
),
|
|
1801
|
+
),
|
|
1802
|
+
"LOGIN_CREDENTIAL_RECOVERY_REQUIRED": Remediation(
|
|
1803
|
+
lines=(
|
|
1804
|
+
"The stored login has an unfinished operation. Starting another sign-in would "
|
|
1805
|
+
"lose its recovery record.",
|
|
1806
|
+
"Run mr-data auth recover and follow its result before retrying sign-in.",
|
|
1807
|
+
),
|
|
1808
|
+
),
|
|
1773
1809
|
# `mr-data` narrows a credential directory it already owns, so what is left for this code to
|
|
1774
1810
|
# report is something it may not touch on its own.
|
|
1775
1811
|
"LOGIN_CREDENTIAL_STORE_PERMISSIONS": Remediation(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/canonical.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/key_seam.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/page_coverage.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/session_probes.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/skill_assets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/table_manifest.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/acquire.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/activity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/approvals.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/categories.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/download.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/narrative.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/parity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/probe.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/propose.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/research.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/router.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/runs.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/session.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/stream.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/transport.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/user_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_catalog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_datasets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_handoff.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_query.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_runs.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_secrets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_stream.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/v4_tables.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/thin/vocabulary.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/attendance.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/clarification.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/cloud_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/path_kind.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/plain_file.py
RENAMED
|
File without changes
|
{mostlyright_data-0.20.0 → mostlyright_data-0.20.2}/src/mostlyright/data_harness/ux/render.py
RENAMED
|
File without changes
|