sayfirst-cli 0.3.0__tar.gz → 0.3.1__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 (110) hide show
  1. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/CHANGELOG.md +19 -0
  2. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/PKG-INFO +27 -27
  3. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/QUICKSTART.md +182 -85
  4. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/README.md +24 -24
  5. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/PACKS.md +13 -9
  6. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/pyproject.toml +3 -3
  7. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/harness.py +69 -8
  8. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/interpreter.py +13 -2
  9. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_instrument_verify.py +143 -2
  10. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_interpreter_target.py +56 -0
  11. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_quickstart_names_what_exists.py +24 -0
  12. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/.github/workflows/ci.yml +0 -0
  13. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/.github/workflows/release.yml +0 -0
  14. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/.gitignore +0 -0
  15. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/LICENSE +0 -0
  16. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/NOTICE +0 -0
  17. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/SECURITY.md +0 -0
  18. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/TRADEMARKS.md +0 -0
  19. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/EVIDENCE-SURFACE.md +0 -0
  20. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/PARTITION.md +0 -0
  21. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/PROVENANCE.md +0 -0
  22. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/publication-checklist.md +0 -0
  23. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/check_dependency_closure.py +0 -0
  24. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/check_developer_certificate_of_origin.py +0 -0
  25. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/gate.sh +0 -0
  26. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/release_version.py +0 -0
  27. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/__init__.py +0 -0
  28. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/approvals.py +0 -0
  29. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/ask.py +0 -0
  30. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/evidence.py +0 -0
  31. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/exit_codes.py +0 -0
  32. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/explain.py +0 -0
  33. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/__init__.py +0 -0
  34. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/_bootstrap/sitecustomize.py +0 -0
  35. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/commands.py +0 -0
  36. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/designation.py +0 -0
  37. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/engine.py +0 -0
  38. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/follow.py +0 -0
  39. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/launch.py +0 -0
  40. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/manifest.py +0 -0
  41. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/verify.py +0 -0
  42. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/main.py +0 -0
  43. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/__init__.py +0 -0
  44. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/database/NOTE.md +0 -0
  45. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/database/interpose.py +0 -0
  46. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/database/pack.toml +0 -0
  47. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/http-client/NOTE.md +0 -0
  48. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/http-client/interpose.py +0 -0
  49. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/http-client/pack.toml +0 -0
  50. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/subprocess/NOTE.md +0 -0
  51. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/subprocess/interpose.py +0 -0
  52. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/subprocess/pack.toml +0 -0
  53. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs_cmd.py +0 -0
  54. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/pages.py +0 -0
  55. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/reads.py +0 -0
  56. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/render.py +0 -0
  57. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/trace.py +0 -0
  58. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/canned_daemon.py +0 -0
  59. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/conftest.py +0 -0
  60. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/contract_absence.py +0 -0
  61. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/documents.py +0 -0
  62. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/governed_programs.py +0 -0
  63. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/pack_doubles.py +0 -0
  64. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/replies.py +0 -0
  65. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_approvals.py +0 -0
  66. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_ask_connection_loss.py +0 -0
  67. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_ask_end_to_end.py +0 -0
  68. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_boundary_runtime.py +0 -0
  69. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_changelog_names_the_version.py +0 -0
  70. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_contract_absence.py +0 -0
  71. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_contract_words.py +0 -0
  72. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_database_pack.py +0 -0
  73. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_decided_name.py +0 -0
  74. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_default_socket.py +0 -0
  75. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_dependency_closure.py +0 -0
  76. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_developer_certificate_of_origin.py +0 -0
  77. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_engine.py +0 -0
  78. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_engine_is_agnostic.py +0 -0
  79. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_evidence_export_and_exports.py +0 -0
  80. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_evidence_history_and_audit.py +0 -0
  81. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_evidence_reads.py +0 -0
  82. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_exit_codes.py +0 -0
  83. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_follow_children.py +0 -0
  84. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_gate_modes.py +0 -0
  85. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_governed_run_leaves_no_trace.py +0 -0
  86. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_http_client_pack.py +0 -0
  87. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_instrument_run.py +0 -0
  88. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_manifest.py +0 -0
  89. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_offline_audit_vectors.py +0 -0
  90. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_pack_designation.py +0 -0
  91. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_pack_doubles.py +0 -0
  92. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_packs_cmd.py +0 -0
  93. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_packs_shipped.py +0 -0
  94. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_partition_boundary.py +0 -0
  95. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_pointers_survive_publication.py +0 -0
  96. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_public_vocabulary.py +0 -0
  97. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_publication_checklist_names_every_act.py +0 -0
  98. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_read_rendering.py +0 -0
  99. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_reads.py +0 -0
  100. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_release_workflow_publishes_only_on_a_tag.py +0 -0
  101. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_review_bounds.py +0 -0
  102. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_running_the_module.py +0 -0
  103. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_shipped_wheel_notices.py +0 -0
  104. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_source_distribution_notices.py +0 -0
  105. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_spdx_identifiers.py +0 -0
  106. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_subprocess_pack.py +0 -0
  107. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_trace_and_explain.py +0 -0
  108. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_verification_claims_what_it_established.py +0 -0
  109. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_verify_asks_every_effect.py +0 -0
  110. {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_verify_pairs_an_inner_spawn.py +0 -0
@@ -6,6 +6,25 @@ declares; a release with no section fails the gate.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.3.1
10
+
11
+ - **`instrument verify` says how a run ended when the program raised, too.** The harness said
12
+ its own ending only for a program that returned, so a program that ended on an exception before
13
+ a line of it was seen — a script that does not compile, a module whose bytecode another Python
14
+ wrote — left no ending behind, and the command answered `4` and « the verification did not run
15
+ to a conclusion »: « the control plane could not be asked », about a plane that had answered.
16
+ It now answers `7`, « the verifier never saw the program's own code start », naming the
17
+ exception the program ended on; the program's own traceback is still printed. For the same
18
+ reason, a program that caught the abort of an unreadable chain and then raised an exception of
19
+ its own now gets the chain's ending (`4`, the transport's own problem code and sentence) rather
20
+ than the generic one.
21
+ - The contract and the boundary move to 0.3.1, released with this client.
22
+ - A `python` the shell cannot find is refused as before (64), and the refusal now names
23
+ the `python3` on the same PATH.
24
+ - Every program the README and the quickstart run is spelled `python3 …`; the
25
+ quickstart starts the daemon again before section 7, shows a closed epoch verified,
26
+ and says how long a rejection stands.
27
+
9
28
  ## 0.3.0
10
29
 
11
30
  - **`--socket` is optional for a per-user profile**, on every verb that opens a connection. Given
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sayfirst-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: The product command-line interface of the sayfirst control plane
5
5
  Project-URL: Repository, https://github.com/fredaime/sayfirst-cli
6
6
  Project-URL: Documentation, https://github.com/fredaime/sayfirst-cli/blob/main/README.md
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Requires-Python: <3.15,>=3.12
18
- Requires-Dist: sayfirst-boundary==0.3.0
19
- Requires-Dist: sayfirst-contract==0.3.0
18
+ Requires-Dist: sayfirst-boundary==0.3.1
19
+ Requires-Dist: sayfirst-contract==0.3.1
20
20
  Description-Content-Type: text/markdown
21
21
 
22
22
  <!-- SPDX-License-Identifier: Apache-2.0 -->
@@ -39,7 +39,8 @@ anything that matters: *may I do this, with these arguments, as this account?*
39
39
  the boundary (`sayfirst-boundary`) holds the grant for exactly one execution.
40
40
  - **A person is in the loop by construction, not by dashboard.** A suspension
41
41
  is a wait. `sayfirst approvals approve` ends it once, the deadline comes from
42
- the policy, and a rejection is final.
42
+ the policy, and a rejection stands until that deadline, or until the daemon
43
+ restarts.
43
44
  - **Every decision leaves a record**, and `sayfirst evidence export` saves a
44
45
  bundle a third party verifies offline, with the contract alone.
45
46
  - **The evidence is honest about itself.** An export of an epoch the daemon has
@@ -60,7 +61,7 @@ flowchart LR
60
61
  ```console
61
62
  $ uv tool install sayfirst-cli --with-executables-from sayfirst-control-plane --with-executables-from sayfirstd
62
63
  $ sayfirst-daemon up --quickstart
63
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
64
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
64
65
  ```
65
66
 
66
67
  The first installs the client (`sayfirst`), the daemon (`sayfirst-daemon`) and
@@ -69,7 +70,7 @@ install` takes one package, so the other two ride on `--with-executables-from`.
69
70
  The second writes a readable starter policy and a per-user configuration under
70
71
  `~/.sayfirst/quickstart/` **if they are not there**, starts the real daemon in
71
72
  the background, and says « ready » only once that daemon has answered. The third
72
- runs a program you already have, under the `python` you named, with every
73
+ runs a program you already have, under the `python3` you named, with every
73
74
  process it starts asked about first:
74
75
 
75
76
  ```console
@@ -81,7 +82,7 @@ policy: ~/.sayfirst/quickstart/policy.toml
81
82
  evidence: ~/.sayfirst/quickstart/evidence
82
83
  integrity grade: observability (the caller can write the store; the chain detects accidental corruption only)
83
84
  …
84
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
85
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
85
86
  hello
86
87
  ```
87
88
 
@@ -103,7 +104,7 @@ restarted — and running `up --quickstart` again never writes over it.
103
104
 
104
105
  ```console
105
106
  $ sayfirstd status # what the daemon says about itself
106
- $ sayfirst instrument verify --pack subprocess --scope local -- python my_agent.py
107
+ $ sayfirst instrument verify --pack subprocess --scope local -- python3 my_agent.py
107
108
  $ sayfirst-daemon down # stops the daemon `up` started, and only that one
108
109
  ```
109
110
 
@@ -111,16 +112,14 @@ $ sayfirst-daemon down # stops the daemon `up` started, and only that one
111
112
  a person's answer, the proof, the chain read back and exported — and every
112
113
  command on that page was run, in that order, before it was written down.
113
114
 
114
- ### What the index holds today
115
+ ### What the index holds
115
116
 
116
- `sayfirst-cli` 0.2.0 is on the Python index, with `sayfirst-contract`,
117
- `sayfirst-boundary`, `sayfirst-contract-stub` and `sayfirst-conformance`. That
118
- release **predates the three commands above**: it requires `--socket`, reads
119
- `--pack` as a directory only, and refuses `python` as the first word of a
120
- target. `sayfirst-control-plane` and `sayfirstd` are **not on the index** as
121
- this is written. Until a release carries this page, build the distributions from
122
- checkouts of the two repositories and install those — the quickstart's first
123
- section gives the three lines, and they are what its walk used.
117
+ Everything the three commands above need is on the Python index at 0.3.1:
118
+ `sayfirst-cli`, the `sayfirst-contract` it speaks and the `sayfirst-boundary` a
119
+ governed program holds its grants in, and the control plane's
120
+ `sayfirst-control-plane` and `sayfirstd`. The first command installs all five
121
+ and builds nothing. A checkout builds the same thing — the quickstart's first
122
+ section gives the three lines.
124
123
 
125
124
  Installed on its own, the client brings three distributions and no more: the
126
125
  command, the contract it speaks, and the boundary in which a governed program
@@ -142,12 +141,13 @@ verified, and one answer rendered as it was given: allow, deny or suspend, and
142
141
  **It puts the boundary in front of somebody else's program.** `sayfirst
143
142
  instrument run --pack PACK … -- <program>` runs a program with the named effects
144
143
  asked about first, reversibly and writing nothing anywhere; a program spelled
145
- `python app.py` is handed, whole command and all, to the interpreter that was
146
- named, so it keeps its own environment's dependencies; `instrument verify`
147
- runs it again under the interpreter's own audit hook and proves, from that and
148
- the scope's evidence chain alone, that every effect of a named kind was preceded
149
- by a decision; `instrument apply` is reserved for the committed code
150
- modification and refuses, saying so. `sayfirst packs list` prints the packs this
144
+ `python3 app.py` (or `python app.py` inside an activated environment) is
145
+ handed, whole command and all, to the interpreter that was named, so it keeps
146
+ its own environment's dependencies; `instrument verify` runs it again under the
147
+ interpreter's own audit hook and proves, from that and the scope's evidence
148
+ chain alone, that every effect of a named kind was preceded by a decision;
149
+ `instrument apply` is reserved for the committed code modification and refuses,
150
+ saying so. `sayfirst packs list` prints the packs this
151
151
  distribution ships — `database`, `http-client`, `subprocess` — each of which
152
152
  `--pack` takes by that name, while a pack of your own is a directory spelled
153
153
  with a separator, `--pack ./own-pack` ([`docs/PACKS.md`](docs/PACKS.md)). A program whose effects are
@@ -247,7 +247,7 @@ contract is built from a checkout of the control plane's repository, at the tag
247
247
  this client pins:
248
248
 
249
249
  ```console
250
- $ SAYFIRST_CONTRACT_SOURCE=../sayfirst-control-plane SAYFIRST_CONTRACT_REF=v0.3.0 ./scripts/gate.sh
250
+ $ SAYFIRST_CONTRACT_SOURCE=../sayfirst-control-plane SAYFIRST_CONTRACT_REF=v0.3.1 ./scripts/gate.sh
251
251
  ```
252
252
 
253
253
  The workflow does the same and carries no credential of any kind: a fork can
@@ -274,9 +274,9 @@ GitHub's private reporting, never a public issue — [`LICENSE`](LICENSE),
274
274
 
275
275
  ## Status
276
276
 
277
- **0.2.0 is the first public release**, 2026-09-17. What is *not* here is named
278
- too, because a surface a reader assumes is an overclaim: `connect`, `profile`,
279
- `whoami`, `integrate` and `version` are in
277
+ **0.2.0 is the first public release**, 2026-09-17. 0.3.1 is the current one.
278
+ What is *not* here is named too, because a surface a reader assumes is an
279
+ overclaim: `connect`, `profile`, `whoami`, `integrate` and `version` are in
280
280
  [`docs/PARTITION.md`](docs/PARTITION.md) and none of them exists here.
281
281
 
282
282
  Next, and only what a document in this tree already says: the `evidence` grade of
@@ -7,13 +7,16 @@ Python program you already have:
7
7
  ```console
8
8
  $ uv tool install sayfirst-cli --with-executables-from sayfirst-control-plane --with-executables-from sayfirstd
9
9
  $ sayfirst-daemon up --quickstart
10
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
10
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
11
11
  ```
12
12
 
13
13
  Everything on this page was run, in this order, before it was written down: the
14
- commands are pasted from that run, and so are their answers. The run used
15
- distributions built from this tree and installed by the first command, in a
16
- shell with neither repository on any path, under a home directory made for it;
14
+ commands are pasted from that run, and so are their answers. The run installed
15
+ 0.3.1 with the first command from wheels built from the two release trees,
16
+ given `--no-index` (the walk ran before 0.3.1 reached the index; « From
17
+ checkouts instead » below is that install), on a PATH with `python3` and no
18
+ `python`, in a shell with neither repository on any path, under a home
19
+ directory made for it;
17
20
  its paths are written here the way they read under an ordinary account (`~`, and
18
21
  `/run/user/1000` for the runtime directory). Your identifiers, timestamps and
19
22
  hashes will differ; the shapes will not.
@@ -22,21 +25,28 @@ hashes will differ; the shapes will not.
22
25
 
23
26
  - Linux or macOS. The control plane listens on a local socket and reads who
24
27
  is calling from the kernel; there is no port and nothing that takes a token.
25
- - Python 3.12, 3.13 or 3.14, and [`uv`](https://docs.astral.sh/uv/). The walk
26
- used uv 0.12.
27
- - A home directory that only you can write. The daemon refuses to put its
28
- socket below a directory a group can write and that is not sticky
29
- (`socket_directory_unprotected`), and it says so instead of starting.
28
+ - Python 3.12, 3.13 or 3.14 as `python3` on your PATH (Linux distributions
29
+ and macOS name it that; inside an activated virtual environment `python`
30
+ works too), and [`uv`](https://docs.astral.sh/uv/). The walk used
31
+ uv 0.12.
32
+ - A home directory that only you can write. The daemon refuses a directory a
33
+ group can write and that is not sticky: below the socket as
34
+ `socket_directory_unprotected`, and above the policy as
35
+ `policy_unavailable_at_start … exposed: group_write`; either way it says so
36
+ instead of starting.
30
37
 
31
38
  ## 1. Install
32
39
 
33
40
  ```console
34
41
  $ uv tool install sayfirst-cli --with-executables-from sayfirst-control-plane --with-executables-from sayfirstd
35
- + sayfirst-boundary==0.3.0
36
- + sayfirst-cli==0.3.0
37
- + sayfirst-contract==0.3.0
38
- + sayfirst-control-plane==0.3.0
39
- + sayfirstd==0.3.0
42
+ Resolved 5 packages in 3ms
43
+ Prepared 5 packages in 6ms
44
+ Installed 5 packages in 1ms
45
+ + sayfirst-boundary==0.3.1
46
+ + sayfirst-cli==0.3.1
47
+ + sayfirst-contract==0.3.1
48
+ + sayfirst-control-plane==0.3.1
49
+ + sayfirstd==0.3.1
40
50
  Installed 1 executable from `sayfirst-control-plane`: sayfirst-daemon
41
51
  Installed 1 executable from `sayfirstd`: sayfirstd
42
52
  Installed 1 executable: sayfirst
@@ -49,14 +59,11 @@ other two ride on `--with-executables-from`; three separate `uv tool install`
49
59
  lines, or `pip install sayfirst-cli sayfirst-control-plane sayfirstd` in an
50
60
  environment of your own, install the same thing.
51
61
 
52
- **What the index holds is not yet what this page installs.** `sayfirst-cli`
53
- 0.2.0 is on the index and predates everything below: it has no default socket,
54
- reads `--pack` as a path only, and refuses `python` as the first word of a
55
- target. `sayfirst-control-plane` and `sayfirstd` are not on the index at all as
56
- this is written. Until a release carries this page, install from checkouts of
57
- the two repositories,
62
+ **From checkouts instead.** A contributor installs the same thing from the two
63
+ repositories' trees,
58
64
  <https://github.com/fredaime/sayfirst-control-plane> and
59
- <https://github.com/fredaime/sayfirst-cli> — which is what the walk did:
65
+ <https://github.com/fredaime/sayfirst-cli>: build the wheels, then install them
66
+ with nothing fetched.
60
67
 
61
68
  ```console
62
69
  $ (cd /path/to/sayfirst-control-plane && uv build --all-packages --wheel --out-dir ~/wheels)
@@ -64,9 +71,9 @@ $ (cd /path/to/sayfirst-cli && uv build --wheel --out-dir ~/wheels)
64
71
  $ uv tool install --no-index --find-links ~/wheels sayfirst-cli --with-executables-from sayfirst-control-plane --with-executables-from sayfirstd
65
72
  ```
66
73
 
67
- `--no-index` keeps the install to the wheels you built: nothing is fetched, so
68
- what runs is exactly those two trees — and two of the five distributions are
69
- not on the index to be fetched anyway.
74
+ `--no-index` keeps the install to the wheels you built: the index carries the
75
+ same version numbers, and an installer allowed to look there could take the
76
+ published files instead of your trees.
70
77
 
71
78
  ## 2. Start a control plane
72
79
 
@@ -82,7 +89,7 @@ grade re-evaluation interval: 30 seconds
82
89
  privacy provider: none (captured content is recorded as given)
83
90
  evidence emission: delivering
84
91
  log: ~/.sayfirst/quickstart/daemon.log
85
- pid: 1622067
92
+ pid: 741184
86
93
  wrote: ~/.sayfirst/quickstart/policy.toml
87
94
  wrote: ~/.sayfirst/quickstart/daemon.toml
88
95
  stop: sayfirst-daemon down
@@ -100,13 +107,15 @@ Everything lives in one private directory (`0700`, files `0600`):
100
107
 
101
108
  ```console
102
109
  $ ls -la ~/.sayfirst/quickstart
103
- drwx------ 3 you you 160 .
104
- -rw------- 1 you you 0 daemon.lock
105
- -rw------- 1 you you 75 daemon.log
106
- -rw------- 1 you you 259 daemon.run.json
107
- -rw------- 1 you you 596 daemon.toml
108
- drwx------ 5 you you 120 evidence
109
- -rw------- 1 you you 3008 policy.toml
110
+ total 16
111
+ drwx------ 3 you you 160 Sep 29 01:06 .
112
+ drwx------ 3 you you 60 Sep 29 01:06 ..
113
+ -rw------- 1 you you 0 Sep 29 01:06 daemon.lock
114
+ -rw------- 1 you you 76 Sep 29 01:06 daemon.log
115
+ -rw------- 1 you you 260 Sep 29 01:06 daemon.run.json
116
+ -rw------- 1 you you 598 Sep 29 01:06 daemon.toml
117
+ drwx------ 5 you you 120 Sep 29 01:06 evidence
118
+ -rw------- 1 you you 3008 Sep 29 01:06 policy.toml
110
119
  ```
111
120
 
112
121
  `daemon.run.json` is what `up` knows about the daemon it started: its pid, the
@@ -142,7 +151,7 @@ subprocess.run(["echo", "hello"], check=True)
142
151
  ```
143
152
 
144
153
  ```console
145
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
154
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
146
155
  hello
147
156
  ```
148
157
 
@@ -162,27 +171,51 @@ that. Three things in that line are worth a sentence each.
162
171
  one name. Nothing is searched for, and whoever answers is still verified to be
163
172
  your own account's process before a byte is sent. `--socket PATH` overrides
164
173
  it, and system mode always names it.
165
- - **`python` means your `python`.** The program is handed to the interpreter
174
+ - **`python3` means your `python3`.** The program is handed to the interpreter
166
175
  you named, found the way your shell finds it, with the boundary installed in
167
176
  that process — so a program living in a project's environment keeps its own
168
- dependencies:
177
+ dependencies. Here `~/project` has an environment of its own
178
+ (`uv venv`, then `uv pip install humanize`) holding a package the `sayfirst`
179
+ tool does not, and `real_agent.py` uses it:
180
+
181
+ ```python
182
+ import sys
183
+
184
+ import humanize
185
+
186
+ print("hello from the project's own interpreter")
187
+ print("running under", sys.prefix)
188
+ print("imported:", humanize.naturalsize(1_000_000))
189
+ ```
190
+
191
+ In `~/project`, with its environment activated, `python3` is that
192
+ environment's:
169
193
 
170
194
  ```console
171
- $ sayfirst instrument run --pack subprocess --scope local -- python real_agent.py
195
+ $ . .venv/bin/activate
196
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 real_agent.py
172
197
  hello from the project's own interpreter
173
198
  running under ~/project/.venv
174
- imported: a dependency only the project has
199
+ imported: 1.0 MB
175
200
  ```
176
201
 
177
- An agent started as a console script rather than as `python …` is handed
202
+ Without the activation the same command finds the system `python3`, and the
203
+ program stops at its import (`ModuleNotFoundError: No module named 'humanize'`).
204
+
205
+ An agent started as a console script rather than as `python3 …` is handed
178
206
  over the same way: `-- ./myagent` reads the executable's shebang
179
207
  (`#!…/.venv/bin/python`) and runs it under that interpreter. Named without an
180
208
  interpreter (`-- my_agent.py`, or `-- -m package`), a program runs inside the
181
209
  interpreter that carries `sayfirst` itself, which as a `uv` tool has none of
182
210
  your project's packages. A runner in that place — `-- uv run app.py` — is
183
211
  refused, because it would pick an interpreter the boundary is not in; name the
184
- interpreter instead. The named interpreter has to be Python 3.12 or later, and
185
- interpreter options (`python -u …`) are not carried.
212
+ interpreter instead. The named interpreter has to be Python 3.12 or later. An
213
+ option after the interpreter's name is refused (`-- python3 -u real_agent.py`
214
+ exits `64`: « found no file to run at '-u' »); an option the interpreter also
215
+ reads from the environment reaches it that way —
216
+ `PYTHONUNBUFFERED=1 sayfirst instrument run …` runs the program unbuffered.
217
+ A `python` the shell cannot find is refused (`64`), and the refusal says the
218
+ same PATH has a `python3`.
186
219
 
187
220
  What the control plane recorded, read back from it:
188
221
 
@@ -195,12 +228,12 @@ grade re-evaluation interval: 30 seconds
195
228
  privacy provider: none (captured content is recorded as given)
196
229
  evidence emission: delivering
197
230
  $ sayfirst evidence history --scope local --from 1 --all
198
- 1 composition daemon 2b12918b5588b2e3b50efa80fb7035ead2d2579537d991118f92b05bc0c0cfb3
199
- 2 grade b5b86aba-5d1b-4b47-8059-38783a488547 7f6c25dfac3eac4ee6174aff24630eaf8f8804d96436a0daf160644e8195806f
200
- 3 grade 7750b836-8ee2-4d2b-8de5-2c9d96b16e75 ca6a3b37ba461f63b70ae8b6d16641c4b1d446be7fbb428a9d50db38c2c55d1e
201
- 4 grade 0db649e3-d5c9-4145-b05a-feb2ed6c1344 23ca566a49a282624fc21c1a65f467c2d437350682d0ad6dfe131e429f76fdc7
202
- 5 effect 0db649e3-d5c9-4145-b05a-feb2ed6c1344 13776e23385f7a6fa0aff7a01af3a3e090ea3be27a7e9aeab83f93cb09dee791
203
- …
231
+ 1 composition daemon b7e07cc5149332bda660b2bfd874409cc3c43c3fcc545bcdfe434eeca08d7992
232
+ 2 grade 5ca3ce19-2d93-44de-839a-a25148efacc4 8e500b2d2f634f07ed10e70b769b0d170821bd07d9121c5fcdabbb914fdf4d66
233
+ 3 grade bc12e161-76be-4939-80d2-711c7ba145d8 614359e106f698383cc7da518ebd5714be580e70acf0ae9e3b9141b40a83ecb4
234
+ 4 effect bc12e161-76be-4939-80d2-711c7ba145d8 effeb84e48ae71368bd796198d1a49c256c4f7c381f72d11d3e7dc5567355d5e
235
+ 5 grade 8abbfb3c-a3b1-4ca1-8f74-fc732ffa7caa c5a7e9be2d2af3fd0dbb3d691caa7a4019a06dcf1dea34417acece0cb91753ea
236
+ 6 grade 8af05baa-2b94-47f9-acdc-3d9dbd51850e 55e018d5ba3cfd56f7e812351b3a6762bb3006e0209eaf30a27cb722c008167a
204
237
  next_from: none
205
238
  ```
206
239
 
@@ -213,10 +246,10 @@ and change one word — `outcome = "allow"` to `outcome = "deny"`. Save. Nothing
213
246
  is restarted; the daemon reads the file when it decides.
214
247
 
215
248
  ```console
216
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
249
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
217
250
  Traceback (most recent call last):
218
251
  …
219
- sayfirst_boundary.errors.Denied: denied: process.spawn (policy_denies, 48293af4-5ab0-4c92-b16b-f0ca974a490b)
252
+ sayfirst_boundary.errors.Denied: denied: process.spawn (policy_denies, 47339de8-e6c3-4b2f-8c91-7eaa6a53aa26)
220
253
  $ echo $?
221
254
  1
222
255
  ```
@@ -231,10 +264,10 @@ suspension or an unreachable control plane as a denial too.
231
264
  Now `outcome = "suspend"`:
232
265
 
233
266
  ```console
234
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
267
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
235
268
  Traceback (most recent call last):
236
269
  …
237
- sayfirst_boundary.errors.Suspended: suspended: process.spawn awaits approval 55fc6d74-275b-4250-9a7f-58871bfc5bf1
270
+ sayfirst_boundary.errors.Suspended: suspended: process.spawn awaits approval 93684c01-f9e8-4f27-9ad8-6c2be85306c5
238
271
  $ echo $?
239
272
  5
240
273
  ```
@@ -242,38 +275,41 @@ $ echo $?
242
275
  Nothing ran, and a person is being waited for. Read the wait, then end it:
243
276
 
244
277
  ```console
245
- $ sayfirst approvals show --approval 55fc6d74-275b-4250-9a7f-58871bfc5bf1 --scope local
246
- approval: 55fc6d74-275b-4250-9a7f-58871bfc5bf1
247
- decision: 45faca76-852a-4991-a5fb-bc2190e42325
278
+ $ sayfirst approvals show --approval 93684c01-f9e8-4f27-9ad8-6c2be85306c5 --scope local
279
+ approval: 93684c01-f9e8-4f27-9ad8-6c2be85306c5
280
+ decision: ace43a8a-88ad-4907-8e91-4e978af85604
248
281
  state: pending
249
- requested_at: 2026-09-23T02:05:45.453931Z
250
- deadline: 2026-09-23T02:10:45.453931Z
282
+ requested_at: 2026-09-28T23:06:51.035245Z
283
+ deadline: 2026-09-28T23:11:51.035245Z
251
284
  resolved_at: not stated
252
285
  reason: not stated
253
- $ sayfirst approvals approve --approval 55fc6d74-275b-4250-9a7f-58871bfc5bf1 --scope local --reason "checked by hand"
254
- approval: 55fc6d74-275b-4250-9a7f-58871bfc5bf1
255
- decision: 45faca76-852a-4991-a5fb-bc2190e42325
286
+ $ sayfirst approvals approve --approval 93684c01-f9e8-4f27-9ad8-6c2be85306c5 --scope local --reason "checked by hand"
287
+ approval: 93684c01-f9e8-4f27-9ad8-6c2be85306c5
288
+ decision: ace43a8a-88ad-4907-8e91-4e978af85604
256
289
  state: approved
257
- requested_at: 2026-09-23T02:05:45.453931Z
258
- deadline: 2026-09-23T02:10:45.453931Z
259
- resolved_at: 2026-09-23T02:05:45.642267Z
290
+ requested_at: 2026-09-28T23:06:51.035245Z
291
+ deadline: 2026-09-28T23:11:51.035245Z
292
+ resolved_at: 2026-09-28T23:06:51.216909Z
260
293
  reason: checked by hand
261
294
  person: user:you
262
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
295
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
263
296
  hello
264
297
  ```
265
298
 
266
299
  That run is the one execution the person's act authorised: run it once more and
267
300
  it waits again, under a new approval. Running it *while* the wait is open
268
301
  returns the same approval rather than opening a second. `approvals reject` ends
269
- a wait the other way, and the next run is denied with `approval_rejected`.
302
+ a wait the other way: every run of the same program is then denied with
303
+ `approval_rejected` until that wait's deadline — five minutes here, since the
304
+ rule you edited names no `review_deadline_seconds` — and a restart of the daemon
305
+ forgets it.
270
306
 
271
307
  ## 5. Prove it
272
308
 
273
309
  Put the rule back to `"allow"`, and ask for proof rather than a run:
274
310
 
275
311
  ```console
276
- $ sayfirst instrument verify --pack subprocess --scope local -- python my_agent.py
312
+ $ sayfirst instrument verify --pack subprocess --scope local -- python3 my_agent.py
277
313
  hello
278
314
  governed subprocess subprocess.Popen process.spawn events=1
279
315
  inspected: subprocess
@@ -296,7 +332,7 @@ alone on stdout.
296
332
 
297
333
  ```console
298
334
  $ sayfirst-daemon down
299
- SayFirst Control Plane stopped (pid 1622067)
335
+ SayFirst Control Plane stopped (pid 741184)
300
336
  policy and evidence are kept under ~/.sayfirst/quickstart
301
337
  ```
302
338
 
@@ -315,6 +351,11 @@ $ sayfirstd status
315
351
  socket: /run/user/1000/sayfirst/daemon.sock (the per-user default; no --socket was given)
316
352
  verified: false (server_uid None, expected None)
317
353
  unreachable: [Errno 2] No such file or directory
354
+ ```
355
+
356
+ It exits `4`, like every read that could not ask.
357
+
358
+ ```console
318
359
  $ sayfirst ask --capability process.spawn
319
360
  socket: /run/user/1000/sayfirst/daemon.sock (the per-user default; no --socket was given)
320
361
  verified: false (server_uid not stated, expected not stated)
@@ -322,7 +363,7 @@ could not ask: unreachable: [Errno 2] No such file or directory
322
363
  retryable: true
323
364
  $ echo $?
324
365
  4
325
- $ sayfirst instrument run --pack subprocess --scope local -- python my_agent.py
366
+ $ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
326
367
  socket: /run/user/1000/sayfirst/daemon.sock (the per-user default; no --socket was given)
327
368
  nothing is there now, so an effect these packs name will fail closed; `sayfirst-daemon up --quickstart` starts a control plane at that address
328
369
  Traceback (most recent call last):
@@ -343,6 +384,14 @@ escape.
343
384
 
344
385
  ## 7. Asking by hand, and reading back
345
386
 
387
+ Section 6 left the control plane stopped; start it again:
388
+
389
+ ```console
390
+ $ sayfirst-daemon up --quickstart
391
+ SayFirst Control Plane ready
392
+ …
393
+ ```
394
+
346
395
  The same three outcomes without a program. `--scope` defaults to `local` for
347
396
  `ask`, and every other read names it:
348
397
 
@@ -352,22 +401,22 @@ verified: true (server_uid 1000, expected 1000)
352
401
  outcome: allow
353
402
  reason: policy_allows
354
403
  capability: process.spawn in scope local
355
- decision: 52ff1ab5-8813-4230-9ca4-2f272612c64d at 2026-09-23T02:05:47.180188Z
404
+ decision: 86c6debb-715a-4f28-9aa8-75026a395f48 at 2026-09-28T23:06:52.489393Z
356
405
  policy version: sha256:efc1746cea9b50cb31aa5c9f08de996cbefabc34a09d4250d8b68f67c709a5dc
357
406
  $ sayfirst ask --capability net.egress
358
407
  verified: true (server_uid 1000, expected 1000)
359
408
  outcome: suspend
360
409
  reason: policy_requires_review
361
410
  capability: net.egress in scope local
362
- decision: 5cb90b68-bb1a-4674-ae2d-97a1bd9c5797 at 2026-09-23T02:05:47.255248Z
411
+ decision: 30fac8bc-d1a1-4058-a7c4-1ff62e4db58d at 2026-09-28T23:06:52.574755Z
363
412
  policy version: sha256:efc1746cea9b50cb31aa5c9f08de996cbefabc34a09d4250d8b68f67c709a5dc
364
- approval: d058eee2-4ecc-488c-837d-037e206c1247
413
+ approval: 335902ec-3ece-4b06-8856-9d45ef78e2e2
365
414
  $ sayfirst ask --capability database.open
366
415
  verified: true (server_uid 1000, expected 1000)
367
416
  outcome: deny
368
417
  reason: policy_absent
369
418
  capability: database.open in scope local
370
- decision: c6284ad7-f77b-4367-a50d-977489e2f6f7 at 2026-09-23T02:05:47.335332Z
419
+ decision: e2791157-0385-4a79-86d0-abfb97cd83b4 at 2026-09-28T23:06:52.655469Z
371
420
  policy version: sha256:efc1746cea9b50cb31aa5c9f08de996cbefabc34a09d4250d8b68f67c709a5dc
372
421
  ```
373
422
 
@@ -379,18 +428,18 @@ rule saying no.
379
428
  One decision, explained in the daemon's own words and traced into the chain:
380
429
 
381
430
  ```console
382
- $ sayfirst explain --scope local --decision 474856fc-e891-46ac-844c-efa30cc9322f
383
- decision_ref: 474856fc-e891-46ac-844c-efa30cc9322f
431
+ $ sayfirst explain --scope local --decision 86c6debb-715a-4f28-9aa8-75026a395f48
432
+ decision_ref: 86c6debb-715a-4f28-9aa8-75026a395f48
384
433
  scope: local
385
434
  capability: process.spawn
386
435
  outcome: allow
387
436
  reason: policy_allows
388
437
  rule_id: local-processes-run
389
438
  policy_version: sha256:efc1746cea9b50cb31aa5c9f08de996cbefabc34a09d4250d8b68f67c709a5dc
390
- decided_at: 2026-09-23T02:05:47.410354Z
439
+ decided_at: 2026-09-28T23:06:52.489393Z
391
440
  …
392
- $ sayfirst trace --scope local --decision 474856fc-e891-46ac-844c-efa30cc9322f | tail -1
393
- chain: sequence 34, entry_hash 0d00934b16b3dc0004d7e13c884b83aec61a79dfb6d43278dab3530d1f8250d7, grade observability
441
+ $ sayfirst trace --scope local --decision 86c6debb-715a-4f28-9aa8-75026a395f48 | tail -1
442
+ chain: sequence 23, entry_hash ee313608e9c4f09b747c7ec9ac5d817e0945bbeb7b9f267f76f1d2c2c4e935d5, grade observability
394
443
  ```
395
444
 
396
445
  Every read is itself recorded: `history`, `explain`, `trace` and `export` each
@@ -409,15 +458,60 @@ chain: intact
409
458
  coverage: unknown
410
459
  issue: coverage_unknown
411
460
  verification: intact
412
- saved: bundle.json (36 entries)
461
+ saved: bundle.json (29 entries)
462
+ ```
463
+
464
+ ### A closed epoch, verified
465
+
466
+ `down` closes the daemon's evidence epoch and `up` opens the next, and only a range
467
+ inside a closed epoch can be checked whole. The history shows where the epoch closed —
468
+ the `recovery` entry just before the next `composition`:
469
+
470
+ ```console
471
+ $ sayfirst evidence history --scope local --from 1 --all | grep -E 'recovery|composition'
472
+ 1 composition daemon b7e07cc5149332bda660b2bfd874409cc3c43c3fcc545bcdfe434eeca08d7992
473
+ 19 recovery daemon f36c43ff25d8a0c15b64dd55a089127fcab352ffa056420c536314ad63c76a3b
474
+ 20 composition daemon 17c41de74accb1d7b03bf8864229f370c446623d9f528eae1f5387c22cb6463e
475
+ ```
476
+
477
+ Export up to that entry with `--to`:
478
+
479
+ ```console
480
+ $ sayfirst evidence export --scope local --from 1 --to 19 --out closed.json
481
+ local_check: unverifiable
482
+ manifest: recomputes
483
+ chain: intact
484
+ coverage: complete
485
+ verification: intact
486
+ saved: closed.json (19 entries)
487
+ ```
488
+
489
+ Coverage is `complete` — nothing in that range is missing — and the check still says
490
+ `unverifiable`, exit `7`, because section 4 put a person in that range: an approval,
491
+ and every decision that follows from one — the run it authorised, and any
492
+ `approval_rejected` denial — re-derives as `unverifiable` with the cause
493
+ `reason_outside_recipe`. A policy file cannot re-derive a human act, and the
494
+ verifier says so rather than counting it (`--json` shows each decision's
495
+ cause). A range with no person's act in it verifies — here the same epoch up
496
+ to the suspension's own entry, before anyone answered it (`sayfirst trace` on
497
+ the suspended decision names its sequence):
498
+
499
+ ```console
500
+ $ sayfirst evidence export --scope local --from 1 --to 10 --out quiet.json
501
+ local_check: confirmed
502
+ manifest: recomputes
503
+ chain: intact
504
+ coverage: complete
505
+ verification: intact
506
+ saved: quiet.json (10 entries)
413
507
  ```
414
508
 
415
- After a `down` and an `up`, the range that ends at the closed epoch's last entry
416
- exports with coverage `complete`; `sayfirst evidence exports DIRECTORY`
417
- re-verifies every saved bundle with the contract distribution alone, no daemon
418
- needed. A decision a person granted re-derives as `unverifiable` with the cause
419
- `reason_outside_recipe`, by design: an approval cannot be re-derived from a
420
- policy file, and the verifier says so rather than counting it.
509
+ The epoch `up` opened in this section is still open, so a range inside it is
510
+ `coverage: unknown` until the next `down` closes it.
511
+
512
+ `sayfirst evidence exports DIRECTORY` re-verifies every saved bundle with the contract
513
+ distribution alone, no daemon needed, and exits with the worst result among them.
514
+ An export never writes over a file: naming one that exists is refused (`64`).
421
515
 
422
516
  ## 8. A control plane of your own
423
517
 
@@ -430,9 +524,12 @@ serving per_user at /run/user/1000/sayfirst/daemon.sock (acl: checked)
430
524
  ```
431
525
 
432
526
  `daemon.toml` names the mode, the policy file and the evidence directory, all
433
- absolute; `~/.sayfirst/quickstart/daemon.toml` is a working example. Given a
434
- `[socket] path`, the daemon serves there instead, and every client command
435
- takes the same path as `--socket`:
527
+ absolute; `~/.sayfirst/quickstart/daemon.toml` is a working example. Stop the
528
+ quickstart daemon first (`sayfirst-daemon down`), or give the copy an
529
+ `[evidence] path` and a `[socket] path` of its own: two daemons never share an
530
+ evidence directory (`decision_store_unusable`) nor an address (`socket_in_use`),
531
+ and the second refuses to start. Given a `[socket] path`, the daemon serves
532
+ there instead, and every client command takes the same path as `--socket`:
436
533
 
437
534
  ```console
438
535
  $ sayfirst ask --capability process.spawn --socket /srv/sayfirst/daemon.sock