Skip to content

The README says it applies to any domain, and shows how - #122

Merged
arpanghoshal merged 2 commits into
mainfrom
readme-domain-neutral
Sep 7, 2026
Merged

arpanghoshal merged 2 commits into
mainfrom
readme-domain-neutral

Conversation

@arpanghoshal

@arpanghoshal arpanghoshal commented Sep 7, 2026

Copy link
Copy Markdown
Member

The problem

At first glance the README read as a payments library, and it never said otherwise. It was not the headings — only one of ten named money — it was that every concrete thing under them was a Stripe refund.

before after
refund 67 35
stripe 23 11
34 21
payment 18 3

Six of the seven runnable blocks were refunds, and the one multi-domain policy in the file sat collapsed inside a <details> where a skimming reader never reaches it. The nine domain templates under examples/policies/ — the best evidence of breadth in the repository — were named in half a sentence downpage.

The approach

Variety in the concrete, not a retreat into philosophy. The hero already states the thesis without a domain — "Autonomy belongs to the action, not the agent" — and replacing the refund story with an abstraction would buy generality at the price of anyone knowing what the library does. The refund keeps its place: AMBIGUOUS versus FAILED is abstract until money moves twice. Everything after it changes, and a new section says outright what the general case is.

Commit 1 — the refund is the example, not the scope

  • One line under the demo GIF says the refund is an instance, and links to the new section. It is the first prose a stranger reads and it is domain-free.
  • The quick start moves to IAM. Its policy carries three domains and all three decisions in twelve lines: crm.update_record allows, iam.grant_role asks a human for anything above read-only, audit.log.delete denies. The approval-binding lesson is unchanged — a human approves admin for u_412, the agent tries owner, and it is refused — and it now demonstrates role_in rather than a third amount band, so the policy language stops looking like it only understands money. The closing paragraph says in one sentence that money is the same shape with amount_gte/amount_lte, and keeps the negative-amount warning.
  • The gateway example becomes delete_document / search_documents.
  • A defect fixed: the output block after approved.py was the wrong command's output — it repeated ctrlrun approve's two lines with a stray D appended, where the script prints three lines of its own. Every output block in the section is now the real output of the block above it, captured from a run.

Commit 2 — say it applies to any domain, and show how

A new section, "The same shape in nine domains", placed before the policy file.

The mechanism first, because that is the part a reader can check: nothing in the kernel knows what a refund is. An action is a name, canonical arguments, an effect key and a resource, and the three questions asked of it do not vary by domain. Two things carry the domain and the operator writes both:

  • The effect key is the only domain knowledge in the systemrefund:{payment_id}, namespace:{cluster}:{name}, grant:{user_id}:{role}, prescription:{patient_id}:{drug}.
  • Conditions are arguments, not amounts. The language is <argument>_<op>, so the same operators read replicas_lte: 10 and host_count_lte: 1 — both already in the shipped templates, and the evidence that a band is available to a domain that has never issued an invoice.

Then a table, one row per template under examples/policies/: an action that is autonomous, one a human decides, one that is never allowed, each linked to the file it came from. The security row is called out in the prose because it argues against the amount-shaped reading of the whole idea — adding a deny rule to a firewall is autonomous and adding an allow rule is not, and no threshold would have told you that.

The table is tested

It is a claim about files that change, so tests/test_examples.py reads the rows back out of the README, resolves each to the template its own cell links to, and asserts the cited action exists there and can reach the decision it is cited for — reachable, not exclusive, since three rows cite a banded action that is honestly in two columns.

Mutation-tested:

mutation result
firewall.add_allow_rule moved into the autonomous column ...carries_the_decision_it_is_cited_for[security] fails
a cited action the template does not define ...carries_the_decision_it_is_cited_for[healthcare] fails
a row deleted test_the_readme_domain_table_has_a_row_per_template fails

What is left

The demo transcript (five refund scenarios) and the ctrlrun verify report (stripe.refund throughout) are still money. Both are generated from real output with tests that hold them byte-exact, so diversifying them means changing the demo scenarios and re-recording the GIF — a separate change. Scenario 3 is worth noting: the prose describes it as "two workers running one kubectl delete namespace" and the code implements a refund.

ctrlrun verify against examples/authority/devops.yaml was considered for the verify block and rejected — it reports 1/1 with 10 N/A, which is a weaker showcase than payments' 11/11.

Checks

  • tools/docs_audit/snippets.py README.md — 7 runnable blocks, 0 failed, offline
  • tools/docs_audit/lint.py and links.py — 0 findings, 0 broken
  • ruff check clean
  • full suite: 4369 passed, 47 skipped (was 4359 — ten new)

@arpanghoshal arpanghoshal changed the title The refund is the example, not the scope The README says it applies to any domain, and shows how Sep 7, 2026
At first glance the README read as a payments library. It was not the
headings — only one names money — it was that every concrete thing under
them was a Stripe refund: 67 occurrences of "refund", 23 of "stripe", 34
euro signs, against 4 mentions of a namespace and 3 of IAM. Six of the
seven runnable blocks were refunds, and the only multi-domain policy in
the file sat collapsed inside a <details> where a skimming reader never
reaches it. The breadth was there; nothing above the fold showed it.

The fix is variety in the concrete, not a retreat into philosophy. The
refund story keeps its place: AMBIGUOUS versus FAILED is abstract until
money moves twice, and it is the sharpest instance we have. What changes
is everything after it.

- One line under the demo GIF says the refund is an instance. It is the
  first prose a stranger reads and it is domain-free.
- The quick start moves to IAM, and its policy carries three domains and
  all three decisions in twelve lines: crm.update_record allows,
  iam.grant_role asks a human for anything above read-only,
  audit.log.delete denies. The approval-binding lesson is unchanged and
  now demonstrates role_in rather than a third amount band, so the
  policy language no longer looks like it only understands money. The
  closing paragraph says in one sentence that money is the same shape
  with amount_gte/amount_lte, and keeps the negative-amount warning.
- The gateway example becomes delete_document / search_documents.

Also fixes a defect: the output block after approved.py was the wrong
command's output. It repeated `ctrlrun approve`'s two lines with a stray
"D" appended, where the script prints three lines of its own. Every
output block in the section is now the real output of the block above
it, captured from a run.

refund 67 -> 35, stripe 23 -> 11, payment 18 -> 3. What remains is the
demo transcript and the verify report, both generated from real output
and both a larger change.
The previous change stopped the README reading as payments-only. It did
not say what the general case is, so a reader in healthcare or devops
still had to infer it from a quick start about IAM.

A new section says it and shows it. The mechanism first, because that is
the part a reader can check: nothing in the kernel knows what a refund
is, an action is a name, canonical arguments, an effect key and a
resource, and the three questions asked of it do not vary by domain. Two
things carry the domain and the operator writes both -- the effect key,
which is the only domain knowledge in the system, and the conditions,
which are `<argument>_<op>` and not amounts. `replicas_lte: 10` and
`host_count_lte: 1` are in the shipped templates and are the evidence
that a band is available to a domain that has never issued an invoice.

Then a table, one row per template under examples/policies/, naming an
action that is autonomous, one a human decides and one that is never
allowed. The nine files were mentioned in half a sentence downpage; they
are the best evidence of breadth in the repository and nothing showed
them. The security row is called out in the prose because it is the one
that argues against the amount-shaped reading of the whole idea: adding
a deny rule to a firewall is autonomous and adding an allow rule is not,
and no threshold would have told you that.

The table is a claim about files that change, so it is tested.
test_examples.py reads the rows back out of the README, resolves each to
the template its own cell links to, and asserts the cited action exists
there and can reach the decision it is cited for -- reachable, not
exclusive, since three rows cite a banded action that is honestly in two
columns. A row that drifts fails there.

The header line under the GIF now links to the section.
@arpanghoshal
arpanghoshal merged commit 70d215e into main Sep 7, 2026
10 checks passed
@arpanghoshal
arpanghoshal deleted the readme-domain-neutral branch September 7, 2026 12:24
arpanghoshal added a commit that referenced this pull request Sep 13, 2026
…hree steps

The README is the GitHub and PyPI surface and ctrlrun.dev is the marketing one, and until
now they led with different sentences. The header now opens with the homepage's H1 and lede
verbatim, the first section carries its extra-zero example, "Three ways to use it" carries its
line about agents you can and can't modify, "How it works" walks the seven steps of its diagram
in the same order, the tiers are the homepage's three, and the file closes on its footer line.
tests/test_readme_assets.py pins the strings here; the docs repository reads them out of
index.mdx on a branch of the same name, so a drift on either side fails CI on the other.

The animation was `ctrlrun demo`'s first two scenarios, piped through sed and a pacing loop.
It was a true recording and a hard one to read: a stranger saw a transcript with two pipes in
the command line and no code, and could not tell from it what CTRLRun is. It is now a recording
of the README's own "Use it in three steps" section: the policy file, the agent that runs one
refund and is stopped on the next, the human answering from the shell, and the same approval
refused for a different amount. The three files it records are committed under
docs/assets/recording/ and asserted byte for byte equal to the README's runnable blocks; the
lines it ends on are committed as before, and the test now runs those files against the
library instead of the demo. Both programs exit non-zero on the path where the refusal did
not happen, so a recording of a broken build fails instead of looking fine. The terminal is
65 columns at 20px rather than 120 at 17px, so the text is legible at the width GitHub
renders it.

The quick start moves from IAM back to a refund so that the README, the animation, the
homepage and the quickstart tell one story; the multi-domain point #122 made is kept by the
sentence after it and by the nine-domain table. "How it works" gains a paragraph on budgets,
scope and tasks, which 0.9 shipped and the README did not mention.

Signed-off-by: arpan <contact@arpanghoshal.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant