Skip to content
9 changes: 9 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Hybrid Language: Development Plan

## Contributor validation — Actions artifact digests (#4241, 2026-11-13)

- `actions_fetch` validates an advertised `sha256:` digest before downloading and verifies the response before creating or replacing the archive. Missing/null digests retain legacy behavior. Four new tests cover matching/malformed/mismatching/legacy cases, including prior-file preservation.
- Refreshed from `b1627e83`; its unmodified `job.py` and `test_job_download.py` blobs match original report base `810888a8` exactly. The code/test patch is unchanged from the original issue, not a separate bounty contribution.
- Fresh local `test_job*.py` run on original candidate `afb050d1`: 99 passed, no skips. Original negative reproduction is recorded in #4241. Compiler, corpus, differential, live Actions downloads and hosted CI were not run by this contributor; draft PR remains pending those gates and maintainer review. No award or payment claimed.
- Follow-up on exact published PR #4441 head `8e685a035a1713b4c3b09ef39d8be6ba03b1227a` (2026-11-13, Linux/Python): all 557 top-level `tools/test_*.py` tests passed in 20.809s, plus 25 fuzz-harness and 29 runner tests, no skips (611 total). Conflict-marker, test-registration (40 files/36 patterns), guarantees, claims-link and corpus-census guards passed. Census checks 139 fixture declarations (45 accept/2 managed/92 reject); it does not execute the compiler corpus. This follow-up changes evidence only, not the tested source or tests.
- Hosted PR runs `43422148689`, `43422148636`, `43422148693` report `action_required`; the CI run has no jobs. No hosted test passed or failed is established by that status. Go/Rust toolchains are unavailable locally, so compiler workspace, executable corpus/differential and live Actions downloads remain unrun; keep the PR draft pending those gates and maintainer review.
- Later local follow-up on published head `2675e51ac1ded99c04ce56a13a7093ccb9ae55ad` (2026-11-13): installed official checksum-verified Go 1.26.9 / Rust 1.99.0 in an isolated environment. Version guard passes; compiler workspace 3293 passed/0 failed/ignored; corpus 139/139, managed tripwire not fired; managed gate 2/2 baseline blocks; differential FULL 47/47 build-and-run + 92/92 reject. Worktree clean after validation; no source/test changes. This supersedes the earlier local-toolchain limitation above. Hosted CI, live Actions downloads and maintainer review remain unverified; draft retained. Reproducible commands and evidence: `spikes/actions-artifact-digest/RESULTS.md`.

## Contributor validation — triage board target (2026-11-06)

- Base `973453c5`: an offline `cmd_board` reproduction overwrote an unrelated issue that quoted the board marker and ranked first in search. Posting now validates the configured `board_issue` (940 for this repository), or requires one trusted exact-title/marker candidate from a complete search. Missing, closed, invalid and ambiguous targets fail before a write. Twelve offline tests pass; baseline produced 15 failed subtest assertions and three passing controls. No live GitHub writes or compiler tests were run; hosted CI, review and any bounty award remain pending.
Expand Down
2 changes: 2 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# STATUS (for the human — 15 lines max)

- Contributor #4241 / draft PR #4441 (2026-11-13): digest verified before archive publication; prior-file preservation and missing/null compatibility retained. Local published-head validation: 611 tooling tests on `8e685a03`; pinned Go1.26.9/Rust1.99.0 on `2675e51a`: workspace3293/3293, corpus139/139, managed gate2/2, differential47/47 build+run +92/92 reject, version guard passes. Supersedes earlier missing-toolchain limitation. Hosted CI/live-download/review remain unverified; no award/payment claim. See spikes/actions-artifact-digest/RESULTS.md and PLAN.md.

- Contributor (2026-11-07): runtime Cargo builds now resolve relative `CARGO_TARGET_DIR` from the caller, matching staticlib discovery. Four CLI regressions pass (two failed before); real Cargo smoke passes all four path modes and fresh reuse. Pinned macOS validation on `4cda40a`: workspace 2945/2945, corpus 135/135, differential 45/45 + 90/90 and guards pass. Hosted CI/review pending; see PLAN.md.
- Contributor (2026-11-07): `hyb build` preserves unmarked/unreadable Go sidecars before compilation; marked outputs remain reusable, while legacy unmarked output requires explicit migration. Six CLI regressions, pinned macOS workspace 2947/2947, corpus 135/135 and differential 45/45 + 90/90 pass. Hosted CI/review pending; see PLAN.md.
- Contributor (2026-11-06): triage board updates validate the configured issue or one trusted discovery result; 12 offline regressions pass. Baseline overwrote a marker-quoting issue. No live writes or compiler tests; CI/review pending. See PLAN.md.
Expand Down
62 changes: 62 additions & 0 deletions spikes/actions-artifact-digest/RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Actions artifact digest verification — local evidence

Contribution: issue #4241 / draft PR #4441. Prepared with AI assistance.

## Scope

Validate the optional documented `digest` artifact metadata (`sha256:<64 hex>`)
before downloading and check the received bytes before creating/replacing the
archive. Missing/null metadata keeps legacy behavior. This is the original
contribution reported in #4241, not a second bounty claim.

## Local validation, 13 November 2026

Compiler validation used a clean isolated checkout of published commit
`2675e51ac1ded99c04ce56a13a7093ccb9ae55ad`, Linux x86_64, official Go 1.26.9
and Rust 1.99.0. `rustc --version`: `rustc 1.99.0 (b940084d7 2026-09-28)`;
`cargo --version`: `cargo 1.99.0 (5f94df478 2026-08-27)`;
`go version`: `go version go1.26.9 linux/amd64`.
Downloads were verified against Rust's version manifest and Go's release JSON
before installing locally. Build jobs were limited to two and serialized with
the repository's heavy-job lock. No repository source, tests or lockfiles changed.

| Command (from repository root unless stated) | Result |
| --- | --- |
| `bash tools/check-versions.sh` | CONSISTENT, exit 0 |
| `cd compiler && cargo test --workspace --locked --quiet` | 3293 passed, 0 failed/ignored/measured/filtered; exit 0; 116.488s |
| `cd compiler && cargo build -p hyb --locked --quiet` | exit 0 |
| `python3 tools/corpus_harness.py --bin compiler/target/debug/hyb` | verdict agreement 139/139; tripwire not fired; exit 0 |
| `python3 tools/managed_gate.py compiler/target/debug/hyb gates/managed-baseline.json tests/checker-corpus/02-shared-map-managed.hy tests/checker-corpus/18-pool-shared-struct-managed.hy` | PASS, 2 blocks / baseline 2; exit 0 |
| `python3 tools/differential_loop.py --bin compiler/target/debug/hyb` | FULL AGREEMENT: accept-side build+run 47/47, reject-side checker 92/92; exit 0; 35.815s |
| `git status --short` after the commands | empty |

The earlier published head `8e685a035a1713b4c3b09ef39d8be6ba03b1227a` passed
557 top-level tools tests, 25 fuzz-harness tests and 29 runner tests locally
(611 total, no skips), with registration/conflict/guarantees/claims/census guards.
`2675e51a` differs only in PLAN.md and STATUS.md; the tested code/tests are unchanged.
Original negative reproduction and focused 14-test regression results remain
recorded in #4241. No competitor's source was copied.

## Download provenance

Rust manifest: `https://static.rust-lang.org/dist/channel-rust-1.99.0.toml`.
Rust archives came from its `x86_64-unknown-linux-gnu` entries under the official
`https://static.rust-lang.org/dist/2026-10-01/` directory.
Go release metadata: `https://go.dev/dl/?mode=json&include=all`;
archive: `https://dl.google.com/go/go1.26.9.linux-amd64.tar.gz`.

| Archive | Verified SHA-256 |
| --- | --- |
| rustc-1.99.0-x86_64-unknown-linux-gnu.tar.xz | 77171ba2a0345fdf2abc4fedda55d6de078dae7a68527c28be8c77dcc9604bd5 |
| rust-std-1.99.0-x86_64-unknown-linux-gnu.tar.xz | 3e58dff2d0b72196b5ea4e90536e174d400de88564a52694686b81e091169933 |
| cargo-1.99.0-x86_64-unknown-linux-gnu.tar.xz | d7674918d28093097614cd9728b6ca60db9ea3038f640f0bd1e9a4188c7568ce |
| go1.26.9.linux-amd64.tar.gz | 42d158b4d8f7b61ac0a830567c940a86098fb7aac52e467a5ebec03ef5cc2f8d |

## Limits

These are local results, not a hosted CI pass. The earlier upstream PR workflows
reported `action_required`, with no jobs in the CI run; they were not bypassed
or rerun. No live Actions artifact download was exercised, and not every job in
the full CI workflow (for example the release integration sample) was run here.
Keep the PR draft pending the remaining review/CI gates. No merge, award,
reserved funds, payout setup or received payment is claimed.
9 changes: 9 additions & 0 deletions tools/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Exit codes: 0 = success, 1 = the fetch retrieved nothing (failed download,
no artifacts, or no summary.json), 2 = usage error.
"""
import hashlib
import json
import math
import os
Expand Down Expand Up @@ -365,6 +366,11 @@ def actions_fetch(run_id, jobdir):
if not isinstance(name, str) or not name or "/" in name or "\\" in name or name in (".", ".."):
print("actions fetch: refusing unsafe artifact name {!r}".format(name), file=sys.stderr)
return 1
digest = a.get("digest")
if digest is not None and (not isinstance(digest, str) or
re.fullmatch(r"sha256:[0-9a-fA-F]{64}", digest) is None):
print("actions fetch: unsupported artifact digest for {!r}".format(name), file=sys.stderr)
return 1
url = a["archive_download_url"]
dest = os.path.abspath(os.path.join(result_dir, name + ".zip"))
if os.path.dirname(dest) != result_dir:
Expand All @@ -378,6 +384,9 @@ def actions_fetch(run_id, jobdir):
# via a sibling temporary file so write/replace failures preserve it too.
with urllib.request.urlopen(req, timeout=120) as r:
data = r.read()
if digest is not None and hashlib.sha256(data).hexdigest() != digest[7:].lower():
print("actions fetch: digest mismatch for artifact {!r}".format(name), file=sys.stderr)
return 1
temporary = None
try:
with tempfile.NamedTemporaryFile(dir=os.path.dirname(dest),
Expand Down
77 changes: 77 additions & 0 deletions tools/test_job_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from email.message import Message
from http.server import BaseHTTPRequestHandler, HTTPServer
import http.client
import hashlib
import importlib.util
import io
import json
Expand Down Expand Up @@ -253,5 +254,81 @@ def test_success_creates_new_archive(self):
self.assertEqual([p.name for p in (root / "result").iterdir()], ["results.zip"])


class ArtifactDigestTests(unittest.TestCase):
"""GitHub's upload-artifact digest covers the downloaded archive bytes."""

def fetch(self, root, artifact, data):
response = io.BytesIO(data)
output, errors = io.StringIO(), io.StringIO()
with mock.patch.object(job, "gh_api", return_value={"artifacts": [artifact]}), \
mock.patch.object(job, "gh_token", return_value="offline-fixture"), \
mock.patch.object(job.urllib.request, "urlopen", return_value=response) as transport, \
contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
result = job.actions_fetch(123, str(root))
return result, output.getvalue(), errors.getvalue(), transport.call_count

def artifact(self, data):
return {"name": "results", "archive_download_url": "https://example.invalid/archive",
"size_in_bytes": len(data), "digest": "sha256:" + hashlib.sha256(data).hexdigest()}

def test_matching_digest_publishes_archive(self):
data = b"complete archive fixture"
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
result, output, errors, calls = self.fetch(root, self.artifact(data), data)
self.assertEqual(result, 0, errors)
self.assertEqual(calls, 1)
self.assertIn("artifact results", output)
self.assertEqual((root / "result/results.zip").read_bytes(), data)

def test_digest_mismatch_preserves_old_archive_or_absence(self):
original = b"complete archive fixture"
# One-byte mutation and successful-but-truncated response both finish read().
for bad in (b"Complete archive fixture", original[:-1]):
for previous in (None, b"previous validated archive"):
with self.subTest(bad=bad, previous=previous), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
result_dir = root / "result"
result_dir.mkdir()
dest = result_dir / "results.zip"
if previous is not None:
dest.write_bytes(previous)
result, output, errors, calls = self.fetch(root, self.artifact(original), bad)
self.assertEqual(result, 1)
self.assertIn("digest mismatch", errors)
self.assertNotIn("artifact results", output)
self.assertEqual(calls, 1)
self.assertEqual(dest.read_bytes() if dest.exists() else None, previous)
self.assertEqual(sorted(p.name for p in result_dir.iterdir()),
[] if previous is None else ["results.zip"])

def test_invalid_advertised_digest_fails_before_download(self):
for digest in ("", "sha256:abc", "sha512:" + "a" * 128,
"sha256:" + "z" * 64, 42, {"sha256": "a" * 64}):
with self.subTest(digest=digest), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact = self.artifact(b"fixture")
artifact["digest"] = digest
result, output, errors, calls = self.fetch(root, artifact, b"fixture")
self.assertEqual(result, 1)
self.assertIn("unsupported artifact digest", errors)
self.assertEqual(calls, 0)
self.assertNotIn("artifact results", output)
self.assertFalse((root / "result/results.zip").exists())

def test_legacy_missing_or_null_digest_still_downloads(self):
for null in (False, True):
with self.subTest(null=null), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact = self.artifact(b"legacy archive")
if null:
artifact["digest"] = None
else:
del artifact["digest"]
result, _, errors, _ = self.fetch(root, artifact, b"legacy archive")
self.assertEqual(result, 0, errors)
self.assertEqual((root / "result/results.zip").read_bytes(), b"legacy archive")


if __name__ == "__main__":
unittest.main()