Fix: verify against the pinned artifact, not HEAD -- ModelRuntime doe… #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # REQ-UPSTREAM-CONTRACT-TESTS — the pi upgrade gate. | |
| # | |
| # Asserts the assumptions pi-dispatch pins about pi, and fails the build when one stops holding. | |
| # CONST-PI-VERSION-PINNED makes an upgrade an explicit commit that edits a version string, so this | |
| # workflow is what turns that commit into a gate rather than a hope. | |
| # | |
| # Every assertion below covers a failure that is SILENT. A crash reports itself and needs no test; | |
| # these do not. Each maps to a point where a claim about pi was verified, believed, and later refuted: | |
| # - "appendSystemPrompt composes with file discovery" -> it REPLACES it, via a `??` | |
| # - "createAgentSession reloads the loader you pass" -> only one it built itself | |
| # - "-nc is a flag you remember" -> it is an object you forget to build | |
| # - "pi never throws" -> its own JSDoc says otherwise | |
| # Each was found by reading source, not by running code. These tests are what catch the next one. | |
| name: pi upgrade check | |
| on: | |
| push: | |
| paths: | |
| - "image/**" | |
| - "guardrails/**" | |
| - "package.json" | |
| - ".github/workflows/pi-upgrade-check.yml" | |
| pull_request: | |
| paths: | |
| - "image/**" | |
| - "guardrails/**" | |
| - "package.json" | |
| - ".github/workflows/pi-upgrade-check.yml" | |
| schedule: | |
| # Weekly: pi ships breaking changes between minors and its HEAD moved within 24h of this | |
| # project's design being written. A pin that is never exercised rots silently. | |
| - cron: "0 6 * * 1" | |
| workflow_dispatch: | |
| jobs: | |
| version-pin: | |
| name: pins are exact (CONST-PI-VERSION-PINNED) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: No floating ranges on pi packages | |
| run: | | |
| # A floating range turns a silent upstream minor into every queued job becoming a | |
| # no-op with no signal, because the queue still reports success. | |
| if grep -rnE '@earendil-works/pi-[a-z-]+@[\^~]|"@earendil-works/pi-[a-z-]+":[[:space:]]*"[\^~]' image/ package.json; then | |
| echo "::error::Floating range on a pi package. CONST-PI-VERSION-PINNED requires an exact pin." | |
| exit 1 | |
| fi | |
| echo "OK: pi pinned exactly" | |
| - name: Base image is pinned by digest | |
| run: | | |
| # Same reasoning, different vendor. A floating base tag is the identical silent break. | |
| if ! grep -qE '^FROM .*@sha256:[0-9a-f]{64}' image/Dockerfile; then | |
| echo "::error::Base image is not digest-pinned in image/Dockerfile." | |
| exit 1 | |
| fi | |
| echo "OK: base pinned by digest" | |
| contract-tests: | |
| name: pinned assumptions still hold (offline, no API key) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: ".nvmrc" # pi requires >=22.19.0; engine-strict enforces it | |
| - run: npm ci --no-audit --no-fund | |
| # The scariest assertions in this project are FREE: every trap lives at the resource-loader | |
| # boundary (pure) or at before_agent_start (which fires strictly before any provider HTTP | |
| # call). No API key, no tokens, no flake, no excuse not to run them on every build. | |
| # | |
| # PI_DISPATCH_REQUIRE_LOADER_TESTS=1 turns a skip into a hard failure. A skipped assertion is | |
| # an UNVERIFIED assertion, and "skipped = pass" is precisely the reasoning that lets a | |
| # guardrail-less agent ship green. | |
| - name: Guardrails present, hostile AGENTS.md absent, exit codes correct | |
| env: | |
| PI_DISPATCH_REQUIRE_LOADER_TESTS: "1" | |
| run: npm test | |
| image: | |
| name: the job image holds its contract | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build | |
| run: docker build -f image/Dockerfile -t pi-job:ci . | |
| # --- INT-SDK-SESSION-OPTIONS --- | |
| # `pi --mode print` does not exist: --mode accepts text|json|rpc, and --print/-p is a separate | |
| # boolean. --help rather than a real prompt, because a real prompt needs a paid API key and a | |
| # contract test that costs money is a contract test that gets disabled. | |
| # --entrypoint is required: ENTRYPOINT is the runner and ignores CMD, so `docker run img pi ...` | |
| # would silently run the runner instead of pi and the assertion would test nothing. | |
| - name: pi -p is still a flag | |
| run: docker run --rm --entrypoint pi pi-job:ci -p --help >/dev/null | |
| # --- CONST-ISOLATION-CONTAINER-PER-JOB --- | |
| # --cap-drop=ALL is the enforcement surface. Read the effective capability set directly rather | |
| # than install libcap just to ask. | |
| - name: No capabilities under --cap-drop=ALL | |
| run: | | |
| caps=$(docker run --rm --cap-drop=ALL --security-opt no-new-privileges \ | |
| --entrypoint sh pi-job:ci -c 'grep ^CapEff /proc/self/status' | awk '{print $2}') | |
| [ "$caps" = "0000000000000000" ] || { echo "::error::Container retains capabilities: $caps"; exit 1; } | |
| echo "OK: CapEff=$caps" | |
| - name: Runs as a non-root user | |
| run: | | |
| uid=$(docker run --rm --entrypoint id pi-job:ci -u) | |
| [ "$uid" != "0" ] || { echo "::error::Job container runs as root."; exit 1; } | |
| # --- INT-CONTAINER-JOB-INPUTS --- | |
| # /job:ro is what makes CONST-ISSUE-TEXT-IS-DATA enforceable by filesystem permission rather | |
| # than by asking nicely. It is a security boundary, so assert the kernel enforces it. | |
| - name: /job is genuinely read-only from inside | |
| run: | | |
| mkdir -p fixture/pi | |
| echo "x" > fixture/pi/APPEND_SYSTEM.md | |
| if docker run --rm --cap-drop=ALL -v "$PWD/fixture:/job:ro" --entrypoint sh pi-job:ci \ | |
| -c 'echo pwned > /job/pi/APPEND_SYSTEM.md' 2>/dev/null; then | |
| echo "::error::/job is writable. The agent can rewrite its own instructions." | |
| exit 1 | |
| fi | |
| echo "OK: /job:ro enforced" | |
| # --- INT-CONTAINER-RUNTIME-CONTRACT --- | |
| # pi lazily creates ~/.pi/agent and writes auth.json on the FIRST credential operation. If the | |
| # dir is root-owned the job dies EACCES at runtime, on a path nothing in the Dockerfile hints | |
| # at. COPY --chown does not fix it (it skips auto-created parents), so assert the real thing. | |
| - name: The agent dir is writable by the runtime user | |
| run: docker run --rm --entrypoint sh pi-job:ci -c 'touch "$HOME/.pi/agent/auth.json" && rm "$HOME/.pi/agent/auth.json"' | |
| - name: The guardrails are baked where the runner reads them | |
| run: | | |
| docker run --rm --entrypoint grep pi-job:ci -q "pi-dispatch-guardrails-v1" /opt/pi-dispatch/HARD_RULES.md \ | |
| || { echo "::error::Guardrails sentinel missing from /opt/pi-dispatch/HARD_RULES.md"; exit 1; } | |
| # --- DES-PLAYWRIGHT-CLI-NOT-CHROME-DEVTOOLS / REQ-FRONTEND-VISUAL-VERIFY --- | |
| # Root-installed Chromium lands in /root/.cache/ms-playwright, invisible to the non-root user | |
| # CONST-ISOLATION-CONTAINER-PER-JOB requires. --shm-size, NOT --ipc=host: Playwright recommends | |
| # the latter, but it shares the HOST IPC namespace with a container running adversarial-input | |
| # agent code. The crash it prevents is caused by a 64MB /dev/shm, so fix that instead. | |
| - name: Chromium screenshots a local page as non-root | |
| run: | | |
| echo '<html><body><h1>pi-dispatch</h1></body></html>' > fixture/page.html | |
| docker run --rm --init --cap-drop=ALL --security-opt no-new-privileges --shm-size=1g \ | |
| -v "$PWD/fixture:/fixture:ro" --entrypoint sh pi-job:ci \ | |
| -c 'playwright-cli screenshot --browser-arg=--no-sandbox file:///fixture/page.html /tmp/x.png && test -s /tmp/x.png' \ | |
| || { echo "::error::Chromium unusable as non-root — check PLAYWRIGHT_BROWSERS_PATH at build AND run"; exit 1; } | |
| # Fonts absent => tofu boxes => screenshots that look fine and contain no legible text. That | |
| # silently guts the requirement full Chromium is in this image for. | |
| - name: Fonts are installed (or screenshots are tofu) | |
| run: | | |
| n=$(docker run --rm --entrypoint sh pi-job:ci -c 'fc-list | wc -l') | |
| [ "$n" -gt 0 ] || { echo "::error::No fonts. Chromium will render boxes and REQ-FRONTEND-VISUAL-VERIFY is a lie."; exit 1; } | |
| echo "OK: $n fonts" |