CONTROL THE ACTION. KEEP THE AUTONOMY.
+Let agents act. Keep control of what happens next.
+CTRLRun checks consequential actions before they reach real systems. Stop duplicate actions, require human approval, and pause unsafe retries.
+ +Open source. Your agent. Your systems. Your rules.
+ONE LOST RESPONSE. TWO REFUNDS.
An agent refunds $500. The provider processes it, but the response is lost. The agent thinks it failed.
Retry the refund another $500 may go out.
Pause the retry. Confirm what happened before trying again.
MAKE IT RELEVANT TO YOU
No login. No API key. No setup.
FOR THE PEOPLE BUILDING IT
Wrap the call. Define your rules. Keep a record of what happened.
Python, agent frameworks, or an MCP gateway.
The execution boundary stays the same.
pip install ctrlrun{"import ctrlrun\n\n@ctrlrun.protect(\n \"stripe.refund\",\n effect=\"refund:{payment_id}\"\n)\ndef refund(payment_id, amount):\n ..."}Illustrative wrapper. The quickstart adds your policy, identity, and approval flow.
WHEN THE ACTIONS ARE REAL
Have an agent taking real actions? Get engineering help deciding what can run automatically, what needs approval, and when to stop and confirm.
Paid architecture reviews, integration sprints, and ongoing production support. CTRLRun OSS stays free.
PRODUCTION AGENTS. ENGINEERING HELP.
Put execution controls where your architecture needs them.
A focused review of your agent’s actions, permissions, approvals, and retry behavior. Leave with an action map: what can run, what needs a person, and what must wait for confirmation.
A paid architecture review of tool boundaries, approval flows, duplicate execution, uncertain outcomes, and action records. A practical control map and prioritized implementation plan.
Hands-on integration, policy design, approval workflows, persistence, retry handling, reconciliation, and production testing. Scope agreed around your architecture.
A recurring engagement for new actions, policy changes, upgrades, incident analysis, and rollout reviews.
Building it yourself? Start integrating with CTRLRun OSS →
SIX QUESTIONS. A CLEARER EXECUTION BOUNDARY.
Find the gaps before your agent takes the next action.
Your answers stay in this browser. No email needed to see your result.
Architecture review for {domain}
} + {risk &&Risk check: {risk}
} + + {prepared &&{sent ? "Thank you. The team will follow up at the work email you provided." : "Review the context below, then send it to the CTRLRun team."}
{brief}{copied ? 'Brief copied. Paste it into an email when you’re ready.' : sent ? 'Your request was accepted for delivery. No further submission is needed.' : 'Your request has not been sent yet.'}
{error &&{error} contact@arpanghoshal.com
}YOUR EXECUTION RISK CHECK
{result.active ? 'Your answers identify ' + result.count + ' execution-risk pattern' + (result.count === 1 ? '' : 's') + ' CTRLRun is designed to address.' : 'You have not selected a consequential action yet. Use these questions before granting agents permission to act.'}
+ {result.unknowns > 0 &&{result.unknowns} answer{result.unknowns === 1 ? ' needs' : 's need'} confirmation. Uncertainty is a reason to inspect your architecture, not proof that a control is missing.
} +{pattern.advice} Implementation guide →
Your answers indicate fewer of these patterns. Validate the controls with concurrency, changed-approval, and lost-response tests before rollout.
} +A planning aid based on your answers, not an audit or a safety certification. High means at least three indicated patterns; “Needs review” means one or more patterns or unknowns.
+ track('protect_clicked')}>Get a production architecture review ↗ +No matching domain. Try “Payments” or “DevOps”.
} +AGENT REQUESTS
{stateKey === 'mismatch' ? changed : original}
{stateKey === 'mismatch' ? 'A person approved: ' + original : 'One consequential action. Checked before execution.'}
CTRLRun_✓ Action recognized
{result.reason}
{result.rule}
{result.code} +Interactive simulation · Example rules, not industry defaults. No real actions are taken.{physical ? ' CTRLRun governs the software authorization and execution workflow; physical safety controls remain separate.' : ''}{domain === 'Healthcare Operations' ? ' Administrative workflows only; no diagnosis or clinical decisions.' : ''}
+Review where execution controls belong in your architecture.
with inline style"
+ assert style, "docs/docs/try-it.mdx: the transcript box is no longer a with inline style"
box = dict(re.findall(r"(\w+):\s*\"([^\"]*)\"", style.group(1)))
assert "maxHeight" in box, "minHeight without maxHeight is a box that can only grow"
@@ -210,7 +210,7 @@ def test_the_transcript_does_not_depend_on_how_the_theme_lays_the_box_out():
assert "output.appendChild(node)" not in SCRIPT, "a line appended straight into the "
style = re.search(r" with inline style"
+ assert style, "docs/docs/try-it.mdx: the transcript box is no longer a with inline style"
box = dict(re.findall(r"(\w+):\s*\"([^\"]*)\"", style.group(1)))
assert box.get("display") == "block", "the theme's flex would lay the lines out in a row"
@@ -502,6 +502,10 @@ def walk(node: object) -> None:
walk(value)
walk(document["navigation"])
- for slug in ("try-it", "verify/get-the-badge", "study/does-your-framework-double-execute"):
+ for slug in (
+ "docs/try-it",
+ "docs/verify/get-the-badge",
+ "docs/study/does-your-framework-double-execute",
+ ):
assert slug in found, f"{slug} is not in docs.json"
assert f"`{slug}`" in plan, f"{slug} has no row in docs/SEO.md"
diff --git a/tests/test_examples.py b/tests/test_examples.py
index c5c971c1..8b208453 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -40,7 +40,7 @@
}
#: Directories under `examples/` that are not one of §1.1's failure scenarios: the sector
-#: templates, and item 8's ACS integration example (SPEC-v0.2 §9, `docs/ACS.md`).
+#: templates, and item 8's ACS integration example (SPEC-v0.2 §9, `docs/docs/ACS.md`).
NOT_A_SCENARIO = (
"policies",
"acs",
diff --git a/tests/test_framework_probe.py b/tests/test_framework_probe.py
index 9890ff66..4a0f6563 100644
--- a/tests/test_framework_probe.py
+++ b/tests/test_framework_probe.py
@@ -543,7 +543,12 @@ def test_no_top_level_document_claims_the_harness_was_run_against_a_framework():
"""A sentence naming one of these four outside the harness's own directory is the shape of
an overclaim, so there are none — and this test is what keeps it that way when somebody
writes the release post."""
- checked = ["README.md", "CHANGELOG.md", "docs/verify.md", "docs/OWASP-AGENTIC-TOP10.md"]
+ checked = [
+ "README.md",
+ "CHANGELOG.md",
+ "docs/docs/verify.md",
+ "docs/docs/OWASP-AGENTIC-TOP10.md",
+ ]
offending = []
for name in checked:
diff --git a/tests/test_owasp_mapping.py b/tests/test_owasp_mapping.py
index 345fb864..1459a4ff 100644
--- a/tests/test_owasp_mapping.py
+++ b/tests/test_owasp_mapping.py
@@ -1,4 +1,4 @@
-"""`docs/OWASP-AGENTIC-TOP10.md`. SPEC-v0.4 §6; T121.
+"""`docs/docs/OWASP-AGENTIC-TOP10.md`. SPEC-v0.4 §6; T121.
The mapping is complete in **both** directions, and the second direction is the one that makes
the first credible: every entry with no guarantee is listed by name under "Not covered by
@@ -15,9 +15,9 @@
from ctrlrun.verify import guarantees as reg
REPO_ROOT = Path(__file__).resolve().parents[1]
-MAPPING = REPO_ROOT / "docs" / "OWASP-AGENTIC-TOP10.md"
+MAPPING = REPO_ROOT / "docs" / "docs" / "OWASP-AGENTIC-TOP10.md"
README = REPO_ROOT / "README.md"
-VERIFY_DOC = REPO_ROOT / "docs" / "verify.md"
+VERIFY_DOC = REPO_ROOT / "docs" / "docs" / "verify.md"
#: The edition this document is written against, as recorded in it. Derived from the
#: OWASP-owned `OWASP/secure-agent-playbook` repository and corroborated against two
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
index 08f791dc..67891f07 100644
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -426,7 +426,7 @@ def test_the_manifest_check_would_notice_a_file_it_does_not_ship():
def test_the_package_never_encodes_a_token():
- """`docs/CLAIMS.md` — "CTRLRun issues no credential and defines no identity format".
+ """`docs/docs/CLAIMS.md` — "CTRLRun issues no credential and defines no identity format".
A claim in the README needs a test, and this one is structural: the package verifies
tokens and never mints one, so no module may call `jwt.encode`, and only `jwt_identity`
@@ -445,7 +445,7 @@ def test_the_package_never_encodes_a_token():
def test_every_test_docs_claims_cites_exists():
- """`docs/CLAIMS.md` maps every README sentence to the test that proves it, and a citation
+ """`docs/docs/CLAIMS.md` maps every README sentence to the test that proves it, and a citation
naming a test that does not exist is the same false claim the file exists to prevent.
It has been regenerated by hand at three releases now, and the names drift: a test gets
@@ -453,7 +453,7 @@ def test_every_test_docs_claims_cites_exists():
nothing. This is cheap and it is the only thing standing between the two.
"""
- claims = REPO_ROOT / "docs" / "CLAIMS.md"
+ claims = REPO_ROOT / "docs" / "docs" / "CLAIMS.md"
if not claims.exists(): # pragma: no cover - not a checkout
pytest.skip("no repository checkout")
cited = set(re.findall(r"`(test_[A-Za-z0-9_]+)`", claims.read_text(encoding="utf-8")))
@@ -653,13 +653,13 @@ def test_the_readme_carries_no_relative_link_and_no_relative_image():
def test_the_relative_link_check_would_see_one():
"""The negative test above proves nothing unless a relative link would actually fail it."""
- text = " and [docs](docs/CLAIMS.md) and [ok](https://x.test)"
+ text = " and [docs](docs/docs/CLAIMS.md) and [ok](https://x.test)"
targets = re.findall(r"\]\(([^)]+)\)", text)
relative = [t for t in targets if not t.startswith(("https://", "http://", "mailto:", "#"))]
- assert relative == ["docs/assets/logo.svg", "docs/CLAIMS.md"]
+ assert relative == ["docs/assets/logo.svg", "docs/docs/CLAIMS.md"]
-# --- T139: the README's adapter section, and `docs/adapters.md` -----------------------------
+# --- T139: the README's adapter section, and `docs/docs/adapters.md` -----------------------------
def _readme() -> str:
@@ -704,7 +704,7 @@ def test_T139_the_adapter_section_names_prevention_and_attribution():
#: Words this project will not claim before the milestone that earns them. They may still be
#: *written* -- the badge paragraph says a passing run "does not mean secure, safe, compliant,
-#: certified or audited", and `docs/adapters.md` says no adapter describes itself as conformant.
+#: certified or audited", and the adapters page says no adapter describes itself as conformant.
#: Forbidding the strings outright would delete those sentences, which are the honest half.
CLAIM_WORDS = ("conformant", "compliant", "certified", "aligned with")
@@ -748,7 +748,7 @@ def test_T139_the_claim_check_can_see_a_claim():
def test_T139_docs_adapters_exists_and_leads_with_the_three_ways_in():
- path = REPO_ROOT / "docs" / "adapters.md"
+ path = REPO_ROOT / "docs" / "docs" / "adapters.md"
if not path.exists(): # pragma: no cover - not a checkout
pytest.skip("no repository checkout")
text = path.read_text(encoding="utf-8")
@@ -761,7 +761,7 @@ def test_T139_docs_adapters_exists_and_leads_with_the_three_ways_in():
def test_the_claims_table_line_numbers_point_at_what_they_name():
- """`docs/CLAIMS.md` says *"Line numbers refer to that tag"*, and until this test they did not:
+ """The claims page says *"Line numbers refer to that tag"*, and until this test they did not:
seven of twenty-nine resolvable references had drifted by the time v0.5 was cut, pointing at
a string literal, a comment, or the middle of another function.
@@ -774,7 +774,7 @@ def test_the_claims_table_line_numbers_point_at_what_they_name():
which is stated rather than left to be assumed.
"""
- claims = REPO_ROOT / "docs" / "CLAIMS.md"
+ claims = REPO_ROOT / "docs" / "docs" / "CLAIMS.md"
if not claims.exists(): # pragma: no cover - not a checkout
pytest.skip("no repository checkout")
@@ -867,7 +867,7 @@ def test_every_documented_install_names_a_distribution_this_repository_builds():
with manifest.open("rb") as handle:
known.add(tomllib.load(handle)["project"]["name"])
- documents = [root / "README.md", root / "docs" / "adapters.md"]
+ documents = [root / "README.md", root / "docs" / "docs" / "adapters.md"]
documents += sorted((root / "adapters").glob("*/README.md")) if adapters.is_dir() else []
unknown = []
@@ -885,7 +885,7 @@ def test_every_documented_install_names_a_distribution_this_repository_builds():
def test_T138_item_sixs_questions_are_recorded_and_each_is_answered():
"""SPEC-v0.5 §8 T138: a third adapter was written against the contract alone, and **the list
- of questions it could not answer is the deliverable**, recorded in `docs/adapters.md` with
+ of questions it could not answer is the deliverable**, recorded in `docs/docs/adapters.md` with
each answered by an edit to `SPEC-v0.5.md`.
*"If the list cannot be emptied, v0.5 is not done."* So this asserts the list exists, that
@@ -893,10 +893,10 @@ def test_T138_item_sixs_questions_are_recorded_and_each_is_answered():
adapter itself is disposable and is deliberately not in the repository — asserting its
existence would be asserting the wrong half.
- This test was missing when v0.5 was otherwise complete, and `docs/adapters.md` carried a
+ This test was missing when v0.5 was otherwise complete, and `docs/docs/adapters.md` carried a
summary of the list rather than the list. A summary cannot be checked against the spec.
"""
- adapters = REPO_ROOT / "docs" / "adapters.md"
+ adapters = REPO_ROOT / "docs" / "docs" / "adapters.md"
spec = REPO_ROOT / "docs" / "SPEC-v0.5.md"
if not adapters.exists() or not spec.exists(): # pragma: no cover - not a checkout
pytest.skip("no repository checkout")
diff --git a/tests/test_readme_assets.py b/tests/test_readme_assets.py
index fcef2498..9eec7423 100644
--- a/tests/test_readme_assets.py
+++ b/tests/test_readme_assets.py
@@ -154,7 +154,7 @@ def test_the_header_carries_the_fixed_copy_and_the_five_badges():
"receipt — and when the outcome is unknown, CTRLRun says so instead of guessing." in head
)
# The category noun, which the hero went without until 0.6: a reader had to reverse-engineer
- # what CTRLRun *is* from three slogans. `docs/index.mdx` carried it and the README did not.
+ # what CTRLRun *is* from three slogans. `docs/docs.mdx` carried it and the README did not.
assert "A Python library that sits between the decision to act and the call that acts." in head
for badge in (
"pypi/v/ctrlrun",
diff --git a/tests/test_release_v0_6.py b/tests/test_release_v0_6.py
index 14ec65ef..10c4c40a 100644
--- a/tests/test_release_v0_6.py
+++ b/tests/test_release_v0_6.py
@@ -41,21 +41,21 @@
SCANNED = (
"README.md",
"CHANGELOG.md",
- "docs/postgres.md",
- "docs/THREAT_MODEL.md",
+ "docs/docs/postgres.md",
+ "docs/docs/THREAT_MODEL.md",
# Added with the Production section, by the independent review that noticed the gap. This
# page is now the site's principal statement of §1.2's third rule -- it says what the chain
# detects, what it does not survive, and that alteration is not authorship -- and it was in
- # neither this scan nor the section's own narrower one. If `docs/postgres.md` earned a place
+ # neither this scan nor the section's own narrower one. If the Postgres guide earned a place
# here for carrying one disclaiming sentence, a page carrying three has a stronger claim.
- "docs/production/receipt-integrity.mdx",
+ "docs/docs/production/receipt-integrity.mdx",
)
def _lines(name: str) -> list[str]:
"""One scanned document's lines.
- A **missing** file is a failure and not a skip: `docs/postgres.md` is required by §8 and by
+ A **missing** file is a failure and not a skip: `docs/docs/postgres.md` is required by §8 and by
this milestone's definition of done, and skipping on its absence would make the scan
disappear exactly when somebody deleted the document it covers. The only skip admitted is
the whole repository being absent, which is the sdist job running this suite from inside a
@@ -98,20 +98,20 @@ def _load(name: str) -> set[str]:
" **alteration**, which is not authorship: receipts are not signed.",
),
"CHANGELOG.md": (
- '- **`docs/ROADMAP.md`\'s v0.6 bullet said "receipt integrity (hash chain / signatures)", and the', # noqa: E501
+ '- **`docs/docs/ROADMAP.md`\'s v0.6 bullet said "receipt integrity (hash chain / signatures)", and the', # noqa: E501
"slash was the problem.** A chain detects **alteration**; a signature proves **origin**, and", # noqa: E501
"this project verifies what it is handed. Signing is out of scope for v0.6 (`SPEC-v0.6.md` §11).", # noqa: E501
- "- **`docs/THREAT_MODEL.md`'s \"Receipts are not signed; a database admin can alter history", # noqa: E501
+ "- **`docs/docs/THREAT_MODEL.md`'s \"Receipts are not signed; a database admin can alter history", # noqa: E501
"which half it does not: **truncation at the end**, authorship, an adversary who can rewrite", # noqa: E501
'- Receipts are not signed. A database administrator can alter history. **This line read "(v0.6)" until v0.6 was built, and that was a promise v0.6 does not keep**: v0.6 adds a hash chain, which detects alteration and is not evidence of authorship, and it does not stop an administrator who can rewrite every row including the chain head. Signing is out of scope (`SPEC-v0.6.md` §11).', # noqa: E501
),
- "docs/postgres.md": (
+ "docs/docs/postgres.md": (
"Receipts are not signed, alteration is not authorship, and the chain is not tamper-proof",
),
- "docs/production/receipt-integrity.mdx": (
+ "docs/docs/production/receipt-integrity.mdx": (
"- **It does not tell you who wrote a receipt.** Alteration is not authorship, it does not survive", # noqa: E501
),
- "docs/THREAT_MODEL.md": (
+ "docs/docs/THREAT_MODEL.md": (
"- Receipts are not signed, and they are not signed after v0.6 either. v0.6 adds a **hash chain** (`SPEC-v0.6.md` §6): each receipt carries the hash of the one before it, with `seq` inside the hashed content, so a partial tamper is detected and named — an `UPDATE` on one row, a `DELETE` from the middle, a reordering. What that closes is **alteration that keeps the receipts after it**: changing what receipt *n* says while leaving the rest in place costs a rewrite of all of them plus the head, rather than one statement. **Not a truncation at the end, and not an append.** Two earlier versions of this line claimed the first; a review measured both at **two statements, undetected** — delete the rows and rewind the head, or insert a well-formed row and advance it. The head is a row in the same database as the receipts, so it raises the cost of *forgetting* and not the cost of erasing; an anchor outside the database is what would close that, and v0.6 has none. What it does **not** close is authorship, and it does not close a database admin who can rewrite every row including the chain head: such an adversary recomputes the chain and it verifies. The malicious-administrator line above is unchanged; v0.6 narrows it rather than removing it. Nor does the chain prove that every action wrote a receipt — a receipt whose write failed leaves no gap in `seq` and is invisible to the chain by construction; the events log is where that is reconciled.", # noqa: E501
),
}
@@ -132,7 +132,7 @@ def _load(name: str) -> set[str]:
"- **`WebhookApprovalProvider`** — core, over stdlib `urllib.request`. One signed POST on",
"`APPROVAL_REQUESTED`; the gateway serves the signed inbound grant/deny at",
),
- "docs/THREAT_MODEL.md": (
+ "docs/docs/THREAT_MODEL.md": (
"| A forged or tampered token | `JWTIdentityProvider` verifies the signature against a JWKS or a pinned key, with the algorithm taken from its own allow-list and never from the token (RFC 8725 §3.1) |", # noqa: E501
"| Signing keys fetched from somewhere else | JWKS over HTTPS only, redirects refused outright, a duplicate `kid` refused rather than resolved, a failed fetch never emptying the cache |", # noqa: E501
"- **A compromised identity provider.** CTRLRun *consumes* identities: it verifies a token somebody else issued and maps the verified claims onto a `Principal`. It issues nothing, and an issuer that signs a token for the wrong subject has told CTRLRun the truth as far as CTRLRun can tell. Everything downstream — grants, delegation, receipts — is then wrong, correctly and consistently.", # noqa: E501
diff --git a/tests/test_repository_signals.py b/tests/test_repository_signals.py
index 156500f4..4b3de63c 100644
--- a/tests/test_repository_signals.py
+++ b/tests/test_repository_signals.py
@@ -174,7 +174,7 @@ def test_the_community_files_exist_and_say_what_they_must():
"scripts/check.sh",
"Specification first",
"mutation-tested",
- "docs/CLAIMS.md",
+ "docs/docs/CLAIMS.md",
"tools/docs_audit",
"trusted publishing",
):
@@ -231,11 +231,11 @@ def test_the_citation_names_the_repository_the_version_and_the_tagline():
def test_how_this_is_built_states_the_review_gap_and_the_tooling_once():
- page = (REPO_ROOT / "docs" / "how-this-is-built.md").read_text(encoding="utf-8")
+ page = (REPO_ROOT / "docs" / "docs" / "how-this-is-built.md").read_text(encoding="utf-8")
assert "no external security audit" in page
assert 1 <= page.count("AI coding agents") <= 3, "stated plainly, not hyped"
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
- assert "docs/how-this-is-built.md" in readme
+ assert "docs/docs/how-this-is-built.md" in readme
assert "Releases carry PyPI provenance attestations from GitHub Actions" in readme
assert "Releases carry PyPI provenance attestations from GitHub Actions" in (
REPO_ROOT / "SECURITY.md"
diff --git a/tests/test_verify_action.py b/tests/test_verify_action.py
index 128e175e..f25e8fa6 100644
--- a/tests/test_verify_action.py
+++ b/tests/test_verify_action.py
@@ -1,9 +1,9 @@
-"""The composite action, the badge and `docs/verify.md`. SPEC-v0.4 §5; T118-T120.
+"""The composite action, the badge and `docs/docs/verify.md`. SPEC-v0.4 §5; T118-T120.
The badge is the shortest sentence this project makes, and the one most likely to be read
without the report behind it. So its text is asserted as a *concatenation* and against a
regex rather than a word list — no adjective can be appended to it later — and the vocabulary
-it is not allowed to use is asserted against the badge, the job summary and `docs/verify.md`
+it is not allowed to use is asserted against the badge, the job summary and `docs/docs/verify.md`
together.
T118's substance runs in this repository's CI, where the action actually executes. What is
@@ -34,7 +34,7 @@
REPO_ROOT = Path(__file__).resolve().parents[1]
ACTION = REPO_ROOT / "action.yml"
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci.yml"
-VERIFY_DOC = REPO_ROOT / "docs" / "verify.md"
+VERIFY_DOC = REPO_ROOT / "docs" / "docs" / "verify.md"
README = REPO_ROOT / "README.md"
AUTHORITY_PAYMENTS = REPO_ROOT / "examples" / "authority" / "payments.yaml"
V1_PAYMENTS = REPO_ROOT / "examples" / "policies" / "payments.yaml"
@@ -231,9 +231,9 @@ def test_T119_the_link_target_carries_the_exact_phrase():
@pytest.mark.parametrize("word", FORBIDDEN)
def test_T119_no_claim_uses_the_forbidden_vocabulary(tmp_path, word):
- """Asserted against the badge, its JSON, the job summary and `docs/verify.md` together.
+ """Asserted against the badge, its JSON, the job summary and `docs/docs/verify.md` together.
- `docs/verify.md` names the words in order to refuse them, and the sentence that does is the
+ `docs/docs/verify.md` names the words in order to refuse them, and the sentence that does is the
only place any of them may appear on the page.
"""
report = run(_write(tmp_path, ALL_APPLICABLE))
@@ -368,13 +368,13 @@ def test_the_readme_carries_the_badge_and_links_it_to_what_it_means():
assert "img.shields.io/endpoint" in readme
assert "verify-badge.json" in readme
- assert "docs/verify.md#what-the-badge-means" in readme
+ assert "docs/docs/verify.md#what-the-badge-means" in readme
def test_the_readme_documentation_table_links_the_verify_page():
readme = _repository_file(README)
- assert "docs/verify.md" in readme
+ assert "docs/docs/verify.md" in readme
assert "declared guarantees pass" in readme
diff --git a/tools/docs_audit/claims.py b/tools/docs_audit/claims.py
index 9a15321d..29edc4c7 100644
--- a/tools/docs_audit/claims.py
+++ b/tools/docs_audit/claims.py
@@ -1,4 +1,4 @@
-"""Which symbol each `file.py:NNN` citation in `docs/CLAIMS.md` belongs to.
+"""Which symbol each `file.py:NNN` citation in `docs/docs/CLAIMS.md` belongs to.
One producer, imported by both `scripts/repoint-claims.py`, which **writes** the line numbers,
and `test_the_claims_table_line_numbers_point_at_what_they_name`, which **checks** them. They
diff --git a/tools/docs_audit/lint-allowlist.txt b/tools/docs_audit/lint-allowlist.txt
index 5ea03dae..03389c20 100644
--- a/tools/docs_audit/lint-allowlist.txt
+++ b/tools/docs_audit/lint-allowlist.txt
@@ -9,7 +9,7 @@
# claim. The regex is deliberately narrow: it names the negating phrase, not the word.
#
# Two documents are exempt by the rule itself and are not listed here:
-# docs/OWASP-AGENTIC-TOP10.md and docs/THREAT_MODEL.md, which exist to list what is not
+# docs/docs/OWASP-AGENTIC-TOP10.md and docs/docs/THREAT_MODEL.md, which exist to list what is not
# covered. The lint hard-codes them (EXEMPT_BY_RULE) so that this file cannot un-exempt them
# by accident or exempt a third by analogy.
@@ -31,7 +31,7 @@ exclude docs/SPEC-*.md
# partner asks"*, and the packs section says in its own words that **no pack describes itself as
# compliant with any regulation**. If a line there ever asserts support rather than naming an
# intention, the answer is to fix the line, not to widen this.
-exclude docs/ROADMAP.md
+exclude docs/docs/ROADMAP.md
# The style sheet and the information architecture name the forbidden words in order to
# forbid them, and name the sections (Compare) whose titles carry a competitor's phrase.
@@ -40,36 +40,36 @@ exclude docs/IA.md
# --- sentences that negate a claim ---------------------------------------------------------
-# README and docs/CLAIMS.md: the verify badge's meaning is stated by listing the words it
+# README and docs/docs/CLAIMS.md: the verify badge's meaning is stated by listing the words it
# does not mean.
allow * does not mean secure, safe, compliant, certified or audited
-# docs/verify.md says the same thing as a rule about which words never appear.
-allow docs/verify.md do not appear as
+# docs/docs/verify.md says the same thing as a rule about which words never appear.
+allow docs/docs/verify.md do not appear as
# "It is not a guardrail library, an IAM system, a workflow engine, or a compliance product."
allow * It is not a .* compliance product
-# Adapter READMEs and docs/adapters.md: the conformance kit is named and then denied as a claim.
+# Adapter READMEs and docs/docs/adapters.md: the conformance kit is named and then denied as a claim.
allow * not a compliance claim
allow * not\*\* a certification
allow * no adapter describes itself as "conformant"
-# docs/CLAIMS.md: the list of claims the README deliberately does not make.
-allow docs/CLAIMS.md Nothing about compliance, conformance or alignment
+# docs/docs/CLAIMS.md: the list of claims the README deliberately does not make.
+allow docs/docs/CLAIMS.md Nothing about compliance, conformance or alignment
-# docs/ACS.md reads somebody else's standard. "A conformant Guardian" is the standard's own
+# docs/docs/ACS.md reads somebody else's standard. "A conformant Guardian" is the standard's own
# term for one of its roles, and the second line says what a mapping would be if it claimed
# more than it can show.
-allow docs/ACS.md A conformant Guardian
-allow docs/ACS.md a compliance claim with nothing behind it
+allow docs/docs/ACS.md A conformant Guardian
+allow docs/docs/ACS.md a compliance claim with nothing behind it
# The Compare pages name the other thing they are compared with, in the heading of the section
# that says what it is good at. That is the page's whole job, and the fixed copy permits a
# category name where it is accurate; what it still forbids is CTRLRun describing *itself* that
# way, which the body of each page does the opposite of.
-allow docs/compare/guardrail-libraries.mdx guardrail
-allow docs/compare/governance-toolkits.mdx oversight toolkits
+allow docs/docs/compare/guardrail-libraries.mdx guardrail
+allow docs/docs/compare/governance-toolkits.mdx oversight toolkits
# The readiness block's "Not yet" list names sector packs in order to say there are none. It is
# generated by tools/docs_audit/render_readiness.py, so the sentence appears in six files: the
@@ -83,7 +83,7 @@ allow docs/compare/governance-toolkits.mdx oversight toolkits
# neighbours**, so under `*` a real sector-pack claim written directly above or below it would
# be permitted anywhere. An independent review found the wider glob.
allow README.md No sector packs\. \(the policy templates are starting points
-allow docs/index.mdx No sector packs\. \(the policy templates are starting points
-allow docs/production/index.mdx No sector packs\. \(the policy templates are starting points
+allow docs/docs.mdx No sector packs\. \(the policy templates are starting points
+allow docs/docs/production/index.mdx No sector packs\. \(the policy templates are starting points
allow docs/generated/readiness.* No sector packs\. \(the policy templates are starting points
-allow docs/CLAIMS.md no external security audit, no third-party review of the kernel, no sector packs
+allow docs/docs/CLAIMS.md no external security audit, no third-party review of the kernel, no sector packs
diff --git a/tools/docs_audit/lint.py b/tools/docs_audit/lint.py
index 6c7f375b..29a7fbe1 100644
--- a/tools/docs_audit/lint.py
+++ b/tools/docs_audit/lint.py
@@ -12,8 +12,8 @@
**Claim words** — *compliance*, *conformant*, *certified*, *aligned with*, *pack*, *sector*, the
named regulations, and social proof that does not exist — are checked **everywhere**, because a
body sentence is where a compliance claim or a sector product gets asserted. Two documents are
-exempt by name, because they exist to list what is *not* covered: `docs/OWASP-AGENTIC-TOP10.md`
-and `docs/THREAT_MODEL.md`. Everything else negates such a word through the allowlist beside
+exempt by name, because they exist to list what is *not* covered: `docs/docs/OWASP-AGENTIC-TOP10.md`
+and `docs/docs/THREAT_MODEL.md`. Everything else negates such a word through the allowlist beside
this file, one regex per legitimate sentence, with the reason.
`lint-allowlist.txt` also names the files the lint does not read, with a reason on each line.
@@ -35,7 +35,7 @@
#: The two documents the rules exempt by name. Not in the allowlist file, because an entry
#: there could be deleted by a session that found it inconvenient; these are the rule.
-EXEMPT_BY_RULE: tuple[str, ...] = ("docs/OWASP-AGENTIC-TOP10.md", "docs/THREAT_MODEL.md")
+EXEMPT_BY_RULE: tuple[str, ...] = ("docs/docs/OWASP-AGENTIC-TOP10.md", "docs/docs/THREAT_MODEL.md")
@dataclass(frozen=True)
diff --git a/tools/docs_audit/render_api.py b/tools/docs_audit/render_api.py
index 0f1d834c..3b1894fd 100644
--- a/tools/docs_audit/render_api.py
+++ b/tools/docs_audit/render_api.py
@@ -23,7 +23,7 @@
from _files import REPO_ROOT, relative
-API_DIR = REPO_ROOT / "docs" / "reference" / "api"
+API_DIR = REPO_ROOT / "docs" / "docs" / "reference" / "api"
DOCS_JSON = REPO_ROOT / "docs" / "docs.json"
GROUP = "Python API"
MARKER = "generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page"
@@ -280,8 +280,8 @@ def _page(member, module_path: str, name: str) -> str: # noqa: ANN001
*_sections(member),
"## Next",
"",
- "- [Python API index](/reference/api/index).",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [Python API index](/docs/reference/api/index).",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
return "\n".join(lines)
@@ -317,13 +317,13 @@ def render() -> dict[str, str]:
"|---|---|---|",
]
for slug, qualified, kind, summary in index_rows:
- lines.append(f"| [`{qualified}`](/reference/api/{slug}) | {kind} | {summary} |")
+ lines.append(f"| [`{qualified}`](/docs/reference/api/{slug}) | {kind} | {summary} |")
lines += [
"",
"## Next",
"",
- "- [CLI reference](/reference/cli).",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [CLI reference](/docs/reference/cli).",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
pages["index.mdx"] = "\n".join(lines)
@@ -332,7 +332,7 @@ def render() -> dict[str, str]:
def navigation_pages(pages: Iterable[str]) -> list[str]:
slugs = sorted(p.removesuffix(".mdx") for p in pages if p != "index.mdx")
- return ["reference/api/index", *(f"reference/api/{slug}" for slug in slugs)]
+ return ["docs/reference/api/index", *(f"docs/reference/api/{slug}" for slug in slugs)]
def _docs_json_group(document: dict) -> dict | None:
diff --git a/tools/docs_audit/render_badges.py b/tools/docs_audit/render_badges.py
index 912543f5..72f186eb 100644
--- a/tools/docs_audit/render_badges.py
+++ b/tools/docs_audit/render_badges.py
@@ -93,12 +93,12 @@ class Badge:
Badge(
"Tests",
f"https://img.shields.io/endpoint?url={BADGES_BRANCH}/tests-badge.json",
- "https://ctrlrun.dev/how-this-is-built",
+ "https://ctrlrun.dev/docs/how-this-is-built",
),
Badge(
"CTRLRun verified",
f"https://img.shields.io/endpoint?url={BADGES_BRANCH}/verify-badge.json",
- "https://ctrlrun.dev/security/verify-guarantees",
+ "https://ctrlrun.dev/docs/security/verify-guarantees",
),
Badge(
"OpenSSF Scorecard",
diff --git a/tools/docs_audit/render_cli.py b/tools/docs_audit/render_cli.py
index 7fcc53b5..e2f532e5 100644
--- a/tools/docs_audit/render_cli.py
+++ b/tools/docs_audit/render_cli.py
@@ -1,4 +1,4 @@
-"""Render `docs/reference/cli.mdx` from click's own help text, and refuse a copy that drifted.
+"""Render `docs/docs/reference/cli.mdx` from click's own help text, and refuse a copy that drifted.
The CLI reference is `ctrlrun --help` and every subcommand's `--help`, verbatim, so the page
cannot say something the command does not. Edit a command's docstring or option help in
@@ -19,7 +19,7 @@
from _files import REPO_ROOT, relative
-TARGET = REPO_ROOT / "docs" / "reference" / "cli.mdx"
+TARGET = REPO_ROOT / "docs" / "docs" / "reference" / "cli.mdx"
COLUMNS = "88"
MARKER = "generated by tools/docs_audit/render_cli.py from click's help text — edit the command docstrings, never this page"
@@ -76,9 +76,9 @@ def render() -> str:
lines += [
"## Next",
"",
- "- [Exit codes](/reference/exit-codes): what each command's exit status means.",
- "- [Resolve an AMBIGUOUS effect](/guides/resolve-an-ambiguous-effect): `effects`, `resolve` and `inspect` in use.",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [Exit codes](/docs/reference/exit-codes): what each command's exit status means.",
+ "- [Resolve an AMBIGUOUS effect](/docs/guides/resolve-an-ambiguous-effect): `effects`, `resolve` and `inspect` in use.",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
return "\n".join(lines)
diff --git a/tools/docs_audit/render_cookbook.py b/tools/docs_audit/render_cookbook.py
index 98535eb5..46f4e442 100644
--- a/tools/docs_audit/render_cookbook.py
+++ b/tools/docs_audit/render_cookbook.py
@@ -1,6 +1,6 @@
"""Extract every cookbook recipe's files from its page into `examples/cookbook//`.
-A recipe page under `docs/cookbook/` is the single source: its `yaml runnable` block is the
+A recipe page under `docs/docs/cookbook/` is the single source: its `yaml runnable` block is the
recipe's `ctrlrun.yaml` and its `python runnable file=main.py` block is the script. This
script writes those into `examples/cookbook//`, so the directory a reader clones runs
exactly what the page shows, and `--check` refuses a copy that drifted either way.
@@ -18,16 +18,16 @@
from _files import REPO_ROOT, fences, relative
-PAGES = REPO_ROOT / "docs" / "cookbook"
+PAGES = REPO_ROOT / "docs" / "docs" / "cookbook"
EXAMPLES = REPO_ROOT / "examples" / "cookbook"
HEADER = (
"# Extracted by tools/docs_audit/render_cookbook.py from\n"
- "# docs/cookbook/{name}.mdx — edit the page, never this file.\n"
+ "# docs/docs/cookbook/{name}.mdx — edit the page, never this file.\n"
)
def recipes() -> dict[str, dict[str, str]]:
- """`{recipe name: {file name: content}}` for every page under `docs/cookbook/`."""
+ """`{recipe name: {file name: content}}` for every page under `docs/docs/cookbook/`."""
out: dict[str, dict[str, str]] = {}
for page in sorted(PAGES.glob("*.mdx")):
if page.stem == "index":
@@ -60,7 +60,7 @@ def check(extracted: dict[str, dict[str, str]]) -> list[str]:
drift.append(f"{relative(target)} missing; run --write")
elif target.read_text(encoding="utf-8") != content:
drift.append(
- f"{relative(target)} differs from docs/cookbook/{name}.mdx; run --write"
+ f"{relative(target)} differs from docs/docs/cookbook/{name}.mdx; run --write"
)
if EXAMPLES.exists():
for existing in EXAMPLES.iterdir():
@@ -69,7 +69,9 @@ def check(extracted: dict[str, dict[str, str]]) -> list[str]:
and existing.name not in extracted
and existing.name != "__pycache__"
):
- drift.append(f"{relative(existing)} has no page under docs/cookbook/; remove it")
+ drift.append(
+ f"{relative(existing)} has no page under docs/docs/cookbook/; remove it"
+ )
return drift
diff --git a/tools/docs_audit/render_probe.py b/tools/docs_audit/render_probe.py
index 826daa7f..a18edabc 100644
--- a/tools/docs_audit/render_probe.py
+++ b/tools/docs_audit/render_probe.py
@@ -1,6 +1,6 @@
"""Render the framework-probe study page from the published results, and refuse a hand edit.
-`docs/study/does-your-framework-double-execute.mdx` is a **render**, never written by hand:
+`docs/docs/study/does-your-framework-double-execute.mdx` is a **render**, never written by hand:
its table comes from `research/framework-probe/results/*.json`, which the maintainer publishes
by committing a run. Until one exists the page says so, in those words, and lists no framework.
@@ -26,7 +26,7 @@
from _files import REPO_ROOT, relative
RESULTS = REPO_ROOT / "research" / "framework-probe" / "results"
-TARGET = REPO_ROOT / "docs" / "study" / "does-your-framework-double-execute.mdx"
+TARGET = REPO_ROOT / "docs" / "docs" / "study" / "does-your-framework-double-execute.mdx"
MARKER = (
"generated by tools/docs_audit/render_probe.py from "
"research/framework-probe/results/ — publish a run, do not edit this page"
@@ -199,9 +199,9 @@ def render() -> str:
"",
"## Next",
"",
- "- [Outcomes and AMBIGUOUS](/concepts/outcomes-and-ambiguous): what a lost reply is, and why a retry is refused.",
- "- [Effect keys](/concepts/effect-keys): the guard the frameworks above are running without.",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [Outcomes and AMBIGUOUS](/docs/concepts/outcomes-and-ambiguous): what a lost reply is, and why a retry is refused.",
+ "- [Effect keys](/docs/concepts/effect-keys): the guard the frameworks above are running without.",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
return "\n".join(lines)
diff --git a/tools/docs_audit/render_readiness.py b/tools/docs_audit/render_readiness.py
index 30f219ac..9fa7e436 100644
--- a/tools/docs_audit/render_readiness.py
+++ b/tools/docs_audit/render_readiness.py
@@ -11,7 +11,7 @@
And it carries a **Not yet** list, inside the block and never below it: no external security
audit, no third-party review of the kernel, no sector packs. The honest half is why the first
half is believed. A line leaves only when the thing it names has become true, in its own pull
-request, and `docs/CLAIMS.md` changes in the same one.
+request, and `docs/docs/CLAIMS.md` changes in the same one.
python tools/docs_audit/render_readiness.py --write # refresh, re-counting the suite
python tools/docs_audit/render_readiness.py --check # what CI runs
@@ -143,9 +143,9 @@ def where(text: str, page: str) -> str:
route to the page that qualifies it travels with it.
"""
if full:
- return f"{text} [Read more](/{page})."
+ return f"{text} [Read more](/docs/{page})."
if page == "production/soak":
- return f"{text} [What it does not establish](https://ctrlrun.dev/{page})."
+ return f"{text} [What it does not establish](https://ctrlrun.dev/docs/{page})."
return text
lines = [
diff --git a/tools/docs_audit/render_schemas.py b/tools/docs_audit/render_schemas.py
index a4b9a765..059a8ccf 100644
--- a/tools/docs_audit/render_schemas.py
+++ b/tools/docs_audit/render_schemas.py
@@ -2,9 +2,9 @@
Three pages, each a marker-headed render that `--check` refuses when it has drifted:
-- `docs/reference/receipt-and-event-schemas.mdx` from the `Receipt` and `Event` dataclasses and
+- `docs/docs/reference/receipt-and-event-schemas.mdx` from the `Receipt` and `Event` dataclasses and
the `EventType` enum;
-- `docs/reference/errors.mdx` from `ctrlrun.errors`, every class with its base and the first
+- `docs/docs/reference/errors.mdx` from `ctrlrun.errors`, every class with its base and the first
line of its docstring.
python tools/docs_audit/render_schemas.py --write
@@ -22,8 +22,8 @@
from _files import REPO_ROOT, relative
-SCHEMAS = REPO_ROOT / "docs" / "reference" / "receipt-and-event-schemas.mdx"
-ERRORS = REPO_ROOT / "docs" / "reference" / "errors.mdx"
+SCHEMAS = REPO_ROOT / "docs" / "docs" / "reference" / "receipt-and-event-schemas.mdx"
+ERRORS = REPO_ROOT / "docs" / "docs" / "reference" / "errors.mdx"
MARKER = "generated by tools/docs_audit/render_schemas.py from the code — edit the dataclasses and docstrings, never this page"
@@ -118,9 +118,9 @@ def render_schemas() -> str:
"",
"## Next",
"",
- "- [Receipts and evidence](/concepts/receipts-and-evidence): what a receipt is for.",
- "- [The receipt chain](/security/receipt-chain): what `seq`, `prev_hash` and `hash` detect.",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [Receipts and evidence](/docs/concepts/receipts-and-evidence): what a receipt is for.",
+ "- [The receipt chain](/docs/security/receipt-chain): what `seq`, `prev_hash` and `hash` detect.",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
return "\n".join(lines)
@@ -176,9 +176,9 @@ def render_errors() -> str:
lines += [
"## Next",
"",
- "- [Fail closed](/concepts/fail-closed): the table of what is refused, and where.",
- "- [Outcomes and AMBIGUOUS](/concepts/outcomes-and-ambiguous): `NotExecuted` and why it is the dangerous one.",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [Fail closed](/docs/concepts/fail-closed): the table of what is refused, and where.",
+ "- [Outcomes and AMBIGUOUS](/docs/concepts/outcomes-and-ambiguous): `NotExecuted` and why it is the dangerous one.",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
return "\n".join(lines)
diff --git a/tools/docs_audit/render_soak.py b/tools/docs_audit/render_soak.py
index 7618dc88..6ee3ec3e 100644
--- a/tools/docs_audit/render_soak.py
+++ b/tools/docs_audit/render_soak.py
@@ -1,6 +1,6 @@
"""Render the soak page from the published results, and refuse a hand edit.
-`docs/production/soak.mdx` is a **render**, never written by hand. Every number on it comes
+`docs/docs/production/soak.mdx` is a **render**, never written by hand. Every number on it comes
from `research/soak/results/*.json`, which the maintainer publishes by committing a run, and
until one exists the page says so in those words and shows no table.
@@ -34,7 +34,7 @@
from _files import REPO_ROOT, relative
RESULTS = REPO_ROOT / "research" / "soak" / "results"
-TARGET = REPO_ROOT / "docs" / "production" / "soak.mdx"
+TARGET = REPO_ROOT / "docs" / "docs" / "production" / "soak.mdx"
MARKER = (
"generated by tools/docs_audit/render_soak.py from research/soak/results/ — "
"publish a run, do not edit this page"
@@ -175,13 +175,13 @@ def render() -> str:
"- **It is not a throughput figure you can plan against.** A policy denial writes a",
" receipt like any other outcome, so nearly every attempt above went through the",
" one-row chain head — but on one host, with four threads, against a database on",
- " the same machine. [Postgres](/postgres) describes that ceiling; this run does not",
+ " the same machine. [Postgres](/docs/postgres) describes that ceiling; this run does not",
" size it for your hardware.",
"- **It does not exercise a partition or a second host.** That is the cross-host",
- " suite, and [how reservation works](/production/how-reservation-works) says which",
+ " suite, and [how reservation works](/docs/production/how-reservation-works) says which",
" of those were actually run.",
"- **It says nothing about the receipt chain's integrity.**",
- " [Receipt integrity](/production/receipt-integrity) is that.",
+ " [Receipt integrity](/docs/production/receipt-integrity) is that.",
"",
"## Run it yourself",
"",
@@ -209,9 +209,9 @@ def render() -> str:
lines += [
"## Next",
"",
- "- [Run it in production](/production/index): the section this belongs to.",
- "- [Recovery after a crash](/production/recovery): what survives a restart, and what does not.",
- "- [Get started](/get-started/quickstart) · [Why](/why).",
+ "- [Run it in production](/docs/production/index): the section this belongs to.",
+ "- [Recovery after a crash](/docs/production/recovery): what survives a restart, and what does not.",
+ "- [Get started](/docs/get-started/quickstart) · [Why](/docs/why).",
"",
]
return "\n".join(lines)