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