From fdd0d6c29f5fb75c5598870c943890eff71f1290 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 23:05:52 +0000 Subject: [PATCH] metadata: say what sayfirst is on the index, and link the control plane The pages of `sayfirst-cli` and `sayfirst` now open with what the product is, carry keywords and the classifiers that describe it, and link to the issues, the control plane's repository and its page on the index. The pointer guard keeps the three URLs into this repository and holds every other URL to a closed table of decided addresses; a watched firing proves it refuses a misspelt project name on the index. The umbrella's URLs are held equal to the client's, key by key. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fw7bWuYredHEd7kt4c7wom Signed-off-by: Claude --- CHANGELOG.md | 7 +++ README.md | 18 ++++++- pyproject.toml | 33 +++++++++++-- tests/test_pointers_survive_publication.py | 49 +++++++++++++++++-- ...mbrella_metadata_agrees_with_the_client.py | 3 +- umbrella/README.md | 15 +++++- umbrella/pyproject.toml | 25 +++++++++- 7 files changed, 138 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 249fde5..430923d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ declares; a release with no section fails the gate. ## Unreleased +- The index pages of `sayfirst-cli` and `sayfirst` say what the product is in + their first line, carry keywords and the classifiers that describe it (the + console, the audience, Linux and macOS, security), and link to the issues, + to the control plane's repository and to its page on the index. The pointer + guard holds each of those links to the one address decided for it. Both + READMEs open with the same summary and the same links. + ## 0.3.3 - A second distribution is built from this repository: `sayfirst`, the product diff --git a/README.md b/README.md index 1e1ff84..a2bf4e9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ -# `sayfirst` — ask before you act +# SayFirst CLI (`sayfirst-cli`) — ask before you act + +**`sayfirst-cli` is the open-source command line of the [SayFirst control plane +(`sayfirst-control-plane`)](https://github.com/fredaime/sayfirst-control-plane): +runtime governance for AI agents and the programs they drive.** A governed +program asks a local daemon before an effect and does only what was allowed; +`sayfirst` puts that question in front of a program that was never written for +it, holds what waits for a person until one approves it, and exports evidence +anyone can verify offline. + +[![PyPI - sayfirst-cli](https://img.shields.io/pypi/v/sayfirst-cli.svg?label=PyPI%20sayfirst-cli)](https://pypi.org/project/sayfirst-cli/) +[![PyPI - sayfirst-control-plane](https://img.shields.io/pypi/v/sayfirst-control-plane.svg?label=PyPI%20sayfirst-control-plane)](https://pypi.org/project/sayfirst-control-plane/) + +**Links:** [`sayfirst-cli` on GitHub](https://github.com/fredaime/sayfirst-cli) · +[`sayfirst-cli` on PyPI](https://pypi.org/project/sayfirst-cli/) · +[control plane on GitHub](https://github.com/fredaime/sayfirst-control-plane) · +[control plane on PyPI](https://pypi.org/project/sayfirst-control-plane/) This repository is **the command**. `sayfirst` is the open-source product command-line interface of the `sayfirst` control plane: it asks the daemon diff --git a/pyproject.toml b/pyproject.toml index 6037870..fbc4b00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,17 +2,37 @@ [project] name = "sayfirst-cli" version = "0.3.3" -description = "The product command-line interface of the sayfirst control plane" +description = "SayFirst CLI (sayfirst-cli): ask the SayFirst control plane before an effect — govern AI agents and programs, approve what waits for a person, verify the evidence offline" readme = "README.md" requires-python = ">=3.12,<3.15" license = "Apache-2.0" authors = [{ name = "fredaime", email = "frederic.aime@gmail.com" }] +keywords = [ + "sayfirst", + "sayfirst-cli", + "sayfirst-control-plane", + "ai-agents", + "agent-security", + "ai-security", + "llm-security", + "runtime-governance", + "policy-enforcement", + "human-in-the-loop", + "audit-trail", +] classifiers = [ "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Topic :: Security", + "Topic :: System :: Systems Administration", ] # Article 13 and article 14: the client depends on the contract distribution and # on nothing else this project publishes. The closure this line produces is what @@ -26,13 +46,18 @@ dependencies = ["sayfirst-contract==0.3.3", "sayfirst-boundary==0.3.3"] # the act that published it (2026-09-17) under the names the operator chose for # that act. The guard # `tests/test_pointers_survive_publication.py` holds them, so a rename lands -# here and there together or not at all. No licence classifier: the licence is -# declared as an expression above, and an index refuses a distribution that -# carries both. +# here and there together or not at all. The last two send a reader to the +# sibling the client asks — the control plane's repository and its page on the +# index — and the same guard holds that they point there and nowhere else. No +# licence classifier: the licence is declared as an expression above, and an +# index refuses a distribution that carries both. [project.urls] Repository = "https://github.com/fredaime/sayfirst-cli" Documentation = "https://github.com/fredaime/sayfirst-cli/blob/main/README.md" Changelog = "https://github.com/fredaime/sayfirst-cli/blob/main/CHANGELOG.md" +Issues = "https://github.com/fredaime/sayfirst-cli/issues" +"Control plane" = "https://github.com/fredaime/sayfirst-control-plane" +"Control plane on PyPI" = "https://pypi.org/project/sayfirst-control-plane/" [project.scripts] sayfirst = "sayfirst_cli.main:run" diff --git a/tests/test_pointers_survive_publication.py b/tests/test_pointers_survive_publication.py index 9f7fe73..e9f86c7 100644 --- a/tests/test_pointers_survive_publication.py +++ b/tests/test_pointers_survive_publication.py @@ -118,12 +118,42 @@ def test_the_rule_leaves_a_local_sibling_path_alone() -> None: PUBLIC_CLIENT = "https://github.com/fredaime/sayfirst-cli" +#: The three URLs every page of this repository's distributions carries, each +#: into this repository. +OWN_URLS = frozenset({"Repository", "Documentation", "Changelog"}) + +#: Every other URL a page here may carry, and the one address each may hold: +#: this repository's issues, and the control plane the client asks — its +#: repository and its page on the index. A closed table rather than a pattern, +#: so a misspelt project name on the index, which would send a reader to a +#: page anybody could register, is a failure here rather than a guess. +CROSS_LINKS = { + "Issues": f"{PUBLIC_CLIENT}/issues", + "Control plane": PUBLIC_CONTROL_PLANE, + "Control plane on PyPI": "https://pypi.org/project/sayfirst-control-plane/", +} + + def project_urls(root: Path) -> dict[str, str]: """The `[project.urls]` table of the distribution at `root`, or an empty one.""" project = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"] return dict(project.get("urls") or {}) +def misdirected(urls: dict[str, str]) -> list[str]: + """Every entry of this table that sends a reader somewhere this module has not decided.""" + found = [f"missing: {name}" for name in sorted(OWN_URLS - set(urls))] + for name, url in sorted(urls.items()): + if NEVER_PUBLISHED in url: + found.append(f"never published: {name} = {url}") + elif name in OWN_URLS: + if not url.startswith(PUBLIC_CLIENT): + found.append(f"outside this repository: {name} = {url}") + elif CROSS_LINKS.get(name) != url: + found.append(f"undecided: {name} = {url}") + return found + + def test_every_published_url_of_this_distribution_is_a_public_repository() -> None: """A package page is read by people who hold nothing of this project. @@ -133,10 +163,20 @@ def test_every_published_url_of_this_distribution_is_a_public_repository() -> No module already refuses in `README.md` and `NOTICE`, made to a wider audience. """ urls = project_urls(REPOSITORY) - assert set(urls) == {"Repository", "Documentation", "Changelog"}, urls - for name, url in urls.items(): - assert url.startswith(PUBLIC_CLIENT), (name, url) - assert NEVER_PUBLISHED not in url, (name, url) + assert misdirected(urls) == [], urls + assert set(CROSS_LINKS) <= set(urls), urls + + +def test_the_url_rule_fires_on_a_misspelt_page_of_the_index() -> None: + """WATCHED FIRING, on the cross-link that is easiest to get wrong: a project + name on the index that nobody here publishes.""" + urls = { + **project_urls(REPOSITORY), + "Control plane on PyPI": "https://pypi.org/project/sayfirst-controlplane/", + } + assert misdirected(urls) == [ + "undecided: Control plane on PyPI = https://pypi.org/project/sayfirst-controlplane/" + ] def test_the_url_rule_fires_on_a_pointer_to_the_repository_that_is_never_published( @@ -156,3 +196,4 @@ def test_the_url_rule_fires_on_a_pointer_to_the_repository_that_is_never_publish assert urls, "the reader saw no table: the probe proves nothing" assert not all(url.startswith(PUBLIC_CLIENT) for url in urls.values()) assert any(NEVER_PUBLISHED in url for url in urls.values()) + assert f"never published: Repository = {urls['Repository']}" in misdirected(urls) diff --git a/tests/test_umbrella_metadata_agrees_with_the_client.py b/tests/test_umbrella_metadata_agrees_with_the_client.py index 85bf114..0a416cc 100644 --- a/tests/test_umbrella_metadata_agrees_with_the_client.py +++ b/tests/test_umbrella_metadata_agrees_with_the_client.py @@ -65,7 +65,8 @@ def test_every_url_of_the_umbrella_is_this_public_repository() -> None: client = project_urls(REPOSITORY) assert set(urls) == set(client), urls assert urls["Repository"] == client["Repository"] == PUBLIC_CLIENT - assert urls["Changelog"] == client["Changelog"] + for key in set(urls) - {"Documentation"}: + assert urls[key] == client[key], key page = urls["Documentation"].removeprefix(f"{PUBLIC_CLIENT}/blob/main/") assert page == "umbrella/README.md", urls["Documentation"] assert (REPOSITORY / page).is_file(), page diff --git a/umbrella/README.md b/umbrella/README.md index a74fde6..ad7a7c7 100644 --- a/umbrella/README.md +++ b/umbrella/README.md @@ -1,5 +1,18 @@ -# `sayfirst` — ask before you act +# SayFirst (`sayfirst`) — ask before you act + +**SayFirst is open-source runtime governance for AI agents: a local control +plane a program asks before an effect, a person who approves what waits, and +evidence anyone can verify offline.** + +[![PyPI - sayfirst](https://img.shields.io/pypi/v/sayfirst.svg?label=PyPI%20sayfirst)](https://pypi.org/project/sayfirst/) +[![PyPI - sayfirst-cli](https://img.shields.io/pypi/v/sayfirst-cli.svg?label=PyPI%20sayfirst-cli)](https://pypi.org/project/sayfirst-cli/) +[![PyPI - sayfirst-control-plane](https://img.shields.io/pypi/v/sayfirst-control-plane.svg?label=PyPI%20sayfirst-control-plane)](https://pypi.org/project/sayfirst-control-plane/) + +**Links:** [`sayfirst-cli` on GitHub](https://github.com/fredaime/sayfirst-cli) · +[`sayfirst-cli` on PyPI](https://pypi.org/project/sayfirst-cli/) · +[control plane on GitHub](https://github.com/fredaime/sayfirst-control-plane) · +[control plane on PyPI](https://pypi.org/project/sayfirst-control-plane/) This distribution is the product, installed whole. It installs no code of its own: it depends on the three distributions that carry the three commands, each diff --git a/umbrella/pyproject.toml b/umbrella/pyproject.toml index cf70e41..fd65b56 100644 --- a/umbrella/pyproject.toml +++ b/umbrella/pyproject.toml @@ -2,17 +2,37 @@ [project] name = "sayfirst" version = "0.3.3" -description = "The sayfirst control plane, its daemon and its command, installed whole" +description = "SayFirst, installed whole: the control plane, its daemon and its command — runtime governance for AI agents, asked before an effect" readme = "README.md" requires-python = ">=3.12,<3.15" license = "Apache-2.0" authors = [{ name = "fredaime", email = "frederic.aime@gmail.com" }] +keywords = [ + "sayfirst", + "sayfirst-cli", + "sayfirst-control-plane", + "ai-agents", + "agent-security", + "ai-security", + "llm-security", + "runtime-governance", + "policy-enforcement", + "human-in-the-loop", + "audit-trail", +] classifiers = [ "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Topic :: Security", + "Topic :: System :: Systems Administration", ] # This distribution is the product installed whole, and it is not the client. # It depends on the client, on the daemon and on the daemon's operator surface, @@ -31,6 +51,9 @@ dependencies = [ Repository = "https://github.com/fredaime/sayfirst-cli" Documentation = "https://github.com/fredaime/sayfirst-cli/blob/main/umbrella/README.md" Changelog = "https://github.com/fredaime/sayfirst-cli/blob/main/CHANGELOG.md" +Issues = "https://github.com/fredaime/sayfirst-cli/issues" +"Control plane" = "https://github.com/fredaime/sayfirst-control-plane" +"Control plane on PyPI" = "https://pypi.org/project/sayfirst-control-plane/" # Each entry mirrors the declaration of the distribution that owns the command, # word for word. An installer that exposes only the commands of the package it