Install the forge-only push guard per clone

39b5f9ace52c · AtlantisPleb · · parent c445ea4d94a0

Install the forge-only push guard per clone

The guard already existed and bound nobody. `.githooks/pre-push` runs it, but
only where `core.hooksPath` points at that directory, and setting it also
turns on the full release gate for every push -- a disposable database and
minutes of wall clock before any commit leaves the machine. So the honest
choices were an unenforced policy or an unusable one.

`ops/dev/install-push-guard.sh` writes the guard alone to Git's default hook
path. Hooks live in the common directory, so one install covers every worktree
of the clone, and a machine that wants the gate as well still gets both from
`core.hooksPath`. The installer refuses to replace a hook it does not
recognize without `--force`, because someone else's pre-push is not ours to
discard.

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 AGENTS.md
  • modified INVARIANTS.md
  • added ops/dev/install-push-guard.sh
  • modified test/openagents/push_remote_contract_test.exs

Diff

4 files changed, +122 -7

AGENTS.md modified +9 -2

@@ -26,8 +26,15 @@ mirror; pushing to it directly leaves the forge behind a mirror it does not

26 26
know about, and nothing reports the divergence until a clone disagrees with
27 27
the site. Automatic mirroring to GitHub is not configured today, so GitHub
28 28
stays at whatever was last pushed to it. `ops/ci/push-remote-check.sh` refuses
29
a non-forge push, and `.githooks/pre-push` runs it. See `INVARIANTS.md`,
30
REPOSITORY-002.
29
a non-forge push. Install it once per clone, on every machine:
30
31
```sh
32
sh ops/dev/install-push-guard.sh
33
```
34
35
One install covers every worktree of that clone. Where `core.hooksPath` points
36
at `.githooks`, its `pre-push` runs the same guard ahead of the release gate.
37
See `INVARIANTS.md`, REPOSITORY-002.
31 38
32 39
## Project guidelines
33 40
INVARIANTS.md modified +10 -5

@@ -1818,10 +1818,15 @@ costs minutes. A bounded, logged override exists for operator-directed

1818 1818
recovery, such as mirroring by hand while the forge is unreachable.
1819 1819
1820 1820
The check is a guard, not a deployment: it refuses a wrong destination and
1821
makes no claim about the candidate. Enforcement requires `core.hooksPath`,
1822
which also enables the release gate on every push.
1823
1824
Evidence: `ops/ci/push-remote-check.sh`, `.githooks/pre-push`,
1821
makes no claim about the candidate. `ops/dev/install-push-guard.sh` installs
1822
it at Git's default hook path, so a clone refuses the wrong destination
1823
without also owing a release-gate receipt for every push; a machine that sets
1824
`core.hooksPath` runs the guard and the gate together instead. Neither is
1825
automatic: an uninstalled clone is unguarded, which is why the command belongs
1826
in `AGENTS.md` rather than in someone's memory.
1827
1828
Evidence: `ops/ci/push-remote-check.sh`, `ops/dev/install-push-guard.sh`,
1829
`.githooks/pre-push`,
1825 1830
`OpenAgents.Forge.Pushes`, `OpenAgents.Forge.MirrorWatch`, and
1826 1831
`test/openagents/push_remote_contract_test.exs`.
1827 1832

@@ -1908,4 +1913,4 @@ contract; the invariant prose above defines the assertion, not the filename.

1908 1913
| STATUS-001 | `test/openagents/network_status_test.exs`, `test/openagents_web/live/network_status_live_test.exs` |
1909 1914
| TRANSPARENCY-001 | `test/openagents/forge/visibility_test.exs`, `test/openagents/forge/browse_test.exs`, `test/openagents_web/live/code_live_test.exs` |
1910 1915
| 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` |
1911
| REPOSITORY-002 | `ops/ci/push-remote-check.sh`, `test/openagents/push_remote_contract_test.exs` |
1916
| REPOSITORY-002 | `ops/ci/push-remote-check.sh`, `ops/dev/install-push-guard.sh`, `test/openagents/push_remote_contract_test.exs` |
ops/dev/install-push-guard.sh added +51

@@ -0,0 +1,51 @@

1
#!/bin/sh
2
set -eu
3
4
# Installs the forge-only push guard into this clone.
5
#
6
# `ops/ci/push-remote-check.sh` is the guard itself, and `.githooks/pre-push`
7
# already runs it -- but `.githooks` binds only where `core.hooksPath` points
8
# at it, and that also turns on the full release gate for every push. This
9
# installs the guard alone, at Git's default hook path, so a clone refuses a
10
# push to GitHub without also demanding a release-gate receipt.
11
#
12
# Hooks live in the common directory, so one install covers every worktree of
13
# this clone. Run it once per clone, on every machine.
14
#
15
#   sh ops/dev/install-push-guard.sh [--force]
16
17
force=${1:-}
18
19
repo_root=$(git rev-parse --show-toplevel)
20
common_dir=$(cd "$(git rev-parse --git-common-dir)" && pwd)
21
hook_path="$common_dir/hooks/pre-push"
22
marker='openagents-push-guard'
23
24
configured_path=$(git config --get core.hooksPath || true)
25
26
if [ -n "$configured_path" ]; then
27
  echo "core.hooksPath is set to $configured_path, so Git ignores $hook_path." >&2
28
  echo "That path's own pre-push hook decides; nothing installed." >&2
29
  exit 1
30
fi
31
32
if [ -e "$hook_path" ] && ! grep -q "$marker" "$hook_path" 2>/dev/null && [ "$force" != "--force" ]; then
33
  echo "$hook_path exists and is not the push guard. Re-run with --force to replace it." >&2
34
  exit 1
35
fi
36
37
mkdir -p "$common_dir/hooks"
38
39
cat >"$hook_path" <<'HOOK'
40
#!/bin/sh
41
# openagents-push-guard — installed by ops/dev/install-push-guard.sh
42
set -eu
43
44
repo_root=$(git rev-parse --show-toplevel)
45
exec "$repo_root/ops/ci/push-remote-check.sh" "$@"
46
HOOK
47
48
chmod +x "$hook_path"
49
50
echo "Installed the forge-only push guard at $hook_path"
51
echo "It covers every worktree of $repo_root."
test/openagents/push_remote_contract_test.exs modified +52

@@ -2,6 +2,7 @@ defmodule OpenAgents.PushRemoteContractTest do

2 2
  use ExUnit.Case, async: true
3 3
4 4
  @script "ops/ci/push-remote-check.sh"
5
  @installer "ops/dev/install-push-guard.sh"
5 6
6 7
  defp check(arguments, environment \\ []) do
7 8
    System.cmd("sh", [@script | arguments], env: environment, stderr_to_stdout: true)

@@ -44,6 +45,57 @@ defmodule OpenAgents.PushRemoteContractTest do

44 45
    assert output =~ "push_remote_override"
45 46
  end
46 47
48
  test "the installer leaves a clone refusing GitHub" do
49
    root = Path.join(System.tmp_dir!(), "push-guard-#{System.unique_integer([:positive])}")
50
    on_exit(fn -> File.rm_rf!(root) end)
51
52
    File.mkdir_p!(Path.join(root, "ops/ci"))
53
    File.mkdir_p!(Path.join(root, "ops/dev"))
54
    File.cp!(@script, Path.join(root, @script))
55
    File.cp!(@installer, Path.join(root, @installer))
56
    {_output, 0} = System.cmd("git", ["init", "-q", root])
57
58
    assert {_output, 0} = System.cmd("sh", [@installer], cd: root, stderr_to_stdout: true)
59
60
    hook = Path.join(root, ".git/hooks/pre-push")
61
    assert File.exists?(hook)
62
63
    assert {output, 1} =
64
             System.cmd("sh", [hook, "origin", "git@github.com:OpenAgentsInc/x.git"],
65
               cd: root,
66
               stderr_to_stdout: true
67
             )
68
69
    assert output =~ "Refusing to push"
70
71
    # The guard alone: a clone that installs it must not inherit the release
72
    # gate, which needs a disposable database and minutes of wall clock.
73
    refute File.read!(hook) =~ "gate.sh"
74
  end
75
76
  test "the installer refuses to overwrite a hook it does not recognize" do
77
    root = Path.join(System.tmp_dir!(), "push-guard-#{System.unique_integer([:positive])}")
78
    on_exit(fn -> File.rm_rf!(root) end)
79
80
    File.mkdir_p!(Path.join(root, "ops/ci"))
81
    File.mkdir_p!(Path.join(root, "ops/dev"))
82
    File.cp!(@script, Path.join(root, @script))
83
    File.cp!(@installer, Path.join(root, @installer))
84
    {_output, 0} = System.cmd("git", ["init", "-q", root])
85
86
    hook = Path.join(root, ".git/hooks/pre-push")
87
    File.write!(hook, "#!/bin/sh\nexit 0\n")
88
89
    assert {output, 1} = System.cmd("sh", [@installer], cd: root, stderr_to_stdout: true)
90
    assert output =~ "--force"
91
    assert File.read!(hook) == "#!/bin/sh\nexit 0\n"
92
93
    assert {_output, 0} =
94
             System.cmd("sh", [@installer, "--force"], cd: root, stderr_to_stdout: true)
95
96
    assert File.read!(hook) =~ "openagents-push-guard"
97
  end
98
47 99
  test "the pre-push hook runs the check before the release gate" do
48 100
    hook = File.read!(".githooks/pre-push")
49 101

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