This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.3.3 instead.
Reason given by maintainers: security: fixed in 0.3.2; use 0.3.2 or later
sayfirst — ask before you act
This repository is the command. sayfirst is the open-source product
command-line interface of the sayfirst control plane: it asks the daemon
before an effect, puts the boundary in front of a program that was never
written for it, and reads back and verifies what was decided.
The 30-second tour
A governed program puts one question to a local control plane before it does anything that matters: may I do this, with these arguments, as this account?
- The daemon answers allow, deny or suspend — from a policy file a person can read, over a Unix socket that tells it who is asking from the kernel's own credentials. No URL, no token, nothing to leak (article 6).
- The program never decides. It asks, and it does only what was allowed:
the boundary (
sayfirst-boundary) holds the grant for exactly one execution. - A person is in the loop by construction, not by dashboard. A suspension
is a wait.
sayfirst approvals approveends it once, the deadline comes from the policy, and a rejection stands until that deadline, or until the daemon restarts. - Every decision leaves a record, and
sayfirst evidence exportsaves a bundle a third party verifies offline, with the contract alone. - The evidence is honest about itself. An export of an epoch the daemon has not closed verifies « coverage unknown » rather than pretending, and a decision a person granted re-derives as « unverifiable », because a policy file cannot re-derive a human act.
flowchart LR
P[your program<br/>+ sayfirst-boundary] -- "ask: capability, scope, digest" --> D[(sayfirst-daemon<br/>policy.toml · evidence chain)]
D -- "allow · deny · suspend" --> P
H[a person<br/>sayfirst approvals approve] -- "one act, once" --> D
V[anyone, offline<br/>sayfirst evidence exports] -. "verify the chain" .-> D
Three commands
$ uv tool install sayfirst-cli --with-executables-from sayfirst-control-plane --with-executables-from sayfirstd
$ sayfirst-daemon up --quickstart
$ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
The first installs the client (sayfirst), the daemon (sayfirst-daemon) and
the daemon's operator surface (sayfirstd) into one tool environment — uv tool install takes one package, so the other two ride on --with-executables-from.
The second writes a readable starter policy and a per-user configuration under
~/.sayfirst/quickstart/ if they are not there, starts the real daemon in
the background, and says « ready » only once that daemon has answered. The third
runs a program you already have, under the python3 you named, with every
process it starts asked about first:
$ sayfirst-daemon up --quickstart
SayFirst Control Plane ready
mode: per_user
socket: /run/user/1000/sayfirst/daemon.sock
policy: ~/.sayfirst/quickstart/policy.toml
evidence: ~/.sayfirst/quickstart/evidence
integrity grade: observability (the caller can write the store; the chain detects accidental corruption only)
…
$ sayfirst instrument run --pack subprocess --scope local -- python3 my_agent.py
hello
Nothing in those lines is told where anything is. A per-user daemon given no
address serves at $XDG_RUNTIME_DIR/sayfirst/daemon.sock (~/.sayfirst/run/
where there is no runtime directory), and a client given no --socket looks at
that one name — it searches for nothing, and it still verifies that whoever
answers is your own account's process before it sends a byte. --pack subprocess is an instrumentation pack, not a policy: it says which calls
are asked about, and the daemon's policy says what the answer is.
The policy is a file you edit: ~/.sayfirst/quickstart/policy.toml.
Change outcome = "allow" to "deny" in its process.spawn rule and the same
run stops before the process starts (status 1); make it "suspend" and the
run stops there too (status 5) while the request waits for a person —
sayfirst approvals approve ends the wait, and the next run of the same program
goes through, once. The daemon reads the file when it decides, so nothing is
restarted — and running up --quickstart again never writes over it.
$ sayfirstd status # what the daemon says about itself
$ sayfirst instrument verify --pack subprocess --scope local -- python3 my_agent.py
$ sayfirst-daemon down # stops the daemon `up` started, and only that one
QUICKSTART.md walks all of it — allow, deny, a suspension and
a person's answer, the proof, the chain read back and exported — and every
command on that page was run, in that order, before it was written down.
What the index holds
Everything the three commands above need is on the Python index at 0.3.1:
sayfirst-cli, the sayfirst-contract it speaks and the sayfirst-boundary a
governed program holds its grants in, and the control plane's
sayfirst-control-plane and sayfirstd. The first command installs all five
and builds nothing. A checkout builds the same thing — the quickstart's first
section gives the three lines.
Installed on its own, the client brings three distributions and no more: the
command, the contract it speaks, and the boundary in which a governed program
holds its grant. Never a web framework, never a database layer — article 14's
direction of dependency, measured on a real install by
scripts/check_dependency_closure.py rather than promised here.
What you get — three directions
A client of this control plane does three things, and this repository ships
all three. sayfirst --help is the claim kept in the dispatch itself; it names
ask, trace, explain, evidence, approvals, instrument and packs.
Before an effect, it asks. One question — a capability, a scope, optionally
a digest of the arguments — put to the daemon over a socket whose peer it
verified, and one answer rendered as it was given: allow, deny or suspend, and
"could not ask" when the daemon could not be reached. That is sayfirst ask.
It puts the boundary in front of somebody else's program. sayfirst instrument run --pack PACK … -- <program> runs a program with the named effects
asked about first, reversibly and writing nothing anywhere; a program spelled
python3 app.py (or python app.py inside an activated environment) is
handed, whole command and all, to the interpreter that was named, so it keeps
its own environment's dependencies; instrument verify runs it again under the
interpreter's own audit hook and proves, from that and the scope's evidence
chain alone, that every effect of a named kind was preceded by a decision;
instrument apply is reserved for the committed code modification and refuses,
saying so. sayfirst packs list prints the packs this
distribution ships — database, http-client, subprocess — each of which
--pack takes by that name, while a pack of your own is a directory spelled
with a separator, --pack ./own-pack (docs/PACKS.md). A program whose effects are
not library calls composes the boundary by hand from sayfirst-boundary.
Afterwards, it reads and verifies. sayfirst trace reads back one decision
and follows it into the evidence that holds it; explain renders the reason the
control plane gave — the rule it applied, the policy version it ran under — and
adds none of its own. evidence history pages a scope, audit puts the served
verdict beside a local check and names a finding wherever the two disagree,
export saves a bundle and exports re-verifies every one in a directory. Each
read from the daemon names its scope (--scope); the two offline checks name
a path and refuse --scope, because the file says which scope it holds
(docs/EVIDENCE-SURFACE.md).
And a person ends a wait. sayfirst approvals show reads where a suspended
request stands: its state, when it was asked, when it ends, and once a person has
acted, when and why. approve and reject end it exactly once, with an optional
--reason — one person's act, nothing that counts signatures (article 12).
The exit codes
0 allow, 1 deny, 5 suspend, 3 the request was refused, 4 the control
plane could not be asked — because article 1 requires that "denied" and "could
not ask" never read as each other. A caller branching on a single non-zero exit
would read an unreachable daemon as a refusal; the codes exist so that it
cannot. instrument run ends with the program's own status — except when the
program does not handle an outcome the boundary raised in it, and then it ends
with that outcome's code (1, 5, 3 or 4) rather than the interpreter's
1, which would read every one of them as a denial. trace, explain and
evidence history exit 0 on a read, whatever the record said. The checks —
evidence audit, export, exports and instrument verify — carry the local
check's own result instead: 0 when it holds, 6 for a finding the plane did
not state, 7 when it could not conclude, the ordinary answer for a bundle from
an open epoch (so sayfirst evidence export && … is not how to script one);
like every read, they answer 3 or 4 when the plane refused them or could not
be asked. A misused invocation is 64; a usage error is 2 everywhere.
What it is not (yet)
- A deployment of this version is graded
observability. The governed program can write or replace the evidence store, so the record it keeps is one that program could have forged — orunverified, which claims nothing. At neither grade is any claim of proof or of tamper detection made. Article 7 defines a third grade,evidence, which no deployment of this version reaches. This client displays the grade it was given and never computes a second one. - Nothing here confines anything. A program that does not call the boundary
is not governed; pair the system with operating-system sandboxing for code you
do not trust (
SECURITY.md). instrument runcan fail open, and that is whyverifyexists. An effect the interposition does not reach runs unasked — a decision never taken, not a denial overridden.verifyreports it as this client's own finding,6, and7for a path the run never walked.- A gate can come back reduced. Where the contract cannot be built, the
gate names, counts and prints every check it did not run, and exits
75. A reduced run is not a pass and never renders as one.
Architecture
| Repository | What it is | Distributions |
|---|---|---|
sayfirst-control-plane |
the daemon, the contract, the boundary, the policy format, the evidence chain | the daemon and its operator surface, sayfirst-contract, sayfirst-boundary, and the stub, conformance and testing kits |
sayfirst-cli |
this repository: the sayfirst command — ask, trace, explain, evidence, approvals, instrument, packs |
sayfirst-cli |
sayfirst-governed-agent-demo |
a LangGraph agent governed node by node — the demonstrator, not a product | none; it is cloned and run |
Inside this one: the command tree and its seven verbs (src/sayfirst_cli/), the
instrumentation engine with its three packs, and the offline verifier that reads
the contract's canonicalization, recipe and vectors — no line of server code.
Where the line falls. The control plane's repository keeps the operator
surface that inspects its own daemon, sayfirstd — today status, whoami,
plugins list and conformance replay. trace, explain and
evidence {audit,history,exports,export} are this client's: they read the
decisions and evidence the plane supplied.
docs/PARTITION.md says it command by command, and names
the questions it does not close.
What arrived by copy: nothing. docs/PROVENANCE.md
carries an empty table and says why that is the answer rather than an omission:
the transport this client speaks — a Unix socket, the identity read from the
peer credential the kernel reports — exists in no other tree to copy.
The name. The product and the command are sayfirst, decided by the operator
on 2026-09-04. This repository claims the distribution sayfirst-cli, the import
package sayfirst_cli and the console script sayfirst; the control plane's
operator surface takes the daemon's own form of the name instead, and
tests/test_decided_name.py holds the name as a word.
The gate, the guards, and contributing
scripts/gate.sh is the whole gate in one command: format, lint, tests, and the
guard that installs this distribution into an empty environment and reads back
everything that arrived with it. What a contributor runs is what decides a merge,
bar the sign-off check below, which reads a range only a pull request has. The
contract is built from a checkout of the control plane's repository, at the tag
this client pins:
$ SAYFIRST_CONTRACT_SOURCE=../sayfirst-control-plane SAYFIRST_CONTRACT_REF=v0.3.1 ./scripts/gate.sh
The workflow does the same and carries no credential of any kind: a fork can
build, test and contribute with nothing but this repository and a public clone
(article 16). Guards hold the rest — every published sentence sends a reader
somewhere they can go (tests/test_public_vocabulary.py), every reader-facing
link resolves (tests/test_pointers_survive_publication.py), every file names its
licence (tests/test_spdx_identifiers.py), and the command surface stays on this
side of the partition (tests/test_partition_boundary.py).
Contributions are Apache-2.0 under the Developer Certificate of Origin, read on
every pull request by scripts/check_developer_certificate_of_origin.py, which
refuses a commit with no well-formed sign-off and refuses a range it cannot read
rather than reporting it as passing (article 15). The control plane's
CONSTITUTION.md
binds this repository too, adopted by pointer, never by copy, because a copy
drifts (article 0); that repository also carries the project's
CONTRIBUTING.md,
its GOVERNANCE.md
and the one marks policy, which TRADEMARKS.md points at rather
than copies. Here: SECURITY.md — report a vulnerability through
GitHub's private reporting, never a public issue — LICENSE,
NOTICE and CHANGELOG.md.
Status
0.2.0 is the first public release, 2026-09-17. 0.3.1 is the current one.
What is not here is named too, because a surface a reader assumes is an
overclaim: connect, profile, whoami, integrate and version are in
docs/PARTITION.md and none of them exists here.
Next, and only what a document in this tree already says: the evidence grade of
article 7, which the control plane must reach first; the open questions of the
partition, including which side answers whoami; and instrument apply, which
keeps the name of the committed code modification and refuses until that mode
exists. CHANGELOG.md names each one as it lands.
Metadata
Release files for sayfirst-cli 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sayfirst_cli-0.3.1.tar.gz | 375.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sayfirst_cli-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 514.7 kB
Release files / sayfirst_cli-0.3.1.tar.gz
| Download URL | sayfirst_cli-0.3.1.tar.gz |
|---|---|
| Size | 375.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
26a7d1979a6600903675d709961a6021c75d1392ee2b4705b04e58fbee782378
|
|
BLAKE2b-256 checksum How to use checksums |
d52c2e81d3157a507844ef5a1b3b158d4e43048947857ff8f72efc089e13b6b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / sayfirst_cli-0.3.1-py3-none-any.whl
| Download URL | sayfirst_cli-0.3.1-py3-none-any.whl |
|---|---|
| Size | 139.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1a2dd2679b9387f60f7db48c77899eeebe30c441961954c2d1f6c68d7433f588
|
|
BLAKE2b-256 checksum How to use checksums |
172dafdba76ceb3303c41710aa6f15e9038a30a83b7c63d1abf0d7be9edb978c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency log