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 command-line interface

The open-source command-line interface of the sayfirst control plane: the product CLI a user installs to connect an existing program to the boundary, to read what was decided, and to verify it afterwards. QUICKSTART.md installs it beside its daemon and walks one governed decision end to end — every command on that page was run before it was written down.

Its content arrives as the partition of the private CLI, by copy with a provenance review, slice by slice. The first slice is here and it runs: docs/PARTITION.md says what crosses and what is still open, and docs/PROVENANCE.md says what arrived by copy — today, nothing, and why that is the right answer rather than an omission.

Three directions

A client of this control plane does three things, and this repository ships all three.

Before an effect, it asks. One question — a capability, a scope — 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, and it is what the first slice implements.

It puts the boundary in front of somebody else's program. sayfirst instrument run --pack DIR … -- <program> runs a program with the named effects asked about first, reversibly and with nothing written anywhere; sayfirst 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; sayfirst instrument apply is reserved for the committed code modification and refuses, saying so. sayfirst packs list prints the convenience packs this distribution ships, one line each with the path --pack accepts, and sayfirst packs check PATH reads one the way the engine will before anything runs with it. docs/PACKS.md is the whole of it.

Afterwards, it reads and verifies. Every read from the daemon names its scope explicitly (--scope) — the writer's local default does not apply. The two offline checks name a path instead and take no scope at all: evidence audit --file checks one saved bundle and evidence exports a directory of them, and both refuse --scope, because the file says which scope it holds. This is a surface rather than a list, and it grows by slices; what it answers today is below, and sayfirst --help is the claim that is kept in the dispatch itself.

sayfirst trace reads back the record of one decision and follows it into the evidence that holds it: the record itself, and where it sits in the chain, bounded to the pages the read walks.

sayfirst explain reads the reason the control plane gave for a decision — the rule it applied and the policy version it ran under — in the plane's own words, adding none of its own.

sayfirst evidence history and sayfirst evidence audit page through scoped evidence: history renders what each page holds, audit puts the served verdict beside a local check and names a finding wherever the two disagree; --file checks an already-saved bundle offline, without opening a socket.

sayfirst evidence export saves a bundle a third party can verify with the contract distribution alone, offline, no server code and no daemon; --out names where it is saved. A bundle taken while the daemon's current epoch is still open verifies with coverage unknown rather than complete — the honest answer, not a failure. sayfirst evidence exports lists and re-verifies every *.json bundle saved in a directory.

sayfirst approvals show reads where one suspended wait stands: its state, when it was asked, when it ends, and — once a person has acted — when and why. sayfirst approvals approve and sayfirst approvals reject end that wait exactly once, an optional --reason and all, and render the same record show would: one person's act, and nothing that counts signatures or names a designation (article 12). These three follow the exit codes below, with no case of their own: an approval already resolved, asked again, answers approval_resolved, which the registry classifies as a refusal — so it is the 3 below, like every other refusal, and this client special-cases nothing.

trace, explain and evidence history exit 0 on a read, whatever the record's own outcome. evidence audit, evidence export and evidence exports exit with the local check's result instead: 0 when it holds, 6 when it finds what the plane did not say, 7 when it cannot conclude — which is the ordinary answer for a bundle taken while the epoch is still open, and why sayfirst evidence export && … is not how to script one. A chain the verifier could not judge — an entry declaring a recipe this generation holds no reader for, a range assembled out of order, an instant outside the calendar — is 7 as well, with the sequence it stopped at: the answer arrived and the check could not conclude, which is neither a failure to obtain the answer nor a finding against the chain. Every command that reads from the control plane exits 3 when the request was refused, 4 when the plane could not be read from or the answer itself could not be read at all, and 64 on a misused invocation; a usage error the parser catches exits 2 everywhere.

sayfirst instrument verify uses the same two local-check codes for the proof it makes: 0 only when every point of every designated pack was governed and no effect went unjudged, 6 for an effect no decision preceded — a finding this client made, never a denial the plane gave — and 7 when the check could not conclude, which includes a path the run never walked and an effect this proof could not judge. A run that concluded nothing at all says which of its endings happened, and answers with that ending's own code: 4 when the chain could not be read, before the program started or while it ran, and when the verifier never saw the program's own code start; 64 when the invocation was refused before it began; 7 when findings arrived and would not read. sayfirst instrument run owns none of these: it passes the governed program's own ending through untouched, and answers 64 only for mistakes in the invocation. sayfirst packs list exits 64 when a pack this distribution ships will not read — it names it, prints the rest of the list anyway, and never ends in a traceback.

The operator closed Q-A on 2026-09-05: this repository owed the evidence surface, and its promise to read and verify became a commitment with a date on it — kept, as of this slice. docs/PARTITION.md records the decision; docs/EVIDENCE-SURFACE.md records what shipped and the contract support it used. Export verification uses the recipe and vectors from the contract distribution, without importing server code.

None of the three defines control semantics or derives a decision. A client "explains and invokes" control semantics; it "never defines them, never keeps a decision past the lifetime the control plane gave it, and never derives an answer the control plane did not give" (article 1). Asking, instrumenting and reading are three ways of carrying an answer someone else made — the third of them, the verifier, reads the interpreter and the chain and reaches no verdict of its own about policy — and that is why all three belong to a client.

The first slice

One command, end to end, against the open control plane's contract distribution: sayfirst ask puts one question to the daemon and renders its answer.

$ sayfirst ask --capability example.effect --scope local --socket /run/user/1000/sayfirst.sock
verified: true (server_uid 1000, expected 1000)
outcome: allow
reason: policy_allows
capability: example.effect in scope local
decision: decision-1 at 2026-09-04T20:11:45.929523+00:00
policy version: sha256:44d91909ffe6283e67c73bad39d698448aa5e8bbdc1b5aa64a635a604e12f7e1

When the daemon cannot be reached, it says that, and it says it as its own result rather than as a refusal:

$ sayfirst ask --capability example.effect --socket /run/user/1000/absent.sock
verified: false (server_uid not stated, expected not stated)
could not ask: unreachable: [Errno 2] No such file or directory
retryable: true
$ echo $?
4

It exits 0 on allow, 1 on deny, 5 on suspend, 3 when the request was refused and 4 when the control plane could not be asked — because article 1 requires that "denied" and "could not ask" never read as each other. There is no --url and nothing that takes a token: the boundary says who the caller is, and it says so from the socket (article 6).

The gate

scripts/gate.sh is the whole gate: format, lint, tests, and the guard that installs this distribution into an empty environment and reads back everything that arrived with it. That last one is article 13 made mechanical rather than promised — and it proves on every run that it can fail, by planting a web framework into the environment that has just passed and requiring the check to reject it.

sayfirst-contract is on no index, because article 0 forbids publishing anything until the marks are filed. So the gate is told where a checkout of the control plane's repository is:

$ SAYFIRST_CONTRACT_SOURCE=../sf-control-plane-lt ./scripts/gate.sh

It fails, rather than skipping, when it is not told. A skipped guard is a guard that cannot fail.

What binds here

The constitution of the open control plane — CONSTITUTION.md, in that repository — binds this repository too. Its article 0 says so: it binds "this repository and every other open repository of the project", and an open repository adopts it by pointer, never by copy, because a copy drifts.

That pointer became a link in the act that published this repository, and was not one before. The open control plane is published as a repository created fresh (article 0), and until it existed the only URL this document could have carried was the one repository the project has decided never to publish — a link that would have resolved for nobody from the first public minute, in the document a newcomer reads first. docs/publication-checklist.md orders the control plane before this client for that reason, and tests/test_pointers_survive_publication.py still reddens on any reader-facing link to the repository that is never published — it is what kept the promise from being made early. The same applies to TRADEMARKS.md, which is why this repository carries one that points rather than one that copies.

The articles this repository will answer to first:

  • article 1 — a client explains and invokes control semantics; it never defines them, never keeps a decision past the lifetime it was given, and never derives an answer the control plane did not give;
  • article 13 — the client depends on the contract distribution and never on the server distribution, so installing it never installs a web framework or a database layer;
  • article 14 — nothing here imports, names or is shaped by a private product; files that arrive from a private repository arrive by copy with a provenance review;
  • article 15 — Apache-2.0, contributions under the Developer Certificate of Origin, read on every pull request by scripts/check_developer_certificate_of_origin.py, which refuses a commit carrying no well-formed sign-off and refuses a range it cannot read rather than reporting it as passing.

The name

The product and the command are sayfirst, decided by the operator on 2026-09-04. Every distribution the open side publishes takes it as a prefix, the way the retired placeholder always said they would: this CLI as sayfirst-cli, and the plane as sayfirst-control-plane where the plane is named. Which distributions this repository ships is a different question — that is the partition, and it is not decided.

Who holds the command — decided, 2026-09-05

This repository claims the distribution sayfirst-cli, the import package sayfirst_cli and the console script sayfirst. Until 2026-09-05 that was an assumption, stated as one: the control plane's repository claimed the same three names for its operator surface, each repository's guard read its own manifest and passed, and no test anywhere could see the other tree. The operator settled it on 2026-09-05, and all three names in all three forms belong to the product command-line interface, which is this repository.

The control plane's repository is giving them up: its operator surface — the commands that inspect its daemon — takes the daemon's own form of the product name instead, the conventional Unix shape in which the daemon and the commands that inspect it share one binary. That change is on a branch there, dated 2026-09-05 and not merged, so until it merges both trees still declare the three names. Nothing had been published under either claim (article 0), so the collision was a fact about two source trees and never about an installed environment. docs/PARTITION.md records the question and its answer as Q-D.

Deciding the name did not unblock publishing on its own. Article 0 forbids publishing anything under it — no package on an index, no public repository, no announcement — until three conditions hold together:

  1. the name is decided — met, 2026-09-04;
  2. the marks are filed — no file here can read a registry, so this repository asserts nothing about it and a session reading this file must not read the decided name as evidence of it. The date the filing was made and the reference it produced are recorded on docs/publication-checklist.md, and article 0 puts that act before any public repository of this project;
  3. every occurrence of the placeholder is replaced — done here; the condition is project-wide, and the other repositories' occurrences are not this repository's to change.

What is still not decided

  • The partition. Which parts of the private CLI cross, and what is renamed on the way, is measured privately and executed slice by slice. docs/PARTITION.md proposes the open command surface, names the three questions it does not close, and says plainly which of its lines is a decision and which is a choice it made to keep moving. Two of its questions are now answered — Q-D, the name collision, and Q-A, the evidence surface — and both stay in the document, marked closed and dated.

Boundary with the control plane

The control plane's operator surface keeps status, doctor, policy show|history and plugins list, which inspect its daemon's own state. systems {register,retire} also stays there pending its separate question. The 2026-09-05 decision assigns trace, explain and evidence {audit,history,exports,export} to this product client: they read the decisions and evidence the plane supplied. Q-B, Q-C and Q-E remain open; Q-D was already closed.

Metadata

Release files for sayfirst-cli 0.2.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.2.0
File Size Uploaded
sayfirst_cli-0.2.0.tar.gz 281.6 kB Details

Built distribution (wheel)

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

Total release size: 381.8 kB

Release files / sayfirst_cli-0.2.0.tar.gz

Download URL sayfirst_cli-0.2.0.tar.gz
Size 281.6 kB
Tags Source
SHA-256 checksum
How to use checksums
dc4ac3184e78aaf3069c76700085c5eae0d6d4e92ea39904e44e3265cc586567
BLAKE2b-256 checksum
How to use checksums
da6d1374d45dd324798b760bbe9b8c3bab750902e0be809d46fd67a474254320
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 17, 2026.

Transparency log

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

Download URL sayfirst_cli-0.2.0-py3-none-any.whl
Size 100.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c809fa9149717db7c8649edb3afd0b707789f83e00fa5b8a82123fc0b16aae9a
BLAKE2b-256 checksum
How to use checksums
1b6a7f4cdb8bdde904b719b2c85c9cb9048106d14d8da93d278ae4eae8a999ee
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 17, 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

0.3.0

2 release files

This release

0.2.0 This release

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