mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-27 07:55:07 +00:00
Two CI-layer fixes found during the v3.25.0 review. Same layer, both small, so they land together (Iron Rule 11). #198 — alignment.yml did not run on a models.json-only PR. The `paths:` filter listed server.mjs / setup.mjs / scripts / lib / ocp / ocp-connect, but not models.json. That made CLAUDE.md hard-requirement #2 ("CI blacklist pass") vacuous for exactly the PRs that change model routing: confirmed on #192, where only gitleaks and test-features ran. models.json is not inert data. It drives MODEL_MAP and VALID_MODELS, the default request model, and — since ADR 0009 — the global MAX_PROMPT_CHARS truncation budget. A models.json-only PR can therefore change server.mjs's runtime behavior with the guardrail never running. models.schema.json is included for the same reason one level up: loosening the schema is what would let a malformed models.json through. Adding paths only widens coverage; the blacklist grep still scans server.mjs, so this costs nothing and closes the gap. Per CLAUDE.md this is a PR amendment to alignment.yml, which is the sanctioned way to change it (removing entries would need an ALIGNMENT.md amendment; nothing is removed here). #202 — release.yml's no-CHANGELOG fallback would have failed the release job. The branch wrote an output named `notes` and exited WITHOUT creating /tmp/release-notes.md, while the create step hard-codes `--notes-file /tmp/release-notes.md`. So the one path that exists to "degrade to minimal notes" instead produced a failed release. Verified both directions by extracting the step's actual script from the YAML and running it, rather than reading it: PRE-FIX, no CHANGELOG -> exit 0, GITHUB_OUTPUT="notes=Release v3.25.0", /tmp/release-notes.md MISSING -> gh release create --notes-file WOULD FAIL POST-FIX, no CHANGELOG -> exit 0, notes_file set, file present ("Release v3.25.0") POST-FIX, with CHANGELOG -> file present, first line "## v3.25.0 — 2026-07-27" Fixed by writing the file in that branch, and by removing the hard-coded-path coupling entirely: both branches now set `notes_file` and the create step consumes `steps.notes.outputs.notes_file`. That output existed already and was never read — the two only agreed by accident. Also added `set -euo pipefail` (the step previously ran unset-tolerant) and an echo of the resolved notes before creating the release, so a wrong-looking body is visible in the job log instead of only on the published release. NOT changed: the empty-extraction guard. My issue text suggested adding one, but `if [ ! -s "$NOTES" ]` was already there and already handles a heading mismatch. Correcting that claim on #202 rather than taking credit for it. Both workflows re-validated as YAML. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8
215 lines
8.6 KiB
YAML
215 lines
8.6 KiB
YAML
name: Alignment Guardrail
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'server.mjs'
|
|
- 'setup.mjs'
|
|
- 'scripts/**'
|
|
- 'lib/**'
|
|
- 'ocp'
|
|
- 'ocp-connect'
|
|
# models.json is NOT inert data: it drives MODEL_MAP and VALID_MODELS, the default
|
|
# request model, and — since ADR 0009 — the global MAX_PROMPT_CHARS truncation budget.
|
|
# A models.json-only PR can therefore change server.mjs's runtime behavior, and before
|
|
# this the guardrail simply did not run on one (#198). models.schema.json is included
|
|
# for the same reason one level up: loosening the schema is what would let a bad
|
|
# models.json through.
|
|
- 'models.json'
|
|
- 'models.schema.json'
|
|
- '.github/workflows/alignment.yml'
|
|
|
|
jobs:
|
|
blacklist:
|
|
name: server.mjs blacklist (hard fail)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Scan server.mjs for hallucinated tokens
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ ! -f server.mjs ]; then
|
|
echo "server.mjs not found; nothing to scan."
|
|
exit 0
|
|
fi
|
|
|
|
# Blacklisted tokens — two kinds (see ALIGNMENT.md "OAuth token-host verification"):
|
|
# (1) known LLM hallucinations (e.g. the 2026-04-11 /api/oauth/usage drift), and
|
|
# (2) pinned wrong-host variants of a VERIFIED Class A endpoint (a hit means a
|
|
# drift to a known-wrong host, not necessarily a hallucination).
|
|
# Extend only via an ALIGNMENT.md amendment PR. Matched as fixed strings vs server.mjs.
|
|
BLACKLIST=(
|
|
"api.anthropic.com/api/oauth/usage"
|
|
"console.anthropic.com/v1/oauth/token"
|
|
)
|
|
|
|
FAIL=0
|
|
for token in "${BLACKLIST[@]}"; do
|
|
if sed "s|//.*\$||" server.mjs | grep -n -F "$token"; then
|
|
echo "::error file=server.mjs::Blacklisted token '$token' detected in server.mjs."
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
cat <<'EOF'
|
|
|
|
============================================================
|
|
ALIGNMENT GUARDRAIL FAILURE
|
|
============================================================
|
|
server.mjs contains a token on the OCP alignment blacklist.
|
|
|
|
These tokens are either LLM hallucinations that never appeared in cli.js,
|
|
or pinned wrong-host variants of a verified Class A endpoint (a drift).
|
|
See ALIGNMENT.md -> "Historical Lesson: The 2026-04-11 Drift"
|
|
(commit b87992f) for the full incident record.
|
|
|
|
Required action:
|
|
1. Remove the token from server.mjs.
|
|
2. grep the reference cli.js for the operation you
|
|
intended and cite the real line numbers.
|
|
3. See ALIGNMENT.md Rules 1, 2, and 5.
|
|
|
|
Do not add allowlist entries to this workflow without an
|
|
amendment PR to ALIGNMENT.md (see Amendment Procedure).
|
|
============================================================
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
echo "Blacklist scan clean."
|
|
|
|
port-spot:
|
|
name: port literal SPOT (hard fail)
|
|
# Background: from 2026-05-08 (PR #71 dogfood accident) through 2026-05-13
|
|
# a hardcoded "3478" in scripts/upgrade.mjs + scripts/doctor.mjs cascaded
|
|
# into wrong baseUrl writes for the OpenClaw "claude-local" provider,
|
|
# taking out the "大内总管" Telegram agent.
|
|
#
|
|
# Rule: the only places allowed to write a literal port number in source
|
|
# are (a) lib/constants.mjs (the SPOT), (b) bash scripts ocp / ocp-connect
|
|
# (which can't import .mjs and must keep the literal in sync — flagged
|
|
# with a `// keep in sync with lib/constants.mjs` style comment), and
|
|
# (c) test-features.mjs (intentionally pins historical ports for plist /
|
|
# systemd parser tests). Everything else MUST import from lib/constants.mjs.
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Scan for hardcoded port literals outside SPOT
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Files/paths exempt from the SPOT requirement.
|
|
EXEMPT_REGEX='^(lib/constants\.mjs|test-features\.mjs|ocp|ocp-connect|CHANGELOG\.md|README\.md|docs/|\.github/workflows/alignment\.yml)'
|
|
|
|
# Hardcoded port literals to forbid in non-exempt source.
|
|
FORBIDDEN_PORTS=("3478" "3456")
|
|
|
|
FAIL=0
|
|
for port in "${FORBIDDEN_PORTS[@]}"; do
|
|
HITS="$(git ls-files | grep -E '\.(mjs|js|ts|json)$' \
|
|
| xargs grep -n -E "[^0-9]${port}[^0-9]" 2>/dev/null \
|
|
| grep -v -E "${EXEMPT_REGEX}" \
|
|
|| true)"
|
|
if [ -n "$HITS" ]; then
|
|
echo "::error::Hardcoded port literal '${port}' found outside lib/constants.mjs:"
|
|
echo "$HITS"
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
cat <<'EOF'
|
|
|
|
============================================================
|
|
PORT LITERAL SPOT VIOLATION
|
|
============================================================
|
|
A hardcoded TCP port literal was found in a source file
|
|
that should import from lib/constants.mjs instead.
|
|
|
|
Background: this rule exists because between 2026-05-08 and
|
|
2026-05-13 a stray hardcoded "3478" in scripts/upgrade.mjs
|
|
and scripts/doctor.mjs cascaded into downstream OpenClaw
|
|
config writes, taking out the OpenClaw Telegram agent.
|
|
See v3.16.3 CHANGELOG and lib/constants.mjs header comment.
|
|
|
|
Required action:
|
|
1. Import DEFAULT_PORT (or related constant) from
|
|
lib/constants.mjs instead of hardcoding the literal.
|
|
2. If the file genuinely cannot import .mjs (e.g. bash
|
|
script), add it to EXEMPT_REGEX in this workflow and
|
|
add a `keep in sync with lib/constants.mjs` comment
|
|
at the reference.
|
|
3. For test files that intentionally pin historical ports
|
|
(test-features.mjs), the regex already exempts them.
|
|
|
|
============================================================
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
echo "Port SPOT scan clean."
|
|
|
|
commit-citation:
|
|
name: commit message citation (soft check)
|
|
runs-on: ubuntu-latest
|
|
# Soft check: reports a warning but does not block merge. Reviewers
|
|
# are expected to enforce per CLAUDE.md. Escalate to hard-fail via
|
|
# an ALIGNMENT.md amendment if drift recurs.
|
|
continue-on-error: true
|
|
steps:
|
|
- name: Checkout full history
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Scan PR commits for uncited assertions
|
|
shell: bash
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ -z "${BASE_SHA:-}" ] || [ -z "${HEAD_SHA:-}" ]; then
|
|
echo "No PR context; skipping."
|
|
exit 0
|
|
fi
|
|
|
|
# Collect commit messages in range.
|
|
MSGS="$(git log --format=%B "${BASE_SHA}..${HEAD_SHA}")"
|
|
|
|
# Look for assertions of the form "Claude Code uses ..." or
|
|
# "cli.js uses ..." (case-insensitive). For each hit, require
|
|
# a citation in the same commit message in one of the forms:
|
|
# cli.js:NNNN (line-number citation)
|
|
# cli.js vE4 <name> (version + function-name citation)
|
|
# Absence of a citation is a soft finding.
|
|
|
|
WARN=0
|
|
# Split log into per-commit blocks for precise matching.
|
|
git log --format="%H" "${BASE_SHA}..${HEAD_SHA}" | while read -r sha; do
|
|
BODY="$(git log -1 --format=%B "$sha")"
|
|
if echo "$BODY" | grep -E -i -q '(claude[[:space:]]+code|cli\.js)[[:space:]]+uses'; then
|
|
if echo "$BODY" | grep -E -q '(cli\.js:[0-9]+|cli\.js[[:space:]]+v[A-Za-z0-9]+[[:space:]]+[A-Za-z_][A-Za-z0-9_]*)'; then
|
|
echo "OK $sha: assertion cited."
|
|
else
|
|
echo "::warning::Commit $sha asserts 'Claude Code uses ...' or 'cli.js uses ...' but does not cite a cli.js line number or versioned function name. See CLAUDE.md -> Commit message conventions."
|
|
WARN=1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [ "$WARN" -ne 0 ]; then
|
|
echo "Soft check raised warnings. Reviewer: please enforce per CLAUDE.md."
|
|
else
|
|
echo "Commit citation soft check clean."
|
|
fi
|