From 92b97ae4c78c3a16019cdf32435ccb85658a9e06 Mon Sep 17 00:00:00 2001 From: Writer Date: Wed, 14 Oct 2026 23:14:20 +0200 Subject: [PATCH] The controls for the checks that already run companygraph/mental-model took a control for each mechanism that already refused something in our repositories and named nowhere, with the three rules they needed (companygraph/mental-model#139). This instance takes the same ten entries, adapted where its mechanism differs: its own MCP host in the registry and served-commit controls, no objective for the cited rule to serve, and, where the repositories sit under a personal account or the organization grants no write by default, the text that says who can push and how push protection reaches a new repository. Verified: companygraph check at 0.94.0 passes, mechanical checks and the form, and each page holds to its schema's writing rules on a reading of them; the host's publish and deploy workflows and its registry environment were read from GitHub. Process: Delivery Phase: Implement Track: Prose Co-Authored-By: Claude Opus 5.5 --- ...-server-is-asked-which-commit-it-serves.md | 29 +++++++++++++++++++ ...y-waits-for-a-release-tag-and-the-owner.md | 29 +++++++++++++++++++ ...elease-tag-stays-where-the-owner-put-it.md | 28 ++++++++++++++++++ .../a-secret-is-refused-at-the-push.md | 29 +++++++++++++++++++ ...s-removing-a-worktree-that-holds-a-plan.md | 29 +++++++++++++++++++ ...ision-and-a-label-are-held-as-they-were.md | 29 +++++++++++++++++++ .../only-the-owner-s-account-changes-main.md | 28 ++++++++++++++++++ ...cret-is-never-written-into-a-repository.md | 19 ++++++++++++ ...e-is-removed-only-once-its-plan-is-done.md | 13 +++++++++ ...from-outside-the-model-is-not-rewritten.md | 13 +++++++++ 10 files changed, 246 insertions(+) create mode 100644 model/controls/a-deployed-server-is-asked-which-commit-it-serves.md create mode 100644 model/controls/a-registry-entry-waits-for-a-release-tag-and-the-owner.md create mode 100644 model/controls/a-release-tag-stays-where-the-owner-put-it.md create mode 100644 model/controls/a-secret-is-refused-at-the-push.md create mode 100644 model/controls/an-agent-s-hook-refuses-removing-a-worktree-that-holds-a-plan.md create mode 100644 model/controls/an-id-a-decision-and-a-label-are-held-as-they-were.md create mode 100644 model/controls/only-the-owner-s-account-changes-main.md create mode 100644 model/rules/a-secret-is-never-written-into-a-repository.md create mode 100644 model/rules/a-worktree-is-removed-only-once-its-plan-is-done.md create mode 100644 model/rules/what-is-cited-from-outside-the-model-is-not-rewritten.md diff --git a/model/controls/a-deployed-server-is-asked-which-commit-it-serves.md b/model/controls/a-deployed-server-is-asked-which-commit-it-serves.md new file mode 100644 index 0000000..f7ae064 --- /dev/null +++ b/model/controls/a-deployed-server-is-asked-which-commit-it-serves.md @@ -0,0 +1,29 @@ +--- +id: 01a13c3b-e14f-7bb9-851d-f1819fa21865 +source: Local +kind: detective +mode: automated +enforces: + - A state is read again before it is relied on +--- + +# A deployed server is asked which commit it serves + +> After every deploy of mcp.blust.ch the run asks the live server which commit of the model it answers at, and fails unless it is the commit the repository pins. + +## How it is carried out + +The deploy job that mcp.blust.ch's workflow calls from the MCP server, on every push to main, once Terraform has applied: it calls the live server's own `list_types` tool, reads the commit every answer names under `model`, and fails unless it is the commit `source.json` pins. A deploy that applied cleanly but left an earlier revision serving, or one built from another commit, then turns the run red instead of reading as done. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| The deploy job and its check | https://github.com/companygraph/mcp-server/blob/main/.github/workflows/deploy-google.yml | +| The workflow that calls it | https://github.com/robertblust/mcp-blust-ch/blob/main/.github/workflows/deploy.yml | diff --git a/model/controls/a-registry-entry-waits-for-a-release-tag-and-the-owner.md b/model/controls/a-registry-entry-waits-for-a-release-tag-and-the-owner.md new file mode 100644 index 0000000..004a51b --- /dev/null +++ b/model/controls/a-registry-entry-waits-for-a-release-tag-and-the-owner.md @@ -0,0 +1,29 @@ +--- +id: 01a13c3b-e14f-7482-8a59-87fcdfb5f595 +source: Local +kind: preventive +mode: automated +enforces: + - The Owner's word merges +--- + +# A registry entry waits for a release tag and the Owner + +> A server's entry in the MCP registry is published only from a release tag and only once the Owner has approved that run, because the key that signs it is held where no other run can reach it. + +## How it is carried out + +GitHub's `registry` environment on mcp.blust.ch's repository, on every run of the publish workflow: it lets a job in only from a tag matching `v*`, and holds that job until the Owner approves it. The key that signs the entry is a secret of the environment and of no other, so a run from a branch, or one nobody approved, never holds it and cannot publish. A repository admin may bypass the review, which is the Owner's account, so an agent working under it is held by An agent's permissions refuse a merge it was not given. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| GitHub's deployment environments | https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments | +| The publish workflow and its registry job | https://github.com/robertblust/mcp-blust-ch/blob/main/.github/workflows/publish.yml | diff --git a/model/controls/a-release-tag-stays-where-the-owner-put-it.md b/model/controls/a-release-tag-stays-where-the-owner-put-it.md new file mode 100644 index 0000000..41eca8b --- /dev/null +++ b/model/controls/a-release-tag-stays-where-the-owner-put-it.md @@ -0,0 +1,28 @@ +--- +id: 01a13c3b-e14f-752b-b9db-56b6563a532c +source: Local +kind: preventive +mode: automated +enforces: + - The Owner's word merges +--- + +# A release tag stays where the Owner put it + +> Each of my repositories refuses a tag beginning with `v` that any account but the Owner's creates, moves or deletes, so a release exists only where the Owner tagged it and stays on the commit it was made on. + +## How it is carried out + +GitHub's ruleset `protect-release-tags` on each repository, on every push of a tag matching `v*`: it refuses its creation, its update and its deletion, and lets only the repository's admin role past, which is the Owner's account. A release is its tag and a consumer takes it by naming the tag, so a tag moved after it was taken would change what every consumer already holds; a release tagged wrongly is followed by a patch release and never moved. An agent working under the Owner's account passes it, so what holds an agent's tag to the Owner's word is An agent's permissions refuse a merge it was not given. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| GitHub's rulesets for branches and tags | https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets#branch-and-tag-rulesets | diff --git a/model/controls/a-secret-is-refused-at-the-push.md b/model/controls/a-secret-is-refused-at-the-push.md new file mode 100644 index 0000000..2731834 --- /dev/null +++ b/model/controls/a-secret-is-refused-at-the-push.md @@ -0,0 +1,29 @@ +--- +id: 01a13c3b-e14f-74a9-add2-d57f91715839 +source: Local +kind: preventive +mode: automated +enforces: + - A secret is never written into a repository +--- + +# A secret is refused at the push + +> GitHub refuses a push to any of my repositories that carries a secret it recognizes, before the secret is published, and scans what is already there for any that got in. + +## How it is carried out + +GitHub's secret scanning with push protection, turned on in each of my repositories one by one, since a personal account has no default that turns it on for a new one: on every push it looks for the shapes of keys and tokens its providers register, refuses the push that carries one and names the file and line, and it keeps scanning the history for any that reached a repository before. Whoever pushes may push past a refusal by giving a reason, that the secret is used in tests, is a false positive or will be fixed later, and GitHub then records an alert, writes the bypass to the audit log and emails the owners. It knows only those shapes, so a password someone made up, or a key in a form no provider registered, passes it; for those the rule rests on whoever writes the change. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Implement | Delivery | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| GitHub's push protection | https://docs.github.com/en/code-security/concepts/secret-security/push-protection | diff --git a/model/controls/an-agent-s-hook-refuses-removing-a-worktree-that-holds-a-plan.md b/model/controls/an-agent-s-hook-refuses-removing-a-worktree-that-holds-a-plan.md new file mode 100644 index 0000000..25744f9 --- /dev/null +++ b/model/controls/an-agent-s-hook-refuses-removing-a-worktree-that-holds-a-plan.md @@ -0,0 +1,29 @@ +--- +id: 01a13c3b-e14f-7b14-9bd5-db46ef6e4c7d +source: Local +kind: preventive +mode: automated +enforces: + - A worktree is removed only once its plan is done +--- + +# An agent's hook refuses removing a worktree that holds a plan + +> Before an agent on my machine runs a shell command, a hook refuses it where it would remove a worktree that still holds a plan's ledger. + +## How it is carried out + +A hook Claude Code runs on my machine before every shell command an agent issues: it reads each `git worktree remove` in the command, resolves the worktree's path through any `cd` or `-C` before it, and refuses the command where that worktree holds a plan's ledger under `.superpowers/sdd/`, saying why, so the agent stops before anything is deleted. It holds an agent and not my own terminal, and a worktree deleted some other way, such as by `rm -rf`, passes it; the hook lives in my own settings and in no repository. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Implement | Delivery | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| Claude Code hooks | https://code.claude.com/docs/en/hooks | diff --git a/model/controls/an-id-a-decision-and-a-label-are-held-as-they-were.md b/model/controls/an-id-a-decision-and-a-label-are-held-as-they-were.md new file mode 100644 index 0000000..a8c4215 --- /dev/null +++ b/model/controls/an-id-a-decision-and-a-label-are-held-as-they-were.md @@ -0,0 +1,29 @@ +--- +id: 01a13c3b-e14f-78de-8e41-201b8987c4d4 +source: Local +kind: preventive +mode: automated +enforces: + - What is cited from outside the model is not rewritten +--- + +# An id, a decision and a label are held as they were + +> A pull request to the model fails where it changes an id already on the default branch, rewrites a decision beyond closing it, or moves, swaps or reuses a label. + +## How it is carried out + +The instance check, required on the default branch, on every pull request: its step `companygraph ids --range` compares each page of the branch with its base after both are put in the one form, and refuses a changed id, a decision whose text changed beyond its status and one dated closing sentence, and a label of a scenario, a step or an invariant that moved to another item, swapped with one or came back after it was removed. A reference name may follow the rename of what it names, and a URL under `## References` may change on its own. Where a release of core changed the schema that governs a type, the decision and label checks stand aside for that type and say so in a note. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| The instance check | https://github.com/companygraph/meta-model/blob/main/.github/workflows/instance-check.yml | +| The change checks | https://github.com/companygraph/meta-model/blob/main/lib/checks.mjs | diff --git a/model/controls/only-the-owner-s-account-changes-main.md b/model/controls/only-the-owner-s-account-changes-main.md new file mode 100644 index 0000000..25bb150 --- /dev/null +++ b/model/controls/only-the-owner-s-account-changes-main.md @@ -0,0 +1,28 @@ +--- +id: 01a13c3b-e14f-7b2a-ac60-5ef22adf037f +source: Local +kind: preventive +mode: automated +enforces: + - The Owner's word merges +--- + +# Only the Owner's account changes main + +> The default branch of each of my repositories takes a push or a merge from my account alone, because no one else holds write access to them. + +## How it is carried out + +GitHub's permissions on a repository under a personal account, on every push and every merge into it: such a repository takes a push only from its owner and the collaborators the owner invites, and mine invite none, so anyone else can fork a repository and open a pull request but cannot merge it. A personal account cannot restrict who may push beyond that, as an organization's branch protection can, so the day a collaborator is invited this control no longer holds. An agent working under my account passes it, so what holds an agent to the Owner's word is An agent's permissions refuse a merge it was not given. + +## Applies to + +| Type | Entity | Owner | +| --- | --- | --- | +| phase | Integrate | Delivery | + +## References + +| What | URL | +| --- | --- | +| GitHub's permission levels for a personal account's repository | https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/permission-levels-for-a-personal-account-repository | diff --git a/model/rules/a-secret-is-never-written-into-a-repository.md b/model/rules/a-secret-is-never-written-into-a-repository.md new file mode 100644 index 0000000..d495e2e --- /dev/null +++ b/model/rules/a-secret-is-never-written-into-a-repository.md @@ -0,0 +1,19 @@ +--- +id: 01a13c3b-e14f-7f29-9337-6620a10a3531 +source: Local +modality: must not +--- + +# A secret is never written into a repository + +> No key, token or password is committed to any of my repositories, in a file, a commit message or a test fixture; a secret lives in the store of the workflow or the service that reads it. + +## Why + +Every repository I work in is public, so whatever is pushed is published the moment it reaches GitHub, and it stays in the history after a later commit removes it. A secret that reached a repository can only be rotated, never taken back, and everything it opened stays open until it is. For the same reason a secret is never printed, not even to check it: a value that reaches a transcript has to be rotated as well. + +## References + +| What | URL | +| --- | --- | +| Working rules | https://github.com/robertblust/conventions/blob/main/conventions/WORKING.md | diff --git a/model/rules/a-worktree-is-removed-only-once-its-plan-is-done.md b/model/rules/a-worktree-is-removed-only-once-its-plan-is-done.md new file mode 100644 index 0000000..e8d0a09 --- /dev/null +++ b/model/rules/a-worktree-is-removed-only-once-its-plan-is-done.md @@ -0,0 +1,13 @@ +--- +id: 01a13c3b-e14f-722f-a2e8-240dc563ab97 +source: Local +modality: must not +--- + +# A worktree is removed only once its plan is done + +> A worktree that holds a plan's ledger is not removed before every task of that plan is done, even when its pull request has merged. + +## Why + +A plan's ledger, its briefs and its task reports live in the worktree under `.superpowers/`, which git ignores, and `git worktree remove` deletes ignored files without asking, with or without `--force`, so nothing brings them back. A merged pull request does not finish a plan: a plan can carry tasks in other repositories that are still owed. Three ledgers were lost this way in September 2026, twice in one day. diff --git a/model/rules/what-is-cited-from-outside-the-model-is-not-rewritten.md b/model/rules/what-is-cited-from-outside-the-model-is-not-rewritten.md new file mode 100644 index 0000000..1513fab --- /dev/null +++ b/model/rules/what-is-cited-from-outside-the-model-is-not-rewritten.md @@ -0,0 +1,13 @@ +--- +id: 01a13c3b-e14f-7492-9f36-ffa64f8bf645 +source: Local +modality: must not +--- + +# What is cited from outside the model is not rewritten + +> Once on the default branch, an entity's id, a decision's text and a label on a page are not changed, because a link, a commit or a test outside the model may already cite them. + +## Why + +An outside system holds on to what it cites: a graph node to an entity's id, a commit or a later decision to a decision as it was decided, a test or a line of code to the label of a scenario, a step or an invariant. Each is the only handle that citation has, and a rewrite leaves it pointing at something that now says another thing, with nothing to tell whoever cited it. So an id stays with its entity, a decision that no longer stands is closed by its status and one dated sentence instead of being edited, and a label stays with the item it was given to and is never given to another.