Skip to content

Commit bd595ea

Browse files
allenwoodsci-smoke
andauthored
chore(security): add public repository boundary (#195)
Co-authored-by: ci-smoke <ci@example.com>
1 parent 5690ce2 commit bd595ea

32 files changed

Lines changed: 6016 additions & 4 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../skills/hackforger-development

‎.claude/agents/forgejo-dev.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ model: opus
99

1010
You are an expert Go developer specializing in Forgejo's architecture. When implementing HackForger features:
1111

12+
Before editing, read and follow `skills/hackforger-development/SKILL.md`, including its public-repository boundary and private-content hydration rules.
13+
1214
## Architecture Rules
1315
1. **Layer discipline**: routers -> services -> models -> modules. Never import upward.
1416
2. **XORM patterns**: Use `xorm:"pk autoincr"` tags. Register tables in `models/hackforger/init.go`.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../skills/hackforger-development

‎.github/pull_request_template.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## Summary
2+
3+
<!-- Describe the reusable HackForger change. -->
4+
5+
## Verification
6+
7+
- [ ] I ran the focused tests for this change.
8+
- [ ] I ran `bash scripts/check-public-repository-boundary.sh`.
9+
- [ ] This PR contains no branded/customer content, real deployment facts,
10+
credentials, production evidence, or operator-local files.
11+
- [ ] Any business-specific counterpart was placed in the confirmed private
12+
business repository and is referenced only from its private handoff.
13+
- [ ] User-facing behavior was verified with appropriate runtime evidence;
14+
instance-specific evidence is stored outside this public repository.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Content publisher
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- v0.1-dev/hackforger
8+
- prod
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
publisher:
16+
name: Content publisher
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 10
19+
steps:
20+
- name: Check out repository
21+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
22+
with:
23+
persist-credentials: false
24+
25+
- name: Check scripts
26+
run: |
27+
bash -n deploy/content/*.sh deploy/content/tests/run.sh
28+
python3 -c 'import ast, pathlib; ast.parse(pathlib.Path("deploy/content/rename-exchange.py").read_text())'
29+
30+
- name: Run content publisher transaction suite
31+
run: bash deploy/content/tests/run.sh
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
name: Public repository boundary
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, synchronize, reopened, ready_for_review, edited]
6+
push:
7+
branches: ['**']
8+
tags: ['**']
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: public-boundary-${{ github.event.pull_request.number || github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
trusted-pr-boundary:
20+
if: github.event_name == 'pull_request_target'
21+
name: Trusted public-boundary policy
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 10
24+
permissions:
25+
contents: read
26+
pull-requests: read
27+
statuses: write
28+
steps:
29+
- name: Mark candidate boundary status pending
30+
id: pending_status
31+
env:
32+
GH_TOKEN: ${{ github.token }}
33+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
34+
run: |
35+
[[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]
36+
gh api --method POST "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
37+
-f state=pending \
38+
-f context='Public repository boundary' \
39+
-f description='Trusted default-branch policy is scanning this commit' \
40+
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
41+
42+
# The policy always comes from the protected default branch. The target
43+
# base is checked out separately and is used only as the opaque-file
44+
# baseline. Candidate code is data and is never executed.
45+
- name: Check out trusted default-branch policy
46+
id: policy_checkout
47+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
48+
with:
49+
repository: ${{ github.repository }}
50+
ref: ${{ github.event.repository.default_branch }}
51+
path: policy
52+
persist-credentials: false
53+
54+
- name: Check out target base baseline
55+
id: baseline_checkout
56+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
57+
with:
58+
repository: ${{ github.repository }}
59+
ref: ${{ github.event.pull_request.base.sha }}
60+
path: baseline
61+
persist-credentials: false
62+
63+
- name: Check out untrusted candidate as data
64+
id: candidate_checkout
65+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
66+
with:
67+
repository: ${{ github.event.pull_request.head.repo.full_name }}
68+
ref: ${{ github.event.pull_request.head.sha }}
69+
fetch-depth: 0
70+
path: candidate
71+
persist-credentials: false
72+
73+
- name: Ensure target base exists in candidate history
74+
id: candidate_base
75+
env:
76+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
77+
UPSTREAM_REPOSITORY: ${{ github.repository }}
78+
run: |
79+
[[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]]
80+
[[ "$UPSTREAM_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]
81+
if ! git -C candidate cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then
82+
GIT_TERMINAL_PROMPT=0 git \
83+
-c credential.helper= \
84+
-c protocol.version=2 \
85+
-c protocol.file.allow=never \
86+
-c protocol.ext.allow=never \
87+
-C candidate fetch --no-tags --depth=1 \
88+
"https://github.com/$UPSTREAM_REPOSITORY.git" \
89+
"$BASE_SHA:refs/boundary/base"
90+
fi
91+
resolved=$(git -C candidate rev-parse --verify "$BASE_SHA^{commit}")
92+
[ "$resolved" = "$BASE_SHA" ]
93+
94+
- name: Test trusted boundary policy
95+
id: policy_tests
96+
run: >-
97+
python3 -m unittest discover
98+
-s policy/scripts/ci
99+
-p 'test_*.py'
100+
101+
- name: Require guard files to match trusted policy
102+
id: guard_integrity
103+
run: >-
104+
python3 policy/scripts/ci/check_boundary_guard_integrity.py
105+
--trusted-root policy
106+
--candidate-root candidate
107+
108+
- name: Scan complete candidate tree with trusted policy
109+
id: candidate_scan
110+
env:
111+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
112+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
113+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
114+
run: |
115+
python3 policy/scripts/ci/check_public_repository_boundary.py \
116+
--root candidate \
117+
--policy policy/scripts/ci/private-content-markers.txt \
118+
--baseline-root baseline \
119+
--history-base-ref "$BASE_SHA" \
120+
--history-head-ref "$HEAD_SHA" \
121+
--ref-name "refs/heads/$HEAD_REF"
122+
123+
- name: Publish candidate boundary status
124+
if: always()
125+
env:
126+
GH_TOKEN: ${{ github.token }}
127+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
128+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
129+
PR_NUMBER: ${{ github.event.pull_request.number }}
130+
STATUS_STATE: ${{ steps.pending_status.outcome == 'success' && steps.policy_checkout.outcome == 'success' && steps.baseline_checkout.outcome == 'success' && steps.candidate_checkout.outcome == 'success' && steps.candidate_base.outcome == 'success' && steps.policy_tests.outcome == 'success' && steps.guard_integrity.outcome == 'success' && steps.candidate_scan.outcome == 'success' && 'success' || 'failure' }}
131+
run: |
132+
[[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]
133+
[[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]]
134+
[[ "$PR_NUMBER" =~ ^[0-9]+$ ]]
135+
current_head=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.sha)
136+
current_base=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .base.sha)
137+
if [ "$current_head" != "$HEAD_SHA" ] || [ "$current_base" != "$BASE_SHA" ]; then
138+
echo 'Skipping final status from a stale pull-request event.'
139+
exit 0
140+
fi
141+
if [ "$STATUS_STATE" = success ]; then
142+
description='Trusted public repository boundary passed'
143+
else
144+
description='Trusted public repository boundary failed'
145+
fi
146+
gh api --method POST "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
147+
-f state="$STATUS_STATE" \
148+
-f context='Public repository boundary' \
149+
-f description="$description" \
150+
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
151+
152+
pushed-tree-boundary:
153+
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && !github.event.deleted)
154+
name: Public repository boundary (advisory push scan)
155+
runs-on: ubuntu-latest
156+
timeout-minutes: 10
157+
steps:
158+
- name: Check out trusted default-branch policy
159+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
160+
with:
161+
repository: ${{ github.repository }}
162+
ref: ${{ github.event.repository.default_branch }}
163+
path: policy
164+
persist-credentials: false
165+
166+
- name: Check out pushed tree as data
167+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
168+
with:
169+
repository: ${{ github.repository }}
170+
ref: ${{ github.sha }}
171+
fetch-depth: 0
172+
path: candidate
173+
persist-credentials: false
174+
175+
- name: Test trusted boundary policy
176+
run: >-
177+
python3 -m unittest discover
178+
-s policy/scripts/ci
179+
-p 'test_*.py'
180+
181+
- name: Require guard files to match trusted policy
182+
run: >-
183+
python3 policy/scripts/ci/check_boundary_guard_integrity.py
184+
--trusted-root policy
185+
--candidate-root candidate
186+
187+
- name: Resolve the exact pushed history
188+
id: pushed_history
189+
env:
190+
AFTER_SHA: ${{ github.sha }}
191+
BEFORE_SHA: ${{ github.event.before }}
192+
PUSHED_REF: ${{ github.ref }}
193+
UPSTREAM_REPOSITORY: ${{ github.repository }}
194+
run: |
195+
[[ "$AFTER_SHA" =~ ^[0-9a-f]{40}$ ]]
196+
[[ "$PUSHED_REF" =~ ^refs/(heads|tags)/[A-Za-z0-9._/-]+$ ]]
197+
[[ "$UPSTREAM_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]
198+
199+
fetch_exact() {
200+
local oid=$1
201+
local destination=$2
202+
if ! git -C candidate cat-file -e "$oid" 2>/dev/null; then
203+
GIT_TERMINAL_PROMPT=0 git \
204+
-c credential.helper= \
205+
-c protocol.version=2 \
206+
-c protocol.file.allow=never \
207+
-c protocol.ext.allow=never \
208+
-C candidate fetch --no-tags --depth=1 \
209+
"https://github.com/$UPSTREAM_REPOSITORY.git" \
210+
"$oid:$destination"
211+
fi
212+
}
213+
214+
if [[ "$PUSHED_REF" == refs/tags/* ]]; then
215+
GIT_TERMINAL_PROMPT=0 git \
216+
-c credential.helper= \
217+
-c protocol.version=2 \
218+
-c protocol.file.allow=never \
219+
-c protocol.ext.allow=never \
220+
-C candidate fetch --no-tags --depth=1 \
221+
"https://github.com/$UPSTREAM_REPOSITORY.git" \
222+
"+$PUSHED_REF:refs/boundary/pushed-tag"
223+
pushed_object=$(git -C candidate rev-parse --verify refs/boundary/pushed-tag)
224+
else
225+
fetch_exact "$AFTER_SHA" refs/boundary/pushed
226+
pushed_object=$AFTER_SHA
227+
fi
228+
object_type=$(git -C candidate cat-file -t "$pushed_object")
229+
[ "$object_type" = commit ] || {
230+
echo 'Annotated tags and non-commit refs require dedicated security review.' >&2
231+
exit 1
232+
}
233+
candidate_commit=$(git -C candidate rev-parse --verify "$pushed_object^{commit}")
234+
[ "$candidate_commit" = "$AFTER_SHA" ]
235+
236+
if [[ "$BEFORE_SHA" =~ ^0{40}$ ]] || [ -z "$BEFORE_SHA" ]; then
237+
baseline_commit=$(git -C policy rev-parse --verify HEAD)
238+
else
239+
[[ "$BEFORE_SHA" =~ ^[0-9a-f]{40}$ ]]
240+
baseline_commit=$BEFORE_SHA
241+
fi
242+
fetch_exact "$baseline_commit" refs/boundary/baseline
243+
resolved_baseline=$(git -C candidate rev-parse --verify "$baseline_commit^{commit}")
244+
[ "$resolved_baseline" = "$baseline_commit" ]
245+
246+
echo "history_base=$baseline_commit" >> "$GITHUB_OUTPUT"
247+
echo "history_head=$candidate_commit" >> "$GITHUB_OUTPUT"
248+
249+
- name: Scan pushed tree against trusted default branch
250+
env:
251+
HISTORY_BASE: ${{ steps.pushed_history.outputs.history_base }}
252+
HISTORY_HEAD: ${{ steps.pushed_history.outputs.history_head }}
253+
PUSHED_REF: ${{ github.ref }}
254+
run: |
255+
python3 policy/scripts/ci/check_public_repository_boundary.py \
256+
--root candidate \
257+
--policy policy/scripts/ci/private-content-markers.txt \
258+
--baseline-ref "$HISTORY_BASE" \
259+
--history-base-ref "$HISTORY_BASE" \
260+
--history-head-ref "$HISTORY_HEAD" \
261+
--ref-name "$PUSHED_REF"

‎.gitignore‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ cpu.out
101101
/tests/**/*.git/**/*.sample
102102
/node_modules
103103
/.venv
104+
__pycache__/
105+
*.py[cod]
104106
/yarn.lock
105107
/yarn-error.log
106108
/npm-debug.log*
@@ -153,8 +155,11 @@ prime/
153155
/man
154156
tests/integration/api_activitypub_person_inbox_useractivity_test.go
155157

156-
# Agent Setup runtime (gitignored)
157-
.agents/
158+
# Agent runtime is local; canonical project skills are tracked through symlinks.
159+
/.agents/*
160+
!/.agents/skills/
161+
/.agents/skills/*
162+
!/.agents/skills/hackforger-development
158163

159164
# User-local launcher overrides
160165
claude.local.sh
@@ -165,10 +170,12 @@ claude.local.sh
165170
# Local environment / secret files (PG creds, admin password, etc.)
166171
/.env
167172
/.env.local
173+
/CLAUDE.local.md
168174
docs/tests/e2e/*.pdf
169175
.claude/*.local.md
170-
# Project-wide guard-rail hooks ARE committed (override the .local.md ignore)
171-
!.claude/hookify.protect-*.local.md
176+
.claude/projects/
177+
.claude/worktrees/
178+
.claude/scheduled_tasks.lock
172179

173180
# E2E test screenshots (temporary artifacts, hosted on GitHub Releases if needed)
174181
tests/screenshots/

‎AGENTS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# HackForger agent instructions
2+
3+
For every development, review, documentation, CI, content, or deployment task in this repository, read and follow [`skills/hackforger-development/SKILL.md`](skills/hackforger-development/SKILL.md) before editing.
4+
5+
HackForger is a public, business-neutral repository. Put branded content, customer or campaign material, real deployment facts, production runbooks, and runtime evidence in the corresponding private business repository. Keep credentials out of every Git repository.

‎CODEOWNERS‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,28 @@ modules/structs/.* @Cyborus
5151
routers/api/v1/.* @Cyborus
5252
routers/api/forgejo/.* @Cyborus
5353
tests/integration/api_.* @Cyborus
54+
55+
# HackForger public repository boundary.
56+
/AGENTS.md @HackForger/developer
57+
/CLAUDE.md @HackForger/developer
58+
/CODEOWNERS @HackForger/developer
59+
/SECURITY.md @HackForger/developer
60+
/.env* @HackForger/developer
61+
/.gitattributes @HackForger/developer
62+
/.gitignore @HackForger/developer
63+
/.gitmodules @HackForger/developer
64+
/.agents/skills/ @HackForger/developer
65+
/.claude/ @HackForger/developer
66+
/.github/ @HackForger/developer
67+
/custom/ @HackForger/developer
68+
/deploy/ @HackForger/developer
69+
/docs/ @HackForger/developer
70+
/options/hackforger-help/ @HackForger/developer
71+
/routers/web/hackforger/ @gusted @HackForger/developer
72+
/scripts/check-public-repository-boundary.sh @HackForger/developer
73+
/scripts/install-public-boundary-hook.sh @HackForger/developer
74+
/scripts/pre-push-public-boundary.sh @HackForger/developer
75+
/scripts/ci/ @HackForger/developer
76+
/services/hackforger/ @HackForger/developer
77+
/skills/ @HackForger/developer
78+
/templates/hackforger/ @beowulf @gusted @HackForger/developer

0 commit comments

Comments
 (0)