sayfirst-cli 0.3.0__tar.gz → 0.3.2__tar.gz

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