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.
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/CHANGELOG.md +19 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/PKG-INFO +27 -27
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/QUICKSTART.md +182 -85
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/README.md +24 -24
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/PACKS.md +13 -9
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/pyproject.toml +3 -3
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/harness.py +69 -8
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/interpreter.py +13 -2
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_instrument_verify.py +143 -2
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_interpreter_target.py +56 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_quickstart_names_what_exists.py +24 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/.github/workflows/ci.yml +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/.github/workflows/release.yml +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/.gitignore +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/LICENSE +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/NOTICE +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/SECURITY.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/TRADEMARKS.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/EVIDENCE-SURFACE.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/PARTITION.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/PROVENANCE.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/docs/publication-checklist.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/check_dependency_closure.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/check_developer_certificate_of_origin.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/gate.sh +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/scripts/release_version.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/__init__.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/approvals.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/ask.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/evidence.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/exit_codes.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/explain.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/__init__.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/_bootstrap/sitecustomize.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/commands.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/designation.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/engine.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/follow.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/launch.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/manifest.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/instrument/verify.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/main.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/__init__.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/database/NOTE.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/database/interpose.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/database/pack.toml +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/http-client/NOTE.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/http-client/interpose.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/http-client/pack.toml +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/subprocess/NOTE.md +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/subprocess/interpose.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs/subprocess/pack.toml +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/packs_cmd.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/pages.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/reads.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/render.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/src/sayfirst_cli/trace.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/canned_daemon.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/conftest.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/contract_absence.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/documents.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/governed_programs.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/pack_doubles.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/replies.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_approvals.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_ask_connection_loss.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_ask_end_to_end.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_boundary_runtime.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_changelog_names_the_version.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_contract_absence.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_contract_words.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_database_pack.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_decided_name.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_default_socket.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_dependency_closure.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_developer_certificate_of_origin.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_engine.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_engine_is_agnostic.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_evidence_export_and_exports.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_evidence_history_and_audit.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_evidence_reads.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_exit_codes.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_follow_children.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_gate_modes.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_governed_run_leaves_no_trace.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_http_client_pack.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_instrument_run.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_manifest.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_offline_audit_vectors.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_pack_designation.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_pack_doubles.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_packs_cmd.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_packs_shipped.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_partition_boundary.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_pointers_survive_publication.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_public_vocabulary.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_publication_checklist_names_every_act.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_read_rendering.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_reads.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_release_workflow_publishes_only_on_a_tag.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_review_bounds.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_running_the_module.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_shipped_wheel_notices.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_source_distribution_notices.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_spdx_identifiers.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_subprocess_pack.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_trace_and_explain.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_verification_claims_what_it_established.py +0 -0
- {sayfirst_cli-0.3.0 → sayfirst_cli-0.3.1}/tests/test_verify_asks_every_effect.py +0 -0
- {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.
|
|
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.
|
|
19
|
-
Requires-Dist: sayfirst-contract==0.3.
|
|
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
|
|
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 --
|
|
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 `
|
|
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 --
|
|
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 --
|
|
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
|
|
115
|
+
### What the index holds
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
`sayfirst-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
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.
|
|
278
|
-
too, because a surface a reader assumes is an
|
|
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 --
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
+ sayfirst-
|
|
39
|
-
+
|
|
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
|
-
**
|
|
53
|
-
|
|
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
|
|
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:
|
|
68
|
-
|
|
69
|
-
|
|
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:
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-rw------- 1 you you
|
|
107
|
-
-rw------- 1 you you
|
|
108
|
-
|
|
109
|
-
-rw------- 1 you you
|
|
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 --
|
|
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
|
-
- **`
|
|
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
|
-
$
|
|
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:
|
|
199
|
+
imported: 1.0 MB
|
|
175
200
|
```
|
|
176
201
|
|
|
177
|
-
|
|
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
|
|
185
|
-
interpreter
|
|
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
|
|
199
|
-
2 grade
|
|
200
|
-
3 grade
|
|
201
|
-
4
|
|
202
|
-
5
|
|
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 --
|
|
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,
|
|
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 --
|
|
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
|
|
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
|
|
246
|
-
approval:
|
|
247
|
-
decision:
|
|
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-
|
|
250
|
-
deadline: 2026-09-
|
|
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
|
|
254
|
-
approval:
|
|
255
|
-
decision:
|
|
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-
|
|
258
|
-
deadline: 2026-09-
|
|
259
|
-
resolved_at: 2026-09-
|
|
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 --
|
|
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
|
|
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 --
|
|
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
|
|
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 --
|
|
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:
|
|
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:
|
|
411
|
+
decision: 30fac8bc-d1a1-4058-a7c4-1ff62e4db58d at 2026-09-28T23:06:52.574755Z
|
|
363
412
|
policy version: sha256:efc1746cea9b50cb31aa5c9f08de996cbefabc34a09d4250d8b68f67c709a5dc
|
|
364
|
-
approval:
|
|
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:
|
|
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
|
|
383
|
-
decision_ref:
|
|
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-
|
|
439
|
+
decided_at: 2026-09-28T23:06:52.489393Z
|
|
391
440
|
…
|
|
392
|
-
$ sayfirst trace --scope local --decision
|
|
393
|
-
chain: sequence
|
|
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 (
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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.
|
|
434
|
-
|
|
435
|
-
|
|
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
|