Refuse a push to anything but the forge

5116bfe5d8ef · AtlantisPleb · · parent fa4b79283164

Refuse a push to anything but the forge

The forge is the authority for this repository: it records every push in the
durable WAL and mirrors main to GitHub itself, so GitHub is a projection of
the forge the way a slug is a projection of a GitHub ID. Pushing to GitHub
directly inverts that — the WAL never sees the objects, the mirror watch
compares the forge against a mirror that is ahead of it, and nothing reports
the divergence until a clone disagrees with the site.

`ops/ci/push-remote-check.sh` admits forge hosts and refuses every other
remote, whatever URL form it takes. The pre-push hook runs it before the
release gate, because where a push is going costs nothing to check and the
gate costs minutes. The override stays bounded and logged, for mirroring by
hand while the forge is unreachable.

Enforcement still requires `core.hooksPath`, which also turns on the release
gate for every push. REPOSITORY-002 says so rather than implying the guard is
already running on every machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016o8HwTaqLKEWCHTjsjFtrB
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified .githooks/pre-push
  • modified AGENTS.md
  • modified INVARIANTS.md
  • modified docs/operations/release-deployment-fallbacks.md
  • added ops/ci/push-remote-check.sh
  • added test/openagents/push_remote_contract_test.exs

Diff

6 files changed, +160 -0

.githooks/pre-push modified +4

@@ -2,4 +2,8 @@

2 2
set -eu
3 3
4 4
repo_root=$(git rev-parse --show-toplevel)
5
6
# Where the push is going, before what it costs to check the push itself.
7
"$repo_root/ops/ci/push-remote-check.sh" "$@"
8
5 9
exec "$repo_root/ops/ci/gate.sh"
AGENTS.md modified +15

@@ -12,6 +12,21 @@ All text in this repo — docs, README, `AGENTS.md`, commit messages, and agent

12 12
- Avoid jargon, buzzwords, metaphors, exclamation marks, and phrases like `simply` or `just`.
13 13
- Avoid `please` in instructions.
14 14
15
## Git remotes
16
17
Push to the forge, never to GitHub:
18
19
```sh
20
git push openagents HEAD:main
21
```
22
23
The `openagents` remote is the forge at `openagents.com`, which records every
24
push in the durable WAL and mirrors `main` to GitHub itself. The `origin`
25
remote is that GitHub mirror; pushing to it directly leaves the forge behind
26
its own mirror, and nothing reports the divergence until a clone disagrees
27
with the site. `ops/ci/push-remote-check.sh` refuses a non-forge push, and
28
`.githooks/pre-push` runs it. See `INVARIANTS.md`, REPOSITORY-002.
29
15 30
## Project guidelines
16 31
17 32
- Use `mix precommit` alias when you are done with all changes and fix any pending issues
INVARIANTS.md modified +26

@@ -1791,6 +1791,31 @@ Evidence: `OpenAgents.Repositories`, `OpenAgents.Repositories.Provisioner`,

1791 1791
`test/openagents_web/controllers/repository_controller_test.exs`, and
1792 1792
`test/openagents/forge/git_http_test.exs`.
1793 1793
1794
### REPOSITORY-002 — Development pushes go to the forge, never to the mirror
1795
1796
Status: Current
1797
1798
This repository's own commits reach the forge first. The forge records each
1799
push in the durable WAL and mirrors `main` to GitHub itself, so GitHub is a
1800
projection of the forge in the same sense that a slug is a projection of a
1801
GitHub ID. A push sent straight to GitHub inverts that: the WAL never sees the
1802
objects, the mirror watch compares the forge against a mirror that is ahead of
1803
it, and nothing reports the divergence until a clone disagrees with the site.
1804
1805
`ops/ci/push-remote-check.sh` admits only forge hosts and refuses every other
1806
remote, whatever URL form it takes. `.githooks/pre-push` runs it before the
1807
release gate, since where a push is going costs nothing to check and the gate
1808
costs minutes. A bounded, logged override exists for operator-directed
1809
recovery, such as mirroring by hand while the forge is unreachable.
1810
1811
The check is a guard, not a deployment: it refuses a wrong destination and
1812
makes no claim about the candidate. Enforcement requires `core.hooksPath`,
1813
which also enables the release gate on every push.
1814
1815
Evidence: `ops/ci/push-remote-check.sh`, `.githooks/pre-push`,
1816
`OpenAgents.Forge.Pushes`, `OpenAgents.Forge.MirrorWatch`, and
1817
`test/openagents/push_remote_contract_test.exs`.
1818
1794 1819
## Executable proof index
1795 1820
1796 1821
This index is part of the ledger. Every `Current` invariant has at least one

@@ -1874,3 +1899,4 @@ contract; the invariant prose above defines the assertion, not the filename.

1874 1899
| STATUS-001 | `test/openagents/network_status_test.exs`, `test/openagents_web/live/network_status_live_test.exs` |
1875 1900
| TRANSPARENCY-001 | `test/openagents/forge/visibility_test.exs`, `test/openagents/forge/browse_test.exs`, `test/openagents_web/live/code_live_test.exs` |
1876 1901
| REPOSITORY-001 | `test/openagents/repository_lifecycle_test.exs`, `test/openagents/repositories/provisioner_test.exs`, `test/openagents_web/controllers/repository_controller_test.exs`, `test/openagents/forge/git_http_test.exs` |
1902
| REPOSITORY-002 | `ops/ci/push-remote-check.sh`, `test/openagents/push_remote_contract_test.exs` |
docs/operations/release-deployment-fallbacks.md modified +3

@@ -28,6 +28,9 @@ Install the repository hook once on each owned development or release machine:

28 28
git config core.hooksPath .githooks
29 29
```
30 30
31
The hook refuses any push aimed somewhere other than the forge before it
32
considers the gate, so an installed hook enforces REPOSITORY-002 as well.
33
31 34
Provision a disposable PostgreSQL database with pgvector already installed,
32 35
then run the complete exact-SHA gate:
33 36
ops/ci/push-remote-check.sh added +52

@@ -0,0 +1,52 @@

1
#!/bin/sh
2
set -eu
3
4
# Refuses a push to anything but the OpenAgents forge.
5
#
6
# The forge is the authority for this repository. It records every push in the
7
# durable WAL, and it mirrors `main` to GitHub itself
8
# (`OpenAgents.Forge.Pushes.mirror_now/1`, watched by
9
# `OpenAgents.Forge.MirrorWatch`). A push sent straight to GitHub arrives
10
# behind the forge's back: the WAL never sees those objects, the mirror watch
11
# compares the forge against a mirror that is now ahead of it, and the
12
# divergence stays invisible until a clone disagrees with the site.
13
#
14
# Git hands a pre-push hook the remote's name and URL on argv. Called directly,
15
# take the same two arguments; a name alone is resolved through `git remote`.
16
17
remote_name=${1:-}
18
remote_url=${2:-}
19
20
if [ -z "$remote_url" ] && [ -n "$remote_name" ]; then
21
  remote_url=$(git remote get-url "$remote_name" 2>/dev/null || printf '%s' "$remote_name")
22
fi
23
24
case "$remote_url" in
25
  https://openagents.com/* | https://*.openagents.com/* | \
26
  https://*@openagents.com/* | https://*@*.openagents.com/* | \
27
  ssh://*openagents.com/* | git@openagents.com:* | git@*.openagents.com:*)
28
    exit 0
29
    ;;
30
esac
31
32
# Bounded and loud, for operator-directed recovery: mirroring by hand while the
33
# forge is down is a real need, and a refusal with no way through invites
34
# someone to delete the hook instead.
35
if [ "${OPENAGENTS_ALLOW_NON_FORGE_PUSH:-}" = "1" ]; then
36
  echo "push_remote_override remote=${remote_name:-unnamed} url=$remote_url" >&2
37
  exit 0
38
fi
39
40
cat >&2 <<MESSAGE
41
Refusing to push to ${remote_name:-this remote} ($remote_url).
42
43
Pushes go to the OpenAgents forge, which records them in the WAL and mirrors
44
main to GitHub itself. Pushing to GitHub directly leaves the forge behind its
45
own mirror.
46
47
  git push openagents HEAD:main
48
49
Set OPENAGENTS_ALLOW_NON_FORGE_PUSH=1 for operator-directed recovery only.
50
MESSAGE
51
52
exit 1
test/openagents/push_remote_contract_test.exs added +60

@@ -0,0 +1,60 @@

1
defmodule OpenAgents.PushRemoteContractTest do
2
  use ExUnit.Case, async: true
3
4
  @script "ops/ci/push-remote-check.sh"
5
6
  defp check(arguments, environment \\ []) do
7
    System.cmd("sh", [@script | arguments], env: environment, stderr_to_stdout: true)
8
  end
9
10
  test "a GitHub remote is refused, whatever its URL form" do
11
    for url <- [
12
          "git@github.com:OpenAgentsInc/openagents.com.git",
13
          "https://github.com/OpenAgentsInc/openagents.com.git",
14
          "ssh://git@github.com/OpenAgentsInc/openagents.com.git"
15
        ] do
16
      assert {output, 1} = check(["origin", url])
17
      assert output =~ "Refusing to push"
18
      assert output =~ "git push openagents HEAD:main"
19
    end
20
  end
21
22
  test "the forge is admitted over HTTPS and SSH" do
23
    for url <- [
24
          "https://openagents.com/OpenAgentsInc/openagents.com.git",
25
          "https://staging.openagents.com/OpenAgentsInc/openagents.com.git",
26
          "git@openagents.com:OpenAgentsInc/openagents.com.git"
27
        ] do
28
      assert {_output, 0} = check(["openagents", url])
29
    end
30
  end
31
32
  # A host that merely ends in the forge's name is a different host.
33
  test "a lookalike host is refused" do
34
    assert {_output, 1} = check(["origin", "https://openagents.com.example.net/x.git"])
35
  end
36
37
  test "the override admits one push and says so" do
38
    assert {output, 0} =
39
             check(
40
               ["origin", "git@github.com:OpenAgentsInc/openagents.com.git"],
41
               [{"OPENAGENTS_ALLOW_NON_FORGE_PUSH", "1"}]
42
             )
43
44
    assert output =~ "push_remote_override"
45
  end
46
47
  test "the pre-push hook runs the check before the release gate" do
48
    hook = File.read!(".githooks/pre-push")
49
50
    assert hook =~ "ops/ci/push-remote-check.sh"
51
    assert hook =~ "ops/ci/gate.sh"
52
53
    [check_at, gate_at] =
54
      for marker <- ["push-remote-check.sh", "gate.sh"] do
55
        hook |> String.split(marker) |> hd() |> String.length()
56
      end
57
58
    assert check_at < gate_at
59
  end
60
end

This page updates live while a promote is in flight · changelog