Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/manual-moderation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Resolve escalated moderation

on:
issue_comment:
types: [created]

jobs:
resolve:
if: ${{ github.event.issue.pull_request && github.event.comment.user.login == 'flemming-n-larsen' && startsWith(github.event.comment.body, '/moderation approve ') }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349
id: app
with:
app-id: ${{ secrets.RUMBLE_MODERATOR_APP_ID }}
private-key: ${{ secrets.RUMBLE_MODERATOR_PRIVATE_KEY }}
owner: robocode-dev
repositories: rumble-bots
- name: Resolve only the current pending commit
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
PR_NUMBER: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
HEAD_SHA="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .head.sha)"
python moderation/moderate.py --pr "$PR_NUMBER" --sha "$HEAD_SHA" --comment-id "$COMMENT_ID"
75 changes: 75 additions & 0 deletions .github/workflows/moderate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Moderate bot submissions

on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]

concurrency:
group: moderate-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
assess:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
outputs:
kind: ${{ steps.assess.outputs.kind }}
reason_b64: ${{ steps.assess.outputs.reason_b64 }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Fetch and pin the untrusted PR as data
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[0-9]+$ && "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]
git fetch --no-tags origin "+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/moderation-${PR_NUMBER}"
[[ "$(git rev-parse "refs/remotes/origin/moderation-${PR_NUMBER}")" == "$HEAD_SHA" ]]
- name: Pull pinned runtime image
run: docker pull ghcr.io/robocode-dev/rumble-client@sha256:32ab3259f0ce69f319174b54ba36f4d85949c1a30ea4e65a94010b9c931acf2d
- name: Assess source and run isolated battle
id: assess
env:
BASE_SHA: ${{ github.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_OWNER: ${{ github.event.pull_request.user.login }}
run: |
python moderation/assess.py --base "$BASE_SHA" --head "$HEAD_SHA" --owner "$PR_OWNER" --output "$RUNNER_TEMP/moderation.json"

verdict:
needs: assess
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349
id: app
with:
app-id: ${{ secrets.RUMBLE_MODERATOR_APP_ID }}
private-key: ${{ secrets.RUMBLE_MODERATOR_PRIVATE_KEY }}
owner: robocode-dev
repositories: rumble-bots
- name: Apply verdict to the exact PR head
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
VERDICT_KIND: ${{ needs.assess.outputs.kind }}
VERDICT_REASON_B64: ${{ needs.assess.outputs.reason_b64 }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
python moderation/moderate.py --pr "$PR_NUMBER" --sha "$HEAD_SHA" --base-sha "$BASE_SHA" --kind "$VERDICT_KIND" --reason-b64 "$VERDICT_REASON_B64" --run-url "$RUN_URL"
8 changes: 4 additions & 4 deletions .github/workflows/publish-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
permissions:
contents: write
pull-requests: read
checks: read
statuses: read

concurrency:
group: publish-catalog
Expand Down Expand Up @@ -56,8 +56,8 @@ jobs:
head_sha="$(jq -r '.head.sha' <<< "$pr")"
owner="$(jq -r '.user.login' <<< "$pr")"
[[ "$head_sha" =~ ^[0-9a-f]{40}$ && "$owner" =~ ^[A-Za-z0-9-]+$ ]] || { echo "Unexpected pull request identity." >&2; exit 1; }
successful_validation="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" --jq '[.check_runs[] | select(.name == "validate" and .app.slug == "github-actions" and .conclusion == "success")] | length')"
[[ "$successful_validation" -gt 0 ]] || { echo "The merged pull request has no successful validate check for its head commit." >&2; exit 1; }
moderation_state="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/status" --jq '[.statuses[] | select(.context == "bot-moderation")][0].state')"
[[ "$moderation_state" == "success" ]] || { echo "The merged pull request has no current successful bot-moderation status for its head commit." >&2; exit 1; }
{
echo "pull_request_number=$pr_number"
echo "head_sha=$head_sha"
Expand All @@ -70,7 +70,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# The separate pull_request validation ran bot smoke without write access. This publisher
# The separate moderation job ran the submitted bot without write access. This publisher
# runs only on trusted main and performs static catalog generation; it never launches bot code.
- name: Regenerate catalog without running bots
env:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ name: Validate bot submission
on:
pull_request:
paths:
- bots/**
- scripts/**
- templates/**
- tests/**
- .github/workflows/validate.yml
- .github/workflows/publish-catalog.yml
- .github/workflows/moderate.yml
- .github/workflows/manual-moderation.yml
- moderation/**

permissions:
contents: read
Expand Down
13 changes: 12 additions & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
* @flemming-n-larsen
# Human review is required for automation, policy, templates, and publisher inputs.
/.github/ @flemming-n-larsen
/moderation/ @flemming-n-larsen
/scripts/ @flemming-n-larsen
/templates/ @flemming-n-larsen
/CODEOWNERS @flemming-n-larsen
/GOVERNANCE.md @flemming-n-larsen
/CONTRIBUTING.md @flemming-n-larsen
/README.md @flemming-n-larsen
/bots/banned.json @flemming-n-larsen
/bots/index.json @flemming-n-larsen
/bots/owners.json @flemming-n-larsen
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Each bot directory's `license` field is its license grant for the complete direc

The first merged pull request for a bot name reserves that name for its submitting forge account. Only that account, or an account later registered in `bots/owners.json` by an already registered account, may submit later versions. A source change requires a version bump; only the latest version remains active. Each owner has five active entries by default, including a TwinDuel team.

Use `python scripts/validate_bot.py --root . --owner <your-forge-account> --smoke` before opening a pull request. CI is authoritative. A green check does not replace moderator review, especially for a first submission.
Use `python scripts/validate_bot.py --root . --owner <your-forge-account> --smoke` before opening a pull request. CI is authoritative. A bot-only PR can be merged automatically after source and security checks and a completed one-round battle. Keep launcher scripts faithful to the published starter; modified launchers, team metadata, security uncertainty and unrelated file changes go to Flemming. If the bot does not run, the PR receives a changes-requested review explaining why; push a fix to rerun moderation.

Start from the [runnable templates and copy-and-rename instructions](templates/README.md). They include pinned dependencies, both launchers and the required `RUMBLE_SMOKE_READY <BotName>` branch. The validator requires Python 3.11+ and `sh`; Windows users can install Git for Windows and make its shell available. It sets `RUMBLE_SMOKE=1`, invokes each individual bot's `.sh` launcher and requires a successful exit with the marker within 20 seconds. This verifies startup rather than a server handshake. Commit each `.sh` with executable mode `100755`, using `git update-index --chmod=+x` after staging it. Team entries contain JSON only and skip individual-bot launcher checks.

Expand Down
14 changes: 9 additions & 5 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Governance

The `robocode-dev` organization owns this repository. At least three moderators should retain ownership and CODEOWNERS review rights before ranked submissions open. One moderator approval and successful validation are required for a new bot; trusted owners may receive auto-merge eligibility for version bumps after repeated clean submissions.
The `robocode-dev` organization owns this repository. Flemming is currently the human moderator; recruiting at least three people with ownership and CODEOWNERS review rights remains a future goal, not a condition for accepting ranked submissions. GitHub moderation automatically approves and merges a new bot or version update only when the pull request changes one bot directory, the source and dependency checks find no restricted behavior, and the bot completes one round against a trusted reference. A failed bot receives a changes-requested review with a diagnostic. Security uncertainty, infrastructure failure, TwinDuel team metadata, and non-bot changes stay blocked for Flemming's decision.

Moderators maintain `bots/banned.json` through reviewed pull requests. A ban can cover an account or a bot and can be temporary. CI prevents a banned account from submitting and excludes disqualified bots from the generated catalog. Facts in the future `rumble-data` repository are never deleted merely because an entry is disqualified.

Moderators resolve name-squatting, lost-account recovery, license complaints, and appeal requests case by case. A credible copyright complaint removes the bot from the working tree and disqualifies it pending resolution.

Once each quarter, a moderator performs a fork drill: fork the repository, enable Actions, run validation, and regenerate the catalog without credentials other than the forge-provided token. Record the outcome in a GitHub issue.
Once each quarter, a moderator performs a fork drill: fork the repository, enable Actions, run validation, and regenerate the catalog. Record the outcome in a GitHub issue. A fork can run the source validator without moderator credentials; automatic review and merge require the installed moderation App.

## GitHub Actions permissions

The pull-request validation workflow requests `contents: read`. Public fork pull requests receive a read-only `GITHUB_TOKEN` even if a workflow asks for write access, and GitHub may require a maintainer with write permission to approve a first-time contributor's workflow run. Approve the validation run only after checking the proposed source; never grant the validation workflow write access to the upstream repository.
The `pull_request_target` moderation workflow runs trusted code from `main` for every pull request. Its assessment job has a read-only token and no secrets. It fetches the exact PR commit as data, checks eligibility and source, then runs one real Battle Runner round in a pinned, non-root container with a read-only bot mount, limited resources, no capabilities and no GitHub credentials. Its separate verdict job never checks out or executes PR content. The repository's Actions event policy must explicitly allow this trusted workflow's `pull_request_target` event. Restrict this policy to this workflow; never run untrusted source in the verdict job.

After a bot pull request is merged, the publisher runs on a push to `main` that changes `bots/**`. It uses the associated merged pull request to check that the submission's `validate` check passed, then checks out trusted `main` and runs static catalog generation without `--smoke`. The publisher must never execute a submitted bot launcher with its write token. Its `GITHUB_TOKEN` needs `contents: write`, `pull-requests: read`, and `checks: read`, as declared in `.github/workflows/publish-catalog.yml`; it uses no personal access token or repository secret.
Install a dedicated GitHub App on **only** `robocode-dev/rumble-bots`, with Contents, Pull requests, Commit statuses and Issues repository permissions set to write. Store its ID and private key as `RUMBLE_MODERATOR_APP_ID` and `RUMBLE_MODERATOR_PRIVATE_KEY` repository secrets. The verdict job obtains a short-lived installation token. It approves and merges only the tested head SHA after the required `bot-moderation` status succeeds; new commits invalidate that result. A failed bot gets a failing status and `REQUEST_CHANGES`. Ambiguous and non-bot PRs receive a pending status and a comment naming Flemming and the reason. Flemming may review a pending PR and comment `/moderation approve <reason>` to make its current head green; Flemming still merges that PR manually. Do not add the moderation App to branch-rule bypasses.

Protect `main` with a required `bot-moderation` status from the moderation App, a pull-request requirement, and code-owner review for the protected paths listed in `CODEOWNERS`. Bot directories have no human code owner, so a clean bot can merge after the App's approval. The existing catalog publisher pushes generated files to `main`, so allow `github-actions[bot]` to bypass that rule for its trusted publisher workflow. Flemming needs a pull-request-only bypass for policy changes he authors, because GitHub will not count an author's approval of their own pull request; use this exception only after human review of the change. Do not grant any bypass to the moderation App. Keep the publisher's `contents: write` token confined to that workflow. Verify the App, status source, branch rule and publisher on a test PR before enabling auto-merge. The automatic security screen is a conservative filter, not proof that arbitrary bot source is harmless; clients still execute bots in their own sandbox.

After a bot pull request is merged by the App, the publisher runs on a push to `main` that changes `bots/**`. It checks that the merged PR's exact head had a successful `bot-moderation` status, then checks out trusted `main` and runs static catalog generation without executing a submitted launcher. Its `GITHUB_TOKEN` needs `contents: write`, `pull-requests: read`, and `statuses: read`, as declared in `.github/workflows/publish-catalog.yml`; it uses no personal access token or repository secret.

For publication to work, GitHub Actions must be enabled and the repository or organization policy must permit these token scopes. The repository's **Settings → Actions → General → Workflow permissions** should allow read and write permissions for `GITHUB_TOKEN` (currently enabled for this repository); the workflow itself still grants only the scopes listed above. If `main` is protected by branch rules, allow `github-actions[bot]` to push the generated catalog files, or publishing will fail. Recheck these settings if organization policy or branch rules change.

GitHub may hold a fork contributor's validation run for approval by a maintainer with write access. The publisher does not need `pull_request_target` or a privileged fork workflow. On a `main` push, it retries the commit-to-PR lookup for up to 55 seconds to allow GitHub to publish the association. To recover a merged submission whose publication still failed, a maintainer can run **Publish bot catalog** from the `main` branch with its merged pull-request number. The dispatch verifies that it merged into `main` and that its head commit has a successful `validate` check before it regenerates the catalog using the PR author's account as owner.
On a `main` push, the publisher retries the commit-to-PR lookup for up to 55 seconds to allow GitHub to publish the association. To recover a merged submission whose publication still failed, a maintainer can run **Publish bot catalog** from `main` with its merged PR number. The dispatch verifies that it merged into `main` and that its head commit passed moderation before it regenerates the catalog using the PR author's account as owner.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ See the [bot author guide](https://robocode.dev/rumble/bot-author-guide) for the
3. Declare one permitted SPDX license: `MIT`, `Apache-2.0`, `BSD-3-Clause`, or `GPL-3.0-or-later`.
4. Run `python scripts/validate_bot.py --root . --owner <your-forge-account> --smoke` before opening a pull request.

Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full submission, ownership, licensing, and moderation rules. The [template guide](templates/README.md#smoke-check-and-executable-scripts) explains smoke markers, runtime prerequisites and executable bits. JSON-only TwinDuel teams use the [team template](templates/README.md#json-only-twinduel-team). The validator's smoke check starts the source entry point; it does not establish a server connection or completed battle. Connecting the smoke check to a temporary Tank Royale server is the next hardening increment before public ranked submissions open.
Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full submission, ownership, licensing, and moderation rules. The [template guide](templates/README.md#smoke-check-and-executable-scripts) explains smoke markers, runtime prerequisites and executable bits. JSON-only TwinDuel teams use the [team template](templates/README.md#json-only-twinduel-team). Local smoke checks only prove source startup; GitHub moderation runs an isolated, complete one-round battle before a clean bot-only PR can be merged automatically.

## Catalog

`bots/index.json` and `bots/owners.json` are generated by CI and must never be edited in a pull request. A catalog entry contains the bot identity, owner, source path, source-tree SHA-256, active lifecycle status, and `teamMembers`. Individual bots publish an empty member list; a TwinDuel team publishes exactly two active `<name> <version>` member identities, so a member release always forces the team entry to be resubmitted. A team directory contains nothing but its own JSON, its members may live under different platform directories, and its published platform is the members' shared platform or `Mixed`. Only `active` entries are eligible for matchmaking.

This repository is designed to be forkable: its validator is standard-library Python. Pull-request validation runs bot smoke checks with read-only permissions; after merge, a trusted `main` publisher verifies the merged pull request passed validation and regenerates catalog data without launching bot code. See [moderator governance](GOVERNANCE.md#github-actions-permissions) for required Actions permissions and recovery.
This repository is designed to be forkable: its validator is standard-library Python. Pull-request assessment runs bot source with read-only permissions inside a restricted container; a separate trusted job makes the exact-commit review and merge decision. After merge, the `main` publisher verifies moderation and regenerates catalog data without launching bot code. See [moderator governance](GOVERNANCE.md#github-actions-permissions) for required Actions permissions and recovery.
Source-only catalog for ranked Tank Royale Rumble bots
Loading
Loading