mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
First batch of pre-Phase-2 cleanup work. 5 GitHub issues closed in
one cohesive commit covering streaming-path correctness, IR
validator hardening, and CI path-trigger hygiene.
Changes (4 files, +302 / -5):
**Code fixes**
1. **#9 — Streaming empty-then-clean-exit headers** (server.mjs)
Pre-D35: when a provider's streaming spawn finished cleanly with
zero chunks (e.g. spec-degenerate stop with no content), the
response went out the SSE_DONE / res.end path without ever calling
writeHead. Result: client saw stream open + close with no headers,
no status code path applied. Now: zero-chunk branch guards
`!res.headersSent` and emits Content-Type + Cache-Control +
Connection + X-Accel-Buffering + all 5 X-OLP-* headers via
olpHeaders (provider attempted, cache miss) before writing the
terminator. Zero-chunk path correctly does NOT cache (cache write
remains gated on irChunk.type === 'stop').
2. **#10 — Streaming post-first-chunk error truncation marker**
(server.mjs, two sibling sites)
Pre-D35: if a provider yielded an error AFTER first content chunk
was emitted, the SSE stream was abandoned with raw socket close.
Client SDKs that wait for finish_reason hung. Now:
- Catch-block firstChunkEmitted=true path: emit synthetic
`{type:'stop', finish_reason:'length'}` via irChunkToOpenAISSE,
write SSE_DONE, end. Per ADR 0004 § Fallback safety: post-first-
chunk truncation surfaces as `length` finish, not a hang.
- Sibling fix in error-chunk path (provider yields `type:'error'`
chunk AFTER first content chunk): same recovery (marker + DONE +
end). Scope-creep acknowledged but identical semantic; clean to
fix together. Comments cross-reference D26 F19 and D35 #10.
3. **#11 — validateIRRequest irVersion strict check** (lib/ir/types.mjs)
ADR 0003 IR contract pins irVersion to '1.0'. Validator pre-D35
accepted ANY value (including no value, undefined, '2.0',
numeric 1.0). Now: `obj.irVersion !== undefined && obj.irVersion
!== '1.0'` → rejection. Strict string match. `undefined` still
accepted (pre-D35 IRs without the field remain valid — back-compat
with sites that haven't yet been migrated to emit it). Error
message uses JSON.stringify for safe rendering.
4. **#12 — alignment.yml scripts/** trigger removal**
(.github/workflows/alignment.yml)
Pre-D35 push.paths and pull_request.paths listed scripts/**. The
scripts/ directory does not currently exist (per AGENTS.md note:
scripts/migrate-from-ocp.mjs is planned for Phase 7). A path
filter referencing a non-existent directory has no effect on
trigger evaluation BUT misleads readers about the workflow's
intent. Removed from both push.paths and pull_request.paths. When
scripts/ lands in Phase 7, the trigger should be re-added at the
same time (see release_kit_overlay.bootstrap_quirk_policy).
**Verification — #4 (uniform X-OLP-Latency-Ms across error paths)**
#4 was found to already be correct via D32. Re-audit of all 7
in-handler sendError sites in handleChatCompletions confirmed all
attach a 5-header set via olpHeaders or olpErrorHeaders:
- 360-361 (415 wrong Content-Type) → olpErrorHeaders
- 368-369 (400 bad JSON) → olpErrorHeaders
- 378-379 (400 BadRequestError IR translation) → olpErrorHeaders
- 402-407 (503 no chain) → olpErrorHeaders
- 617-618 (503 provider disappeared) → olpErrorHeaders
- 760-761 (502 streaming pre-first-chunk error) → olpHeaders
- 778-779 (500 fallback engine error) → olpErrorHeaders
The 404 (line 922) and outer 500 (line 926) are router-level paths
without startMs context and correctly lack OLP headers. D35 adds
the #4-audit regression test pinning the 5-header invariant on the
503 no-provider response so future drift is caught immediately.
**Tests** (test-features.mjs): 416 → 424 (+8):
- #4-audit ×1 (5-header invariant on 503 no-provider sendError)
- #9 ×1 (zero-chunk streaming → 200 + Content-Type=text/event-stream
+ 5 X-OLP-* headers + [DONE])
- #10 ×1 (catch-throw after first chunk → marker + length finish + DONE)
- #10b ×1 (provider error chunk after first chunk → same recovery)
- #11a ×1 (irVersion undefined accepted)
- #11b ×1 (irVersion '1.0' accepted)
- #11c ×1 (irVersion '2.0' rejected)
- #11d ×1 (irVersion numeric 1.0 rejected)
Pre-commit fold-in (per evidence-first checkpoint #4):
- **D35 reviewer flagged JSDoc/validator drift on irVersion**
(Suggestion #1, non-blocking). The @property typedef at
lib/ir/types.mjs:43 said `{string} irVersion - always IR_VERSION`
but the validator at lines 185-186 accepts `undefined`. Future
reader who scans the @property alone sees contradiction without
the rationale comment 140 lines below. Folded: typedef marked
`[irVersion]` (optional) and description updated to "optional;
when present must equal IR_VERSION ('1.0'). Pre-D35 IRs lack
this field and remain valid."
Two other non-blocking reviewer suggestions not folded (out of
scope for D35; tracked as future polish):
- Distinct event names for the two streaming_error_after_first_chunk
log sites (provider-emitted string vs JS exception message).
- Phase 7 TODO: re-add scripts/** trigger to alignment.yml when
scripts/migrate-from-ocp.mjs lands.
Authority:
- ADR 0004 § Fallback safety — post-first-chunk truncation surfaces
as `length` finish (#10 + #10b)
- ADR 0003 § IR contract — irVersion pinned to '1.0' (#11)
- AGENTS.md § Implementation status — scripts/ planned for Phase 7
(#12)
- CLAUDE.md release_kit_overlay phase_rolling_mode — D35 lands
under "Unreleased" against Phase 2; no version bump
- CC 开发铁律 v1.6 § 10.x — independent fresh-context reviewer
required for code change
Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus,
independent of drafter): APPROVE_WITH_MINOR. Critical depth checks:
- Verified all 7 sendError sites in handleChatCompletions attach
5-header set (cited line numbers reconciled with current state)
- Verified writeHead block guarded by !res.headersSent; correctly
placed AFTER optional truncation marker, BEFORE SSE_DONE
- Verified irVersion validator strict-equality semantics across all
4 cases (undefined / '1.0' / '2.0' / numeric 1.0)
- Verified scripts/** removed from both push and pull_request paths
- Verified hygiene: 0 hits for personal markers, home paths, OAuth
tokens, internal IPs
- 424/424 tests pass independently in reviewer's run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
202 lines
8.3 KiB
YAML
202 lines
8.3 KiB
YAML
name: Alignment Guardrail
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'server.mjs'
|
|
- 'lib/**'
|
|
- 'models-registry.json'
|
|
- '.github/workflows/alignment.yml'
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'server.mjs'
|
|
- 'lib/**'
|
|
- 'models-registry.json'
|
|
- '.github/workflows/alignment.yml'
|
|
|
|
jobs:
|
|
blacklist:
|
|
name: source blacklist (hard fail)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Scan source for hallucinated tokens
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Known-hallucinated tokens. Extend only via an ALIGNMENT.md amendment PR.
|
|
# Each token is matched as a fixed string against the OLP source tree
|
|
# (excluding docs/, CHANGELOG, README, this workflow, and tests that
|
|
# may legitimately reference the historical token as a guardrail).
|
|
#
|
|
# Inherited transitively from OCP's 2026-04-11 drift:
|
|
# - api.anthropic.com/api/oauth/usage : fabricated Anthropic OAuth
|
|
# usage endpoint. Does not appear in any shipped @anthropic-ai/
|
|
# claude-code cli.js. Carried forward as a transitive guardrail.
|
|
#
|
|
# OLP-native entries: added as drift incidents accumulate.
|
|
BLACKLIST=(
|
|
"api.anthropic.com/api/oauth/usage"
|
|
)
|
|
|
|
# Source files in scope. Exclude docs, CHANGELOG, README, the
|
|
# workflow itself, and the test file (which may pin historical
|
|
# strings intentionally).
|
|
SOURCE_FILES="$(git ls-files \
|
|
| grep -E '\.(mjs|js|ts|json)$' \
|
|
| grep -v -E '^(docs/|CHANGELOG\.md|README\.md|test-features\.mjs|\.github/workflows/alignment\.yml)')"
|
|
|
|
FAIL=0
|
|
|
|
# 1. Blacklist scan
|
|
for token in "${BLACKLIST[@]}"; do
|
|
if echo "$SOURCE_FILES" | xargs grep -n -F "$token" 2>/dev/null; then
|
|
echo "::error::Blacklisted token '$token' detected in OLP source."
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
# 2. Excluded-provider scan: Google Antigravity is permanently
|
|
# excluded per ADR 0006 / ALIGNMENT.md Risk Tier A. Any reference
|
|
# to a `google-antigravity` plugin file or provider key in source
|
|
# (outside docs which may discuss the exclusion) is a finding.
|
|
FORBIDDEN_PROVIDER_TOKENS=(
|
|
"google-antigravity"
|
|
"antigravity"
|
|
)
|
|
for token in "${FORBIDDEN_PROVIDER_TOKENS[@]}"; do
|
|
HITS="$(echo "$SOURCE_FILES" | xargs grep -n -F "$token" 2>/dev/null || true)"
|
|
if [ -n "$HITS" ]; then
|
|
echo "::error::Tier-A-excluded provider token '$token' detected in OLP source. Per ALIGNMENT.md / ADR 0006, this provider is excluded by default; re-inclusion requires ADR 0006 supersession/amendment with new primary-source evidence."
|
|
echo "$HITS"
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
echo ""
|
|
echo "============================================================"
|
|
echo "ALIGNMENT GUARDRAIL FAILURE"
|
|
echo "============================================================"
|
|
echo "OLP source contains a token on the alignment blacklist or"
|
|
echo "references a Tier-A-excluded provider."
|
|
echo ""
|
|
echo "Blacklist tokens were introduced by LLM hallucinations and"
|
|
echo "do not appear in the relevant authority (provider CLI,"
|
|
echo "OpenAI spec, or ADR). See ALIGNMENT.md and (where the"
|
|
echo "token is inherited from OCP) the OCP 2026-04-11 drift"
|
|
echo "record at https://github.com/dtzp555-max/ocp."
|
|
echo ""
|
|
echo "Excluded providers are listed in ALIGNMENT.md \xc2\xa7 Risk Tier"
|
|
echo "Framework and ADR 0006. Tier-A exclusion means not"
|
|
echo "bundled, not pluggable, not added via opt-in;"
|
|
echo "re-inclusion requires ADR 0006 amendment with new"
|
|
echo "primary-source evidence."
|
|
echo ""
|
|
echo "Required action:"
|
|
echo " 1. Remove the token from source."
|
|
echo " 2. Cite the real authority (provider CLI doc / OpenAI"
|
|
echo " spec section / ADR) for the operation you intended."
|
|
echo " 3. See ALIGNMENT.md Rules 1, 2, and 5."
|
|
echo ""
|
|
echo "Do not add allowlist entries to this workflow without an"
|
|
echo "amendment PR to ALIGNMENT.md (see Amendment Procedure)."
|
|
echo "============================================================"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Blacklist + excluded-provider scan clean."
|
|
|
|
models-registry:
|
|
name: models-registry.json sanity (hard fail)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Validate models-registry.json
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ ! -f models-registry.json ]; then
|
|
echo "models-registry.json not found yet (bootstrap phase). Skipping."
|
|
exit 0
|
|
fi
|
|
|
|
# Basic JSON validity.
|
|
if ! node -e "JSON.parse(require('fs').readFileSync('models-registry.json','utf8'))"; then
|
|
echo "::error::models-registry.json is not valid JSON."
|
|
exit 1
|
|
fi
|
|
|
|
# Provider keys in models-registry.json must match the inventory
|
|
# in ALIGNMENT.md.
|
|
KNOWN_PROVIDERS='["anthropic","openai","mistral","grok","kimi","minimax","glm","qwen"]'
|
|
node -e "
|
|
const fs = require('fs');
|
|
const known = ${KNOWN_PROVIDERS};
|
|
const reg = JSON.parse(fs.readFileSync('models-registry.json','utf8'));
|
|
const providers = reg.providers || {};
|
|
const bad = Object.keys(providers).filter(p => !known.includes(p));
|
|
if (bad.length > 0) {
|
|
console.error('::error::Unknown provider key(s) in models-registry.json: ' + bad.join(', '));
|
|
console.error('Known providers per ALIGNMENT.md: ' + known.join(', '));
|
|
process.exit(1);
|
|
}
|
|
console.log('models-registry.json provider keys OK: ' + Object.keys(providers).join(', '));
|
|
"
|
|
|
|
commit-citation:
|
|
name: per-provider commit citation (soft check)
|
|
runs-on: ubuntu-latest
|
|
continue-on-error: true
|
|
steps:
|
|
- name: Checkout full history
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Scan PR commits for uncited assertions
|
|
if: github.event_name == 'pull_request'
|
|
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
|
|
|
|
# Use process substitution `< <(...)` rather than piping into the
|
|
# while loop. A piped while runs in a subshell, so `WARN=1` would
|
|
# never propagate back out to this scope — the if-check below would
|
|
# always report "clean" even when warnings were emitted. Classic
|
|
# Bash subshell trap; see commit history for the codex review that
|
|
# caught this.
|
|
WARN=0
|
|
while read -r sha; do
|
|
BODY="$(git log -1 --format=%B "$sha")"
|
|
if echo "$BODY" | grep -E -i -q '(provider|claude|codex|vibe|grok|kimi|minimax|glm|qwen|cli)[[:space:]]+(code[[:space:]]+)?uses'; then
|
|
if echo "$BODY" | grep -E -i -q '(cli[[:space:]]+v[0-9]+|https?://|ADR[[:space:]]+[0-9]{4})'; then
|
|
echo "OK $sha: assertion cited."
|
|
else
|
|
echo "::warning::Commit $sha asserts 'Provider X uses ...' or '<provider> CLI uses ...' but does not cite a CLI version, docs URL, or ADR number. See CLAUDE.md -> Commit message conventions."
|
|
WARN=1
|
|
fi
|
|
fi
|
|
done < <(git log --format="%H" "${BASE_SHA}..${HEAD_SHA}")
|
|
|
|
if [ "$WARN" -ne 0 ]; then
|
|
echo "Soft check raised warnings. Reviewer: please enforce per CLAUDE.md."
|
|
else
|
|
echo "Commit citation soft check clean."
|
|
fi
|