Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9a47c10
fix(e2e): restrict readiness polling to loopback destinations
cursoragent Aug 25, 2026
3f0c3ac
fix(e2e): resolve localhost and require loopback answers
cursoragent Aug 25, 2026
0ef296d
fix(strix): strip MODEL QUALITY WARNING boxes without fail-open
cursoragent Aug 25, 2026
9397bf4
fix(strix): keep same-box failure evidence when stripping the banner …
cursoragent Aug 25, 2026
c1ab957
fix(strix): match the banner heading anywhere on the line
cursoragent Aug 25, 2026
f02eb90
fix(strix): delete cosmetic-only banner boxes, keep same-box failures
cursoragent Aug 25, 2026
51291c3
fix(strix): align overlay to gpt-5.4 and keep same-box rate-limit evi…
cursoragent Aug 25, 2026
6b06a92
fix(strix): align gpt-5.4 contracts and expect rate-limit retries
cursoragent Aug 25, 2026
d025cc1
merge(main): make #1314 a descendant of 8fd471a3 for exact-head
cursoragent Aug 25, 2026
d4ea752
fix(strix): keep OpenCode dispatch-pool harness needles on gpt-5.6-luna
cursoragent Aug 25, 2026
006c523
merge(main): make #1314 a descendant of d2c554db after #1320
cursoragent Aug 25, 2026
58789e6
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
seonghobae Aug 25, 2026
7d3dddd
fix(strix): ignore known internal console warnings
seonghobae Aug 25, 2026
908f794
fix(strix): preserve malformed console failure markers
seonghobae Aug 25, 2026
9f267b8
fix(strix): invalidate stale classified console copies
seonghobae Aug 25, 2026
0f0adf8
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
seonghobae Aug 26, 2026
e294587
docs(copy): make web verification guidance actionable
seonghobae Aug 26, 2026
460c321
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
opencode-agent[bot] Aug 26, 2026
e13fdda
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
opencode-agent[bot] Aug 26, 2026
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
5 changes: 5 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ sequenceDiagram
invents a hypothesis, observed result, or verdict.
- Sandbox helpers copy the workspace, drop secret environment values unless
explicitly allowlisted by **name**, and run subprocesses with `shell=False`.
Web E2E readiness URLs are loopback-only; see
[`docs/doctoring/sandboxed-web-readiness-loopback-boundary.md`](docs/doctoring/sandboxed-web-readiness-loopback-boundary.md)
and [`docs/adr/0003-sandboxed-web-readiness-loopback-boundary.md`](docs/adr/0003-sandboxed-web-readiness-loopback-boundary.md).
- Logs and review receipts redact credential shapes (tokens, bearer values,
known provider prefixes). They do not mask operational PII that the
control plane must process.
Expand Down Expand Up @@ -173,3 +176,5 @@ resolver conflict.
— product-specific psychometric repair heartbeat and scientific gates.
- [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md)
— current increment's attestation decision and APA 7th citations.
- [`docs/doctoring/sandboxed-web-readiness-loopback-boundary.md`](docs/doctoring/sandboxed-web-readiness-loopback-boundary.md)
— loopback-only web E2E readiness polling and APA 7th citations.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Web verification now checks services through local readiness addresses only.
Start the backend and frontend on this computer and use their local health
URLs when running the check.
- Review results now separate cosmetic notices from blocking failures. Open the
failure details and correct the requested issue before running the check
again.
- Resolve Strix visibility from the trusted GitHub event for ordinary push,
schedule, and pull-request runs, reserving API retries for cross-repository
dispatches whose workflow token may not see the target repository.
Expand Down
9 changes: 9 additions & 0 deletions docs/adr/0003-sandboxed-web-readiness-loopback-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# ADR-0003: Sandboxed web readiness loopback boundary

- Status: accepted
- Date: 2026-08-25
- Scope: ContextualWisdomLab/.github control-plane E2E sandbox
- Decision: Poll `--backend-ready-url` and `--frontend-ready-url` only after the URL is proven to be HTTP(S) loopback. Accept literal `localhost` or a standard-library loopback address, resolve `localhost` and require every answer to be loopback, unwrap IPv4-mapped IPv6, reject userinfo and missing hosts, and keep redirects disabled.
- Ownership: `.github` owns the sandbox helper. Product repositories keep pointing readiness at their own loopback services.
- Figma File ID: N/A. This repository has no customer UI.
- Consequence: A review run cannot use the sandbox poller as an SSRF trampoline to metadata services or public hosts. Operators fix a rejected URL by pointing it at `127.0.0.1` or `::1`. Papers live in `docs/doctoring/sandboxed-web-readiness-loopback-boundary.md`.
85 changes: 85 additions & 0 deletions docs/doctoring/sandboxed-web-readiness-loopback-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Sandboxed web readiness loopback boundary

## Decision

`sandboxed_web_e2e.py` opens a readiness URL only after
`require_loopback_readiness_url` accepts it. The accepted destinations are
literal `localhost` (a trailing FQDN dot is stripped) or an address that
Python's standard-library `ipaddress` module classifies as loopback after
IPv4-mapped IPv6 addresses are unwrapped. Literal `localhost` is then
resolved; every A/AAAA answer must itself be loopback, so a poisoned hosts
file cannot smuggle a public address through the name allowlist. Redirects
remain disabled.

This supports the complete IPv4 loopback block, including `127.0.0.2`, and
IPv6 `::1`. It rejects `0.0.0.0`, `::`, public hosts, `.localhost`
subdomains, cloud-metadata link-local addresses, missing hosts, and
userinfo-confused URLs such as `http://user@127.0.0.1/`. A mapped public
address such as `::ffff:8.8.8.8` cannot pass merely because it is IPv6.

The boundary uses the standard library rather than a second address table.
It therefore follows the runtime's maintained special-purpose definitions and
keeps one fail-closed validation point before any network request. Do not add
individual non-loopback exceptions.

This successor lands the same buyer-facing repair as ContextualWisdomLab/.github#1244
on current `main` and keeps Strix classifier ownership out of the SSRF slice
(unlike ContextualWisdomLab/.github#1313).

## Operator next action

Point `--backend-ready-url` and `--frontend-ready-url` at the sandboxed
service on loopback. If readiness fails with `URL cannot target external
hostname`, replace the destination with `http://127.0.0.1:<port>/...` or
`http://[::1]:<port>/...` instead of opening the firewall or adding a
hostname exception.

## Verification

The regression exercises literal `localhost`, a trailing-dot `localhost.`,
`127.0.0.1`, another address in `127.0.0.0/8`, IPv6 `::1`, mapped loopback
`::ffff:127.0.0.1`, an unspecified address, a `.localhost` subdomain, a
public hostname, the common cloud metadata address, mapped public IPv6,
userinfo, a missing host, and poisoned localhost resolution (public A,
mapped public AAAA, empty answers, resolver errors, and non-IP answers).
The existing no-redirect test continues to prove that an allowed readiness
endpoint cannot redirect the poller across the boundary.

```mermaid
flowchart TD
Url["Readiness URL"]
Scheme{"http or https?"}
Userinfo{"userinfo present?"}
Host{"loopback IP, or localhost whose every resolved answer is loopback?"}
Open["Poll with redirects disabled"]
Reject["Fail closed before any request"]

Url --> Scheme
Scheme -->|"no"| Reject
Scheme -->|"yes"| Userinfo
Userinfo -->|"yes"| Reject
Userinfo -->|"no"| Host
Host -->|"no"| Reject
Host -->|"yes"| Open
```

## References

Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform Resource
Identifier (URI): Generic syntax* (RFC 3986). Internet Engineering Task
Force. https://doi.org/10.17487/RFC3986

Cotton, B., Vegoda, L., Bonica, R., & Haberman, B. (2013). *Special-purpose
IP address registries* (RFC 6890). Internet Engineering Task Force.
https://doi.org/10.17487/RFC6890

Internet Assigned Numbers Authority. (2026). *IANA IPv4 special-purpose
address registry*. Retrieved August 25, 2026, from
https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml

OWASP Foundation. (n.d.). *Server-side request forgery prevention cheat
sheet*. Retrieved August 25, 2026, from
https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html

Python Software Foundation. (2026). *ipaddress — IPv4/IPv6 manipulation
library*. https://docs.python.org/3/library/ipaddress.html
56 changes: 54 additions & 2 deletions scripts/ci/sandboxed_web_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
from __future__ import annotations

import argparse
import ipaddress
import json
import os
import signal
import shutil
import shlex
import socket
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Sequence
from dataclasses import dataclass
Expand Down Expand Up @@ -115,12 +118,61 @@ def start_service(label: str, command: str, cwd: Path, env: dict[str, str], logs
return Service(label=label, command=command, process=process, log_path=log_path)


def _require_loopback_ip_text(ip_text: str, hostname: str) -> None:
"""Reject a literal or resolved address that is not loopback."""
try:
address = ipaddress.ip_address(ip_text)
except ValueError as exc:
raise ValueError(f"URL cannot target external hostname: {hostname}") from exc
if address.version == 6 and address.ipv4_mapped is not None:
address = address.ipv4_mapped
if not address.is_loopback:
raise ValueError(f"URL cannot target external hostname: {hostname}")


def _require_resolved_loopback_hostname(hostname: str) -> None:
"""Resolve a literal localhost name and require every answer to be loopback."""
try:
results = socket.getaddrinfo(hostname, None)
except OSError as exc:
raise ValueError(f"URL cannot target unresolved hostname: {hostname}") from exc
if not results:
raise ValueError(f"URL cannot target unresolved hostname: {hostname}")
for result in results:
_require_loopback_ip_text(result[4][0], hostname)
Comment thread
seonghobae marked this conversation as resolved.


def require_loopback_readiness_url(url: str) -> None:
"""Reject a readiness URL that is not a local loopback HTTP(S) target.

Operators should point ``--backend-ready-url`` and ``--frontend-ready-url``
at the sandboxed service itself. Public hosts, cloud metadata addresses,
unspecified bind addresses, DNS names other than literal ``localhost``,
and userinfo-confused URLs are rejected before any request is opened.
Literal ``localhost`` is resolved and every answer must be loopback, so a
poisoned hosts file cannot smuggle a public A/AAAA record through the
name allowlist. IPv4-mapped IPv6 addresses are unwrapped and re-checked
so ``::ffff:8.8.8.8`` cannot bypass the loopback rule.
"""
parsed = urllib.parse.urlparse(url)
if parsed.scheme.lower() not in {"http", "https"}:
raise ValueError(f"URL must start with http:// or https://, got: {url}")
if parsed.username or parsed.password:
raise ValueError("URL cannot include userinfo")
hostname = (parsed.hostname or "").lower().rstrip(".")
if not hostname:
raise ValueError("URL must include a loopback hostname")
if hostname == "localhost":
_require_resolved_loopback_hostname(hostname)
return
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
_require_loopback_ip_text(hostname, hostname)
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.


def wait_for_url(url: str, timeout: int, service: Service) -> bool:
"""Poll a readiness URL until it responds or the service exits."""
if not url:
return True
if not (url.startswith("http://") or url.startswith("https://")):
raise ValueError(f"URL must start with http:// or https://, got: {url}")
require_loopback_readiness_url(url)
deadline = time.monotonic() + timeout
opener = urllib.request.build_opener(NoRedirectHandler())
while time.monotonic() < deadline:
Expand Down
Loading
Loading