Skip to main content
Yanked

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 approve ends it once, the deadline comes from the policy, and a rejection is final.
  • Every decision leaves a record, and sayfirst evidence export saves 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 -- python 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 python 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 -- python 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 -- python 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 today

sayfirst-cli 0.2.0 is on the Python index, with sayfirst-contract, sayfirst-boundary, sayfirst-contract-stub and sayfirst-conformance. That release predates the three commands above: it requires --socket, reads --pack as a directory only, and refuses python as the first word of a target. sayfirst-control-plane and sayfirstd are not on the index as this is written. Until a release carries this page, build the distributions from checkouts of the two repositories and install those — the quickstart's first section gives the three lines, and they are what its walk used.

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 python app.py 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 — or unverified, 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 run can fail open, and that is why verify exists. An effect the interposition does not reach runs unasked — a decision never taken, not a denial overridden. verify reports it as this client's own finding, 6, and 7 for 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.0 ./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. 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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sayfirst-cli 0.3.0
File Size Uploaded
sayfirst_cli-0.3.0.tar.gz 370.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sayfirst-cli 0.3.0
File Interpreter ABI Platform
sayfirst_cli-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 508.7 kB

Release files / sayfirst_cli-0.3.0.tar.gz

Download URL sayfirst_cli-0.3.0.tar.gz
Size 370.6 kB
Tags Source
SHA-256 checksum
How to use checksums
301399298755b3d91ee5ed25a8cb8225f06bc9bf4dc30b950be1fb5986d75d41
BLAKE2b-256 checksum
How to use checksums
32487b464af4839f81945294ad44f0cd6dfc22603ae7a04f070bea3448c22995
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 23, 2026.

Transparency log

Release files / sayfirst_cli-0.3.0-py3-none-any.whl

Download URL sayfirst_cli-0.3.0-py3-none-any.whl
Size 138.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dde181f165831f29df9215c64ecdd7fde808123da73a09b6ad0e5a118299ee28
BLAKE2b-256 checksum
How to use checksums
e360de7dda17a9e246dd7297a52d4af2bbade32d3deb555b4f59b1b2bb1ad567
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 23, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page