mostlyright-data 0.31.12__tar.gz → 0.31.14__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 (140) hide show
  1. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/PKG-INFO +8 -1
  2. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/README.md +7 -0
  3. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/commands.md +1 -1
  4. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/patterns.md +3 -2
  5. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/readers.md +12 -2
  6. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/sources.md +4 -0
  7. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/recipe.py +2 -2
  8. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/router.py +2 -0
  9. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/runs.py +2 -0
  10. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/session.py +28 -1
  11. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/stream.py +2 -0
  12. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/support.py +32 -8
  13. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/table_search_contract.json +8 -2
  14. mostlyright_data-0.31.14/src/mostlyright/data_harness/thin/user_agent.py +234 -0
  15. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4.py +2 -2
  16. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_datasets.py +19 -6
  17. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_public.py +3 -1
  18. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_query.py +95 -6
  19. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_reader.py +26 -0
  20. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_search.py +3 -1
  21. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/attendance.py +11 -6
  22. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/cloud_auth.py +2 -2
  23. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/commands/clarify.py +10 -5
  24. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/login.py +7 -2
  25. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/remediation.py +12 -1
  26. mostlyright_data-0.31.12/src/mostlyright/data_harness/thin/user_agent.py +0 -76
  27. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/.gitignore +0 -0
  28. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/pyproject.toml +0 -0
  29. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/scripts/hatch_build.py +0 -0
  30. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/SKILL.md +0 -0
  31. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/agents/openai.yaml +0 -0
  32. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  33. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  34. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  35. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  36. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  37. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  38. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  39. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  40. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  41. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/agent-protocol.md +0 -0
  42. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  43. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  44. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/boundaries.md +0 -0
  45. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  46. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  47. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
  48. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/installation-parity.md +0 -0
  49. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/live-run.md +0 -0
  50. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  51. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  52. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  53. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/one-install.md +0 -0
  54. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/prediction-labels.md +0 -0
  55. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  56. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/promote.md +0 -0
  57. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/receipts.md +0 -0
  58. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/recipe-test.md +0 -0
  59. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  60. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  61. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/reference-pages.md +0 -0
  62. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/required-protocol.md +0 -0
  63. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/source-credentials.md +0 -0
  64. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  65. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/transforms.md +0 -0
  66. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  67. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  68. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  69. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/__init__.py +0 -0
  70. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  71. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/canonical.py +0 -0
  72. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/formats.py +0 -0
  73. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  74. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/key_seam.py +0 -0
  75. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/page_coverage.py +0 -0
  76. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  77. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/session_probes.py +0 -0
  78. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/skill_assets.py +0 -0
  79. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/table_manifest.py +0 -0
  80. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  81. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  82. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  83. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/activity.py +0 -0
  84. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  85. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/backoff.py +0 -0
  86. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/categories.py +0 -0
  87. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/commands.py +0 -0
  88. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  89. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/delivery.py +0 -0
  90. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/description_shape.py +0 -0
  91. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/download.py +0 -0
  92. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
  93. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  94. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
  95. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  96. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/parity.py +0 -0
  97. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/probe.py +0 -0
  98. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  99. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  100. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/propose.py +0 -0
  101. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  102. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
  103. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  104. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  105. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
  106. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
  107. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/release_check.py +0 -0
  108. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/research.py +0 -0
  109. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  110. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
  111. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  112. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  113. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/table_view.py +0 -0
  114. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/transport.py +0 -0
  115. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  116. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  117. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  118. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  119. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  120. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  121. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
  122. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  123. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  124. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
  125. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  126. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  127. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  128. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  129. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  130. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  131. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  132. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  133. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  134. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  135. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  136. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  137. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  138. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  139. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  140. {mostlyright_data-0.31.12 → mostlyright_data-0.31.14}/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.31.12
3
+ Version: 0.31.14
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/
@@ -46,6 +46,13 @@ Once a day, after a command has printed its answer, `mr-data` asks the Python pa
46
46
  a newer release is published and says so in one line on stderr. `MOSTLYRIGHT_RELEASE_CHECK=0`
47
47
  switches that off.
48
48
 
49
+ Every call to Mostly Right names the coding agent running `mr-data` in `X-Mostlyright-Agent`
50
+ (`claude-code`, `codex`, `gemini-cli`, `cursor`, `ci` or `none`, read from the variables those
51
+ hosts set), and a program that imports this package sends it too. An `mr-data` command also sends
52
+ one random id in `X-Mostlyright-Invocation`, so the requests of one command can be grouped; a
53
+ library call sends none. Neither is sent to storage hosts. `MOSTLYRIGHT_NO_AGENT_HEADERS=1`
54
+ switches both off.
55
+
49
56
  ## Sign in
50
57
 
51
58
  ```bash
@@ -31,6 +31,13 @@ Once a day, after a command has printed its answer, `mr-data` asks the Python pa
31
31
  a newer release is published and says so in one line on stderr. `MOSTLYRIGHT_RELEASE_CHECK=0`
32
32
  switches that off.
33
33
 
34
+ Every call to Mostly Right names the coding agent running `mr-data` in `X-Mostlyright-Agent`
35
+ (`claude-code`, `codex`, `gemini-cli`, `cursor`, `ci` or `none`, read from the variables those
36
+ hosts set), and a program that imports this package sends it too. An `mr-data` command also sends
37
+ one random id in `X-Mostlyright-Invocation`, so the requests of one command can be grouped; a
38
+ library call sends none. Neither is sent to storage hosts. `MOSTLYRIGHT_NO_AGENT_HEADERS=1`
39
+ switches both off.
40
+
34
41
  ## Sign in
35
42
 
36
43
  ```bash
@@ -17,7 +17,7 @@ run in full.
17
17
  | `mr-data clarify` | Check whether clarification is allowed. Use `--attended` for an active user chat and `--unattended` for a scheduled invocation; an explicit operator veto such as `MOSTLYRIGHT_ATTENDED=0` still wins. A non-TTY subprocess does not prove the user is absent. The command does not ask the person in chat or wait for a reply; the agent must do that. It exits non-zero when nobody can answer or the recipe is already registered. |
18
18
  | `mr-data probe` | `probe SOURCE_ID QUESTION_ID --dataset DATASET_ID [--kind source_inspect\|sample_rows\|profile_columns\|evaluate_expression]` asks one already-registered source one question and prints the answer. **It rides the frozen `/v3/sessions` routes, which a deployment may have switched off, so do not plan a source inspection around it.** Both positionals are identifiers. `QUESTION_ID` is a question identifier, which no v4 registration receipt returns. The v4 way to look at a source before registering it is `mr-data source inspect`, in the next row. The other way to read one is a bounded run of a scratch table, never of the real table, as [the probe rule](3-probe-read-a-source-before-committing-to-it.md#the-probe-rule-test-on-a-scratch-table-never-on-the-real-table) says. |
19
19
  | `mr-data source` | `source inspect` takes a bounded look at one source, and it is the v4 path where `probe` is not. [Stage 3](3-probe-read-a-source-before-committing-to-it.md) says when to use it. It starts one of three ways, and each prints the `session_id`, `probe_id` and `source_id`, plus the `dataset_id` and `question_id` when it has them. For a new public HTTPS CSV that needs no key, give `--dataset-id`, `--question`, `--source-name`, `--source-url`, `--claimed-basis` and `--claim-evidence-digest`: it prepares the source and opens a session. For a source already prepared, give `--dataset-id`, `--question-id` and `--source-id`, which opens a session. In a session you already hold, give `--session-id` with `--source-id`. `--context-source-id` adds a source to a session it opens. `--session-id` with `--probe-id` reads the answer back. A look is bounded to 65,536 bytes, 1,000 rows and 300 seconds. `queued` and `truncated` are not complete answers, and `capability_unavailable` means this deployment cannot inspect, so read the source by a bounded run of a scratch table instead, as [the probe rule](3-probe-read-a-source-before-committing-to-it.md#the-probe-rule-test-on-a-scratch-table-never-on-the-real-table) says. What it finds is exploratory evidence, not a run and not a receipt. |
20
- | `mr-data reader` | `reader normalize --family-id ID --family-version VERSION --options-file FILE [--workspace-id ID]` asks Studio's Reader service to apply one certified family's own defaults and option rules to a JSON object of options, at most 64 KiB, and prints the normalized `decode_options` and `decode_options_json`. The second is the text a recipe carries as `reader.decode_options`, pinned as [Readers](readers.md) describes. It changes nothing, and this client does not validate or invent a family's options: Studio decides. Run it for every Reader pin before you register, so a wrong option is refused here and not inside a run. |
20
+ | `mr-data reader` | `reader normalize --family-id ID --family-version VERSION --options-file FILE [--workspace-id ID]` asks Studio's Reader service to apply one certified family's own defaults and option rules to a JSON object of options, at most 64 KiB, and prints the normalized `decode_options`, `decode_options_json` and `options_digest`. The second is the text a recipe carries as `reader.decode_options`, and the third is the `reader.options_digest` registration accepts for exactly those options, pinned as [Readers](readers.md) describes. It changes nothing, and this client does not validate or invent a family's options: Studio decides. Run it for every Reader pin before you register, so a wrong option is refused here and not inside a run. |
21
21
  | `mr-data catalog` | `catalog search "QUESTION"` asks the sealed public-source catalogue which of its entries might answer a question and ranks them best first. Every ranked entry comes back with the disposition its own facts earned — `admitted`, `human_escalation_required` or `refused` — because an entry the catalogue could not vouch for is still a finding. `--format csv` states the one data format the question requires — one token per question, never repeated — and it is not a filter: an entry that does not declare that format still comes back ranked, with disposition `refused` and `filters_match` false, so nothing is held back; `--limit N` ranks at most N, up to 25. It fetches nothing and registers nothing. The catalogue holds one provider (Data.gov) and only part of it, so it is never exhaustive and an empty answer is not evidence that no such source exists. When the answer is `catalog_unavailable`, this deployment has no catalogue to search: record the lane and carry on. |
22
22
  | `mr-data public` | Read Mostly Right's public dataset catalogue. `public search "QUERY"` ranks published datasets (`--topic`, `--limit` and `--cursor` narrow and page it), `show SLUG` reads one overview, `tables SLUG` lists its table identifiers, `schema TABLE_ID` reads one table's schema, `sample TABLE_ID` prints its published preview rows, `lookup "QUERY"` is the connector search shape and `fetch SLUG` returns the dataset's Markdown document. Those seven need no login. `query TABLE_ID`, with `--body` or `--body-file`, and `download-url TABLE_ID` do need `mr-data login`: they ask Cloud, with the device key, to run one query over a public table and to say how it can be downloaded. Preview rows are not a query, and no table bytes pass through the command. |
23
23
  | `mr-data connected` | `connected list` shows the public datasets connected to this workspace and `connected add SLUG` connects one by its slug. It needs `mr-data login`: both ask Cloud, with the device key, over the routes MCP's `list_connected_datasets` and `connect_dataset` use. |
@@ -225,8 +225,9 @@ for the days that can still change, through the two request `parameters` it rend
225
225
  unit reads its own page count, so size `limits.max_requests` from a measured unit.
226
226
  [Paging a keyed API in parallel](sources.md#paging-a-keyed-api-in-parallel) has the rules.
227
227
 
228
- `options_digest` is the SHA-256 of the canonical JSON of `decode_options`. Registration names it
229
- when the two disagree.
228
+ `options_digest` is the SHA-256 of the canonical JSON of `decode_options`, and
229
+ `mr-data reader normalize` prints it for the options it returns. Registration names the digest it
230
+ expected when the two disagree.
230
231
 
231
232
  ```json
232
233
  {
@@ -57,9 +57,17 @@ mr-data reader normalize --family-id json.tabular --family-version 1.1.0 \
57
57
 
58
58
  `options.json` holds the keys the source needs, at most 64 KiB. Studio applies the family's own
59
59
  defaults and option rules. The answer's `decode_options_json` is the text to write as
60
- `reader.decode_options`. A refusal arrives here, before any run is spent. Run it for every
60
+ `reader.decode_options`, and its `options_digest` is the `reader.options_digest` to pin beside it.
61
+ That digest binds the default-filled options the answer returns, not the subset in `options.json`,
62
+ so copy both together. A refusal arrives here, before any run is spent. Run it for every
61
63
  Reader pin before you register.
62
64
 
65
+ `normalize` checks the options against the family, not against the place the pin goes. A
66
+ Generic API `generic_api.response.reader` admits fewer coordinates: `json.tabular` `1.0.0`,
67
+ `1.1.0` or `1.2.0`, `delimited_text` `1.0.0` or `1.1.0`, and `parquet.tabular` `1.0.0`.
68
+ `json.tabular` `1.3.0` and `1.4.0` normalize but are refused there. The highest-version advice
69
+ below applies inside that set.
70
+
63
71
  A column that may be absent from a record must state `"required": false`. Publishers routinely omit
64
72
  a key rather than sending null, and a required column that is missing refuses the record.
65
73
 
@@ -87,6 +95,7 @@ decoder. Zero matches and multiple matches both refuse.
87
95
  | `archive.zip@1.0.0` | `member`; `member_format` (`csv` or `tsv`); optional `header: true`. |
88
96
  | `archive.zip@1.1.0` | Same, plus `member_format: semicolon` for publishers such as DWD. |
89
97
  | `archive.zip@1.2.0` | Same as `archive.zip@1.1.0`; also accepts a response served as `application/octet-stream`. |
98
+ | `archive.zip@4.0.0` | Exact safe `member`, `member_format: lines` and `encoding: utf-8 or cp1252`; no header. Streams the member's physical lines as line_number, raw_line, as `archive.tar@2.0.0` does for one member. |
90
99
  | `archive.gzip@1.0.0` | `member_format`; optional `header: true`. `member` is invalid. |
91
100
  | `archive.gzip@1.1.0` | Same; also accepts a response served as `application/octet-stream`. |
92
101
  | `archive.tar@1.0.0` | Same as `archive.zip@1.0.0`. |
@@ -181,7 +190,8 @@ Use `public.https.api@1.0.0` only when `keyless_window` is literal `true`. Other
181
190
  The new adapter uses the existing `generic_api.response` Reader contract. For Binance klines,
182
191
  state `format: json`, `records_pointer: ""`, and `reader.family_id: json.tabular` with
183
192
  `family_version: 1.0.0` (or the already admitted `1.1.0`). Pin `options_digest` to the SHA-256
184
- of the canonical decode options, rather than inserting a placeholder digest. These decode
193
+ of the canonical decode options, the `options_digest` `mr-data reader normalize` prints, rather
194
+ than inserting a placeholder digest. These decode
185
195
  options project a root JSON array of arrays, with RFC 6901 numeric indexes into each row:
186
196
 
187
197
  ```json
@@ -335,6 +335,10 @@ to separate them. No per-row collection may derive its rows from a `closed` or `
335
335
  `pagination` is a `page` or `offset` walk with a required `parallel` member, in one of two forms.
336
336
  Prefer the counted form.
337
337
 
338
+ `@2.1.0` refuses `{"kind": "none", "max_pages": 1}`, and `cursor` and `link` walks too. For one
339
+ request, use `@2.0.0`. Use `authenticated.https.connector@3.0.0` instead when the request
340
+ declares a `per_call` cost, which `@2.0.0` does not admit.
341
+
338
342
  | Form | Members | Use it when |
339
343
  | --- | --- | --- |
340
344
  | counted | `total_pages_pointer`, `max_page_bytes`, `min_interval_milliseconds`, optional `concurrency` | the first page states how many pages exist. A `page` walk over a `json` response only |
@@ -170,12 +170,12 @@ FILE_DECODER_COORDINATES: frozenset[tuple[str, str]] = frozenset(
170
170
  for family, versions in {
171
171
  "archive.gzip": ("1.0.0", "1.1.0", "2.0.0", "2.1.0"),
172
172
  "archive.tar": ("1.0.0", "1.1.0", "3.0.0", "4.0.0"),
173
- "archive.zip": ("1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "3.0.0"),
173
+ "archive.zip": ("1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "3.0.0", "4.0.0"),
174
174
  "delimited_text": ("1.0.0", "1.1.0", "2.0.0"),
175
175
  "geoparquet.vector": ("1.0.0",),
176
176
  "json.tabular": ("1.1.0", "1.2.0"),
177
177
  "meteorology.bufr": ("1.0.0",),
178
- "meteorology.grib": ("1.0.0",),
178
+ "meteorology.grib": ("1.0.0", "2.0.0"),
179
179
  "microdata.sas_xpt": ("1.0.0",),
180
180
  "microdata.spss_sav": ("1.0.0",),
181
181
  "microdata.stata_dta": ("1.0.0",),
@@ -52,6 +52,7 @@ from collections.abc import Callable, Mapping, Sequence
52
52
  from typing import Any
53
53
 
54
54
  from mostlyright.data_harness import skill_assets
55
+ from mostlyright.data_harness.thin import user_agent
55
56
  from mostlyright.data_harness.thin.transport import ThinLaneError
56
57
  from mostlyright.data_harness.thin.vocabulary import (
57
58
  CLASSIFICATION,
@@ -781,6 +782,7 @@ def main(argv: Sequence[str] | None = None) -> int:
781
782
  # not write, it never raises, and `MOSTLYRIGHT_SKILL_AUTOINSTALL=0` turns it off. A skill that
782
783
  # arrived only when somebody knew to ask for it helped exactly the people who did not need it.
783
784
  skill_assets.ensure_installed()
785
+ user_agent.begin_invocation()
784
786
 
785
787
  arguments = list(sys.argv[1:] if argv is None else argv)
786
788
  hosted_requested = _hosted_flag_present(arguments)
@@ -46,6 +46,7 @@ from mostlyright.data_harness.thin.transport import (
46
46
  UrlLibTransport,
47
47
  signed_transfer_url,
48
48
  )
49
+ from mostlyright.data_harness.thin.user_agent import agent_headers
49
50
 
50
51
  #: The contract version every payload below is written against. ``studio_boundary`` pins the
51
52
  #: generated client to the same 3.0.0 and to one OpenAPI source digest; this constant is the thin
@@ -279,6 +280,7 @@ class StudioApiClient:
279
280
  headers = {
280
281
  "Accept": "application/json",
281
282
  CLIENT_HEADER: CLIENT_NAME,
283
+ **agent_headers(),
282
284
  }
283
285
  if extra_headers:
284
286
  headers.update(extra_headers)
@@ -42,6 +42,7 @@ from mostlyright.data_harness.thin.transport import (
42
42
  UrlLibTransport,
43
43
  service_origin,
44
44
  )
45
+ from mostlyright.data_harness.thin.user_agent import agent_headers
45
46
  from mostlyright.data_harness.ux.credentials import ResolvedCloudCredentials
46
47
 
47
48
  #: Cloud's token-exchange route. Held identical to ``hosted_deploy.CLOUD_TOKEN_PATH`` by a test.
@@ -396,7 +397,7 @@ def open_studio_session(
396
397
  status, raw, headers = selected.request(
397
398
  "POST",
398
399
  f"{cloud_url}{CLOUD_TOKEN_PATH}",
399
- {"Accept": "application/json", "x-api-key": credentials.raw_key},
400
+ {"Accept": "application/json", "x-api-key": credentials.raw_key, **agent_headers()},
400
401
  b"",
401
402
  MAX_TOKEN_RESPONSE_BYTES,
402
403
  )
@@ -586,6 +587,19 @@ def sessions_for(session: StudioSession | StudioSessionProvider) -> StudioSessio
586
587
  #: a sentence with spaces in it.
587
588
  CLOUD_REFUSAL_CODE = re.compile(r"[A-Z][A-Z0-9_]{0,63}")
588
589
 
590
+ #: Cloud's refusal of a web chat sandbox key on a route that would change something
591
+ #: (Cloud ``lib/sandbox-key-refusal.ts``). The key is valid and reads; every change it
592
+ #: would make goes through the chat's own approval-gated tools or a person on the dashboard.
593
+ #: Folded onto ``THIN_AUTHORIZATION_DENIED`` it told an agent in the web chat to run
594
+ #: ``mr-data login``, which cannot help, so it keeps its own code -- and Cloud's sentence,
595
+ #: which names the chat tool that does the job.
596
+ SANDBOX_KEY_READ_ONLY_CODE = "SANDBOX_KEY_READ_ONLY"
597
+ THIN_SANDBOX_KEY_READ_ONLY = f"THIN_CLOUD_{SANDBOX_KEY_READ_ONLY_CODE}"
598
+ SANDBOX_KEY_READ_ONLY_SENTENCE = (
599
+ "This is a web chat key: it reads, and changes go through the chat's own tools or a "
600
+ "person on the dashboard; signing in again will not change that"
601
+ )
602
+
589
603
  #: The family of Studio's refusals of a call that would skip Cloud's usage meter
590
604
  #: (``DATA_EGRESS_THROUGH_CLOUD``). From a Cloud route it means Studio refused CLOUD's call, which
591
605
  #: is Mostly Right's misconfiguration and never the caller's key.
@@ -843,6 +857,18 @@ def cloud_refusal(
843
857
  "this computer's or the device key's; nothing here needs to change. Run the command "
844
858
  "again later",
845
859
  )
860
+ elif status == 403 and named == SANDBOX_KEY_READ_ONLY_CODE:
861
+ # NOT A MISSING OR EXPIRED KEY, so not the shared authorization code: the chat's
862
+ # key is valid and only this change is not its to make. Cloud's sentence names the
863
+ # tool that makes it, so it is quoted (bounded, printable, attributed) even though a
864
+ # code was named, which no other named refusal here does.
865
+ stated_alternative = _cloud_sentence(parsed.get("error"))
866
+ refusal = ThinLaneError(
867
+ THIN_SANDBOX_KEY_READ_ONLY,
868
+ f"{SANDBOX_KEY_READ_ONLY_SENTENCE}. Cloud said: {stated_alternative}"
869
+ if stated_alternative
870
+ else SANDBOX_KEY_READ_ONLY_SENTENCE,
871
+ )
846
872
  elif status in {401, 403}:
847
873
  if named:
848
874
  suffix = f" (Cloud said {named}, HTTP {status})"
@@ -953,6 +979,7 @@ class CloudClient:
953
979
  headers = {
954
980
  "Accept": "application/json",
955
981
  "Authorization": f"Bearer {self._credentials.raw_key}",
982
+ **agent_headers(),
956
983
  }
957
984
  encoded: bytes | None = None
958
985
  if body is not None:
@@ -49,6 +49,7 @@ from mostlyright.data_harness.thin.session import (
49
49
  stated_sentence,
50
50
  )
51
51
  from mostlyright.data_harness.thin.transport import ThinLaneError, Transport, UrlLibTransport
52
+ from mostlyright.data_harness.thin.user_agent import agent_headers
52
53
 
53
54
  #: Upstream's frame names. The first two are the tick/proof partition; ``end`` is its
54
55
  #: end-of-stream notice.
@@ -593,6 +594,7 @@ class RunEventStream:
593
594
  headers = {
594
595
  "Accept": "text/event-stream",
595
596
  CLIENT_HEADER: CLIENT_NAME,
597
+ **agent_headers(),
596
598
  **session.authorization(),
597
599
  }
598
600
  # Resume in Studio's own cursor space when we have one. `from_seq` is only ever the
@@ -31,7 +31,11 @@ from typing import Any
31
31
 
32
32
  from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX
33
33
  from mostlyright.data_harness.thin.backoff import jittered
34
- from mostlyright.data_harness.thin.session import CloudClient, require_device_key
34
+ from mostlyright.data_harness.thin.session import (
35
+ THIN_SANDBOX_KEY_READ_ONLY,
36
+ CloudClient,
37
+ require_device_key,
38
+ )
35
39
  from mostlyright.data_harness.thin.transport import ThinLaneError, UrlLibTransport
36
40
  from mostlyright.data_harness.ux.credentials import resolve_cloud_credentials
37
41
 
@@ -61,6 +65,14 @@ SETTLED = frozenset({"answered", "budget_exhausted", "failed"})
61
65
  SETTLED_RECEIPTS = frozenset(f"support_{state}" for state in SETTLED)
62
66
  STATUS_VERB = "status"
63
67
 
68
+ #: What a web chat key is told when Cloud refuses it this route. Said here as well as by
69
+ #: Cloud, so an agent in the chat learns the tool even if Cloud's sentence did not survive
70
+ #: the bound on quoted text.
71
+ CHAT_SUPPORT_TOOLS = (
72
+ "In the web chat, ask with the chat's ask_support tool, then read the answer with "
73
+ "get_support_answer"
74
+ )
75
+
64
76
 
65
77
  def declare_arguments(parser: argparse.ArgumentParser) -> None:
66
78
  """Add `ask`'s arguments to the shared hosted parser."""
@@ -106,17 +118,29 @@ def declare_arguments(parser: argparse.ArgumentParser) -> None:
106
118
  )
107
119
 
108
120
 
121
+ def _name_chat_tools(failure: ThinLaneError) -> ThinLaneError:
122
+ """A web chat key refused here is pointed at the chat's own support tools."""
123
+
124
+ if failure.code == THIN_SANDBOX_KEY_READ_ONLY and "ask_support" not in failure.detail:
125
+ failure.detail = f"{failure.detail}. {CHAT_SUPPORT_TOOLS}"
126
+ failure.args = (f"{failure.code}: {failure.detail}",)
127
+ return failure
128
+
129
+
109
130
  class SupportClient(CloudClient):
110
131
  """The two support routes over the shared device-key Cloud client."""
111
132
 
112
133
  def submit(self, body: Mapping[str, Any]) -> dict[str, Any]:
113
- return self.call(
114
- "POST",
115
- CLOUD_SUPPORT_QUESTIONS_PATH,
116
- act="ask the support agent a question",
117
- body=body,
118
- expected=(202,),
119
- )
134
+ try:
135
+ return self.call(
136
+ "POST",
137
+ CLOUD_SUPPORT_QUESTIONS_PATH,
138
+ act="ask the support agent a question",
139
+ body=body,
140
+ expected=(202,),
141
+ )
142
+ except ThinLaneError as failure:
143
+ raise _name_chat_tools(failure) from None
120
144
 
121
145
  def read(self, question_id: str, *, run: bool) -> dict[str, Any]:
122
146
  suffix = "" if run else "?wait=0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": "mostlyright-thin-table-search-contract.v1",
3
- "source_commit": "5109da1f92eaaf4fc41ed35a03e5d010268d7c7b",
3
+ "source_commit": "b296c21484f40cce334e1b7c0f6c7c03b44a41e9",
4
4
  "recipe_search_definition": "recipe_search",
5
5
  "$defs": {
6
6
  "common.safe_integer": {
@@ -234,6 +234,12 @@
234
234
  "asc",
235
235
  "desc"
236
236
  ]
237
+ },
238
+ "quality": {
239
+ "description": "Optional owner quality note (4.55.0). A string of 1 to 200 characters. Shown on the order question only when this phrase occurs in the query. It does not change the sort. A boolean is invalid.",
240
+ "type": "string",
241
+ "minLength": 1,
242
+ "maxLength": 200
237
243
  }
238
244
  },
239
245
  "required": [
@@ -242,7 +248,7 @@
242
248
  "direction"
243
249
  ]
244
250
  },
245
- "description": "Order phrases (4.45.0: at most 256), one package order key each, under the aliases' shared byte budget."
251
+ "description": "Order phrases (4.45.0: at most 256), one package order key each, under the aliases' shared byte budget. Optional quality (4.55.0) is the owner's note for that phrase."
246
252
  },
247
253
  "default_order": {
248
254
  "description": "The order of a search with nothing to rank by (4.44.0): a browse or an entity plan with no order of its own walks the rows in this order instead of the package's row order (popularity). Mojirama's `known` (vote count descending) for a film table; the column must be sortable (declared sortable, a sort alias's, or the popularity or votes role).",
@@ -0,0 +1,234 @@
1
+ """The identity every first-party request to Mostly Right states.
2
+
3
+ Without it the standard library sends ``Python-urllib/3.x``, which is what every unattended
4
+ script on the internet sends: the CLI's own traffic could not be told apart from anything else in
5
+ a server log, so nothing downstream could measure it, rate-limit it separately, or exempt it.
6
+
7
+ The name follows the convention the two crawler lanes already use -- a product token, a version,
8
+ and the contact segment that says where to reach the people running it -- so an origin and our own
9
+ logs see one family of identities from this deployment rather than one per code path.
10
+
11
+ Two more headers ride beside it on first-party calls only (:func:`agent_headers`): which coding
12
+ agent, if any, is driving this process, and, under the ``mr-data`` command, one id shared by every
13
+ request that command makes. They go where ``X-Mostlyright-Client`` goes and on Cloud's own
14
+ routes, never through the transport, for the reason ``thin.runs.CLIENT_HEADER`` gives: the
15
+ transport also carries signed transfers to storage nobody here owns.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import secrets
22
+ from collections.abc import Mapping
23
+ from functools import cache
24
+ from importlib.metadata import PackageNotFoundError, version
25
+ from typing import NamedTuple
26
+
27
+ #: The contact segment ``hosted_worker.CRAWLER_CONTACT_SEGMENT`` states for the keyed-source
28
+ #: agents and ``hosted_crawler_fetch.PUBLIC_HTTPS_USER_AGENT`` carries for public sources, with
29
+ #: the measurement that makes it load-bearing recorded at the first of those.
30
+ #:
31
+ #: It is spelled again here rather than imported because both of those modules import ``pyarrow``
32
+ #: at module scope and this package installs with no compiled extension at all -- and they are
33
+ #: Studio's now, so importing them is not a choice this repository has. The spellings are held
34
+ #: against each other by ``tests/test_thin_user_agent.py`` under
35
+ #: ``make verify-cross-repo STUDIO_ROOT=...``, which reads the other one out of Studio's source.
36
+ CRAWLER_CONTACT_SEGMENT = "(+https://mostlyright.md/crawler; crawler@mostlyright.md)"
37
+
38
+ #: The distribution this client is installed as, and the product token it therefore reports.
39
+ DISTRIBUTION_NAME = "mostlyright-data"
40
+
41
+ #: The version reported from a source tree, where no distribution metadata exists to read. A
42
+ #: number rather than a word, so every value this header can carry parses the same way.
43
+ UNINSTALLED_VERSION = "0.0.0"
44
+
45
+ _USER_AGENT_HEADER = "User-Agent"
46
+
47
+ #: The header naming the agent driving this process, one of :data:`AGENTS`.
48
+ AGENT_HEADER = "X-Mostlyright-Agent"
49
+
50
+ #: The header carrying :func:`invocation_id`, sent only after :func:`begin_invocation`.
51
+ INVOCATION_HEADER = "X-Mostlyright-Invocation"
52
+
53
+ #: Set to anything but ``0`` or empty, and neither header above is sent.
54
+ NO_AGENT_HEADERS_ENV = "MOSTLYRIGHT_NO_AGENT_HEADERS"
55
+
56
+ AGENT_CLAUDE_CODE = "claude-code"
57
+ AGENT_CODEX = "codex"
58
+ AGENT_GEMINI_CLI = "gemini-cli"
59
+ AGENT_CURSOR = "cursor"
60
+ AGENT_CI = "ci"
61
+ AGENT_NONE = "none"
62
+
63
+
64
+ class AgentMarker(NamedTuple):
65
+ """One variable an agent host exports into the commands it runs.
66
+
67
+ ``conversation`` says whether the attendance check (``ux.attendance``) reads the variable as
68
+ a conversation existing. Only a marker every headless run of that host lacks may say so:
69
+ ``CODEX_SANDBOX`` is set by ``codex exec`` on a cron job too, so it names Codex here and is
70
+ not read as a person being there.
71
+ """
72
+
73
+ agent: str
74
+ variable: str
75
+ conversation: bool
76
+
77
+
78
+ #: Every marker, grouped by agent in precedence order.
79
+ #:
80
+ #: Presence of a non-empty value is the signal and the value is never read: these are the hosts'
81
+ #: variables, and their values are theirs to change. A coding agent outranks ``ci``, so
82
+ #: ``claude -p`` inside a GitHub Action reports ``claude-code``. Nothing that every terminal of an
83
+ #: editor exports is listed, because a person typing there would read as the agent.
84
+ AGENT_MARKERS: tuple[AgentMarker, ...] = (
85
+ AgentMarker(AGENT_CLAUDE_CODE, "CLAUDECODE", True),
86
+ AgentMarker(AGENT_CLAUDE_CODE, "CLAUDE_CODE_SESSION", True),
87
+ AgentMarker(AGENT_CODEX, "CODEX_SESSION", True),
88
+ AgentMarker(AGENT_CODEX, "CODEX_SANDBOX", False),
89
+ AgentMarker(AGENT_CODEX, "CODEX_SANDBOX_NETWORK_DISABLED", False),
90
+ AgentMarker(AGENT_GEMINI_CLI, "GEMINI_CLI", False),
91
+ AgentMarker(AGENT_CURSOR, "CURSOR_AGENT", False),
92
+ AgentMarker(AGENT_CI, "CI", False),
93
+ AgentMarker(AGENT_CI, "GITHUB_ACTIONS", False),
94
+ )
95
+
96
+ #: Every value :data:`AGENT_HEADER` can carry.
97
+ AGENTS: tuple[str, ...] = (*dict.fromkeys(marker.agent for marker in AGENT_MARKERS), AGENT_NONE)
98
+
99
+ #: What :func:`invocation_id` returns: 16 random bytes as 32 lowercase hex digits.
100
+ INVOCATION_ID_PATTERN = r"^[0-9a-f]{32}$"
101
+
102
+ _invocation: str | None = None
103
+
104
+
105
+ @cache
106
+ def client_user_agent() -> str:
107
+ """Return the ``User-Agent`` this client presents to Mostly Right.
108
+
109
+ Cached because the answer is a property of the installed environment, and reading distribution
110
+ metadata walks ``sys.path``; a command that makes twenty calls would otherwise pay for it
111
+ twenty times.
112
+ """
113
+
114
+ try:
115
+ installed = version(DISTRIBUTION_NAME)
116
+ except PackageNotFoundError:
117
+ installed = UNINSTALLED_VERSION
118
+ return f"{DISTRIBUTION_NAME}/{installed} {CRAWLER_CONTACT_SEGMENT}"
119
+
120
+
121
+ def with_user_agent(headers: Mapping[str, str]) -> dict[str, str]:
122
+ """Return ``headers`` as a new dict carrying this client's identity.
123
+
124
+ A caller that stated its own ``User-Agent`` keeps it, under whatever spelling it used: HTTP
125
+ header names are case-insensitive, so adding a second one under a different capitalization
126
+ would send two identities rather than override one.
127
+ """
128
+
129
+ resolved = dict(headers)
130
+ if any(name.lower() == "user-agent" for name in resolved):
131
+ return resolved
132
+ resolved[_USER_AGENT_HEADER] = client_user_agent()
133
+ return resolved
134
+
135
+
136
+ def agent_env(agent: str) -> tuple[str, ...]:
137
+ """Return the variables that name ``agent``; empty for ``none`` and for a name not listed."""
138
+
139
+ return tuple(marker.variable for marker in AGENT_MARKERS if marker.agent == agent)
140
+
141
+
142
+ def conversation_env() -> tuple[str, ...]:
143
+ """Return the markers the attendance check reads as a conversation existing."""
144
+
145
+ return tuple(marker.variable for marker in AGENT_MARKERS if marker.conversation)
146
+
147
+
148
+ def first_set(names: tuple[str, ...], environ: Mapping[str, str] | None = None) -> str | None:
149
+ """Return the first of ``names`` set to a non-empty value, or ``None``."""
150
+
151
+ table = os.environ if environ is None else environ
152
+ return next((name for name in names if (table.get(name) or "").strip()), None)
153
+
154
+
155
+ def detect_agent(environ: Mapping[str, str] | None = None) -> str:
156
+ """Return the agent driving this process, one of :data:`AGENTS`."""
157
+
158
+ table = os.environ if environ is None else environ
159
+ for marker in AGENT_MARKERS:
160
+ if (table.get(marker.variable) or "").strip():
161
+ return marker.agent
162
+ return AGENT_NONE
163
+
164
+
165
+ def begin_invocation() -> str:
166
+ """Draw a fresh invocation id for the command starting now, and return it.
167
+
168
+ Called by the ``mr-data`` entry point once per command. A program importing this package
169
+ never calls it, so its requests carry no invocation id rather than one that lasts as long as
170
+ the process does.
171
+ """
172
+
173
+ global _invocation
174
+ _invocation = secrets.token_hex(16)
175
+ return _invocation
176
+
177
+
178
+ def end_invocation() -> None:
179
+ """Forget the current invocation id, so later requests carry none."""
180
+
181
+ global _invocation
182
+ _invocation = None
183
+
184
+
185
+ def invocation_id() -> str | None:
186
+ """Return the current command's id (see :data:`INVOCATION_ID_PATTERN`), or ``None``."""
187
+
188
+ return _invocation
189
+
190
+
191
+ def agent_headers(environ: Mapping[str, str] | None = None) -> dict[str, str]:
192
+ """Return the agent and invocation headers, or none when :data:`NO_AGENT_HEADERS_ENV` is set.
193
+
194
+ The invocation header is present only inside a command :func:`begin_invocation` started. A
195
+ first-party call adds these itself; the transport never does.
196
+ """
197
+
198
+ table = os.environ if environ is None else environ
199
+ if (table.get(NO_AGENT_HEADERS_ENV) or "").strip() not in ("", "0"):
200
+ return {}
201
+ headers = {AGENT_HEADER: detect_agent(table)}
202
+ if _invocation is not None:
203
+ headers[INVOCATION_HEADER] = _invocation
204
+ return headers
205
+
206
+
207
+ __all__ = [
208
+ "AGENTS",
209
+ "AGENT_CI",
210
+ "AGENT_CLAUDE_CODE",
211
+ "AGENT_CODEX",
212
+ "AGENT_CURSOR",
213
+ "AGENT_GEMINI_CLI",
214
+ "AGENT_HEADER",
215
+ "AGENT_MARKERS",
216
+ "AGENT_NONE",
217
+ "CRAWLER_CONTACT_SEGMENT",
218
+ "DISTRIBUTION_NAME",
219
+ "INVOCATION_HEADER",
220
+ "INVOCATION_ID_PATTERN",
221
+ "NO_AGENT_HEADERS_ENV",
222
+ "UNINSTALLED_VERSION",
223
+ "AgentMarker",
224
+ "agent_env",
225
+ "agent_headers",
226
+ "begin_invocation",
227
+ "client_user_agent",
228
+ "conversation_env",
229
+ "detect_agent",
230
+ "end_invocation",
231
+ "first_set",
232
+ "invocation_id",
233
+ "with_user_agent",
234
+ ]
@@ -149,8 +149,8 @@ from mostlyright.data_harness.thin.v4_matching import CONTRACT_PATHS as MATCHING
149
149
  #: ``JOB_INVALID`` outright, which in production was every refresh a collection epoch was offered
150
150
  #: for rather than only the collection runs. This package must not reach a Studio older than the
151
151
  #: commit it pins; ``docs/V4-WORKER-PROTOCOL.md`` states it beside the layout's own ordering rule.
152
- PINNED_V4_OPENAPI_SOURCE_SHA256 = "e7bd4e994b32d62ae00b4d7fc2f8936e4964cd4160b7aa30ae010528ff57b1a8"
153
- PINNED_V4_CONTRACT_VERSION = "4.54.0"
152
+ PINNED_V4_OPENAPI_SOURCE_SHA256 = "b378d4f70d580aed094b00edeec5acc39cae5c79f19dc4e869fe40304fccf654"
153
+ PINNED_V4_CONTRACT_VERSION = "4.57.0"
154
154
 
155
155
  #: The ``schema_version`` every command body that predates 4.14.0 stamps: ``dataset activity``,
156
156
  #: ``table rename``, ``table archive`` and ``dataset archive``. It is the number the 0.25 line
@@ -136,6 +136,7 @@ from mostlyright.data_harness.thin.session import (
136
136
  CLOUD_REFUSAL_CODE,
137
137
  MAX_JSON_RESPONSE_BYTES,
138
138
  MAX_STATED_MESSAGE_CHARS,
139
+ THIN_SANDBOX_KEY_READ_ONLY,
139
140
  CloudClient,
140
141
  StudioSession,
141
142
  StudioSessionProvider,
@@ -1615,18 +1616,30 @@ def _sync_written_identity(dataset_id: str, cloud_url: str) -> dict[str, Any]:
1615
1616
  )
1616
1617
  return {"public_projection_synced": True}
1617
1618
  except (ThinLaneError, LoginError, OSError) as error:
1618
- return {
1619
+ stated = isinstance(error, ThinLaneError) and getattr(error, "http_status", None) != 404
1620
+ result: dict[str, Any] = {
1619
1621
  "public_projection_synced": False,
1620
- "public_projection_error": (
1621
- error.code
1622
- if isinstance(error, ThinLaneError) and getattr(error, "http_status", None) != 404
1623
- else "THIN_IDENTITY_SYNC_UNAVAILABLE"
1624
- ),
1622
+ "public_projection_error": (error.code if stated else "THIN_IDENTITY_SYNC_UNAVAILABLE"),
1625
1623
  "public_projection_note": (
1626
1624
  "The dataset was saved, but its public page could not be updated. "
1627
1625
  f"Retry only the sync with: mr-data dataset sync {dataset_id}"
1628
1626
  ),
1629
1627
  }
1628
+ # ⚠ THE CODE ALONE SENT PEOPLE TO THE WRONG FIX. Every refusal on this path left
1629
+ # as a bare code, so a key Cloud would never let update the page and an expired one
1630
+ # read the same. The refusal's own sentence -- this lane's words, with Cloud's
1631
+ # quoted inside them already bounded and printable by ``cloud_refusal`` -- rides
1632
+ # along. Nothing branches on it; the code beside it is what a script reads.
1633
+ if stated:
1634
+ result["public_projection_message"] = error.detail
1635
+ if stated and error.code == THIN_SANDBOX_KEY_READ_ONLY:
1636
+ # Retrying with this key is refused the same way: the chat does it.
1637
+ result["public_projection_note"] = (
1638
+ "The dataset was saved, but this web chat key cannot update its public "
1639
+ "page. Use the chat's update_dataset tool with sync_only: true, or a "
1640
+ "person syncs it from the dashboard."
1641
+ )
1642
+ return result
1630
1643
 
1631
1644
 
1632
1645
  def _held(record: Mapping[str, Any], member: str) -> Any: