Files
Linux_post_install/AgentsReport/reviewer/2026-09-04_hf-downloader-review.md
T
he 6a6c323a89
gates / consistency-and-conventions (push) Successful in 1m35s
ai need continue
2026-09-04 13:58:38 -04:00

20 KiB

Reviewer Report — pos ai hf (Hugging Face Model Downloader)

Date: 2026-09-04 Status: CHANGES_REQUIRED


TL;DR

Verdict: CHANGES_REQUIRED

Reviewed: bin/pos-ai-hf (470 lines), DOC/POS.md updates, generated docs/completions, AGENT_TODO entry.

1 BLOCKING findingtotal_size accumulated inside a pipe subshell is always 0, so multi-file download summaries show incorrect total size. 2 REQUIRED findings — missing disk space pre-flight check (architect-specified) and missing hf_repo_files API fallback (architect-specified). 2 SUGGESTED findings — rate-limit HEAD request inefficiency and error message format deviation.

Severity Count
BLOCKING 1
REQUIRED 2
SUGGESTED 2
NOTE 3

Checklist Results

Code Quality

Item Status Evidence
set -euo pipefail present [PASS] Line 2: set -euo pipefail
# POS: header correct format with em-dash [PASS] Line 3: # POS: ai hf — Download AI models from Hugging Face (search, download, manage) — em-dash confirmed
# POS_FLAGS: correct [PASS] Line 4: # POS_FLAGS: --branch --gguf --output — matches actual flag parsing (lines 88-109)
# POS_DEPS: correct [PASS] Line 5: # POS_DEPS: curl jq — matches deps guards on lines 17-18
# POS_CONFIG: correct format [PASS] Line 6: # POS_CONFIG: ai | ai.env | HF_TOKEN=secret:… | HF_DOWNLOAD_DIR=:… — uses secret: prefix convention matching other tools (pos-docker-compose, pos-communication-matrix-sender, pos-communication-telegram-sender)
# POS_EXAMPLES: present [PASS] Lines 7-12: 6 example lines covering search, download (repo, gguf, single file), list, remove
Sources lib/common.sh via standard fallback [PASS] Line 14: source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" — exact template pattern
Deps guards BEFORE -h|--help [PASS] Lines 17-18 (deps) before line 90 (-h|--help case)
usage() present and comprehensive [PASS] Lines 44-79: all 4 subcommands, download options, examples, config keys, exit codes documented
All 4 subcommands implemented [PASS] cmd_search (line 279), cmd_download (line 303), cmd_list (line 402), cmd_remove (line 438); dispatch at line 464
Config loader reads ai.env with env-var precedence [PASS] load_hf_config() (lines 25-39): reads CONFIG_FILE, env-already-set wins (if [ -z "${!k:-}" ]), strips quotes, CR, comments
Auth header: Authorization: Bearer $HF_TOKEN [PASS] hf_auth_header() (lines 128-132): printf 'Authorization: Bearer %s' "$HF_TOKEN"
Rate limit handling: 429 → sleep + retry [PASS] Lines 150-167: loop with attempt < 2, on 429 extracts Retry-After or defaults 60, sleeps, retries once
Resume: curl -C - [PASS] Line 259: curl_args=(-L -C - --progress-bar -o "$target")
.hf-meta metadata tracking [PASS] Lines 368-382: writes JSON with repo_id, branch, timestamp, files array
Output matches contract (emojis, paths, sizes) [PASS] Lines 392-398: 📥 and 📁 emojis, repo-id, size, path format matches Architect Decision 6
Error handling: 404 [PASS] Line 175: err "Model not found: ${endpoint#/api/models/}"
Error handling: 401/403 [PASS] Line 174: err "Authentication failed — check HF_TOKEN (pos config ai)"
Error handling: 429 [PASS] Line 176: err "Rate limit exceeded — try again later"
Error handling: timeout [PASS] Line 153: err "Connection timed out — check network"
Error handling: jq parse [PASS] Line 182: err "Failed to parse API response — check network or HF status"
Error handling: no token [PASS] Line 121: warn "No HF_TOKEN set — using anonymous access" — continues for public repos per spec
All file paths seam-guarded [PASS] CONFIG_FILE="${CONFIG_FILE:-$HOME/…}" (line 21), HF_TOKEN="${HF_TOKEN:-}" (line 22), HF_DOWNLOAD_DIR="${HF_DOWNLOAD_DIR:-$HOME/…}" (line 23), re-guarded at line 117 after --output override
No err "msg" 1 pattern [PASS] All 19 err calls use err "message" with no trailing exit code — confirmed by grep

Convention Compliance

Item Status Evidence
bash -n passes [UNVERIFIED] Cannot execute bash -n due to sandbox restrictions. Builder claims pass.
make gen && make check passes [UNVERIFIED] Cannot execute make. Builder claims pass. Generated files (AGENT_Context, completions) contain correct entries.
make lint passes (0 FAIL, 0 WARN) [UNVERIFIED] Cannot execute make. Builder claims 0 FAIL, 0 WARN.
Tool is executable (chmod 100755) [UNVERIFIED] Cannot check permissions. git ls-files shows file is tracked. Builder claims chmod 100755.
POS.md has ai hf row + detail block [PASS] POS.md line 58: bin/pos-ai-hf in **File:** line. Lines 103-112: command table + auth/rate-limit/resume detail block.
No INTERACTIVE_CMDS change needed [PASS] bin/pos line 269: pos-ai-hf NOT in INTERACTIVE_CMDS string — tool does not read stdin.
No changes to bin/pos-ai [PASS] Grep for "hf" in bin/pos-ai returns 0 matches.
No changes to bin/pos [PASS] pos-ai-hf not in INTERACTIVE_CMDS. No other modifications visible.
No changes to preinstall.sh [PASS] Not in git diff.
No changes to lib/common.sh [PASS] Not in git diff.
Generated docs contain ai-hf [PASS] AGENT_Context: tree line 66, dispatch line 283, filetable line 613. Completions line 6: _pos_flags[ai-hf]="--branch --gguf --output"
AGENT_TODO Done entry added [PASS] Git diff shows new entry at top of Done section, dated 2026-09-04.

Security

Item Status Evidence
Token never printed in output [PASS] $HF_TOKEN referenced only at lines 22, 120, 129-130, 138, 257 — none in any printf/echo output path. Token warning (line 121) only prints the literal string "No HF_TOKEN set".
Token passed via header, not URL [PASS] Lines 128-132: hf_auth_header() constructs Authorization: Bearer … header. Lines 144-146, 260-262: added as -H arg to curl. Never in URL string.
Config file permissions (ai.env chmod 600) [UNVERIFIED] Tool reads from ~/.config/linux_post_install/ai.env. The chmod is set by postinstall.sh (not by this tool). The tool does NOT change permissions — correct behavior.
No command injection via repo-id [PASS] repo_id is user input. Used in: API endpoint construction (line 198-199, passed as curl URL arg — safe), hf_repo_dir() (line 214: string substitution ${repo_id//\//-} — safe), jq filter argument (line 322: --arg fn "$filename" — safe), .hf-meta heredoc (line 375-382: unquoted heredoc — variables expanded but context is JSON file, not shell execution). No eval, no exec with user-controlled path.
No command injection via filenames [PASS] Filenames from API response are parsed by jq -r and used in path construction ($target="${target_dir}/${fname}"). Passed to mkdir -p and curl -o — no shell interpretation of the filename value itself.

Step 1: Subshell Variable Loss in Multi-File Downloads

[FAIL — BLOCKING]

Finding: In cmd_download, the multi-file download loop at lines 345-366 runs inside a pipe (printf … | jq … | while IFS= read -r file_json; do … done). In bash, a pipe creates a subshell, so variables modified inside the while loop — specifically $total_size (line 349) and $downloaded (line 355) — are lost when the pipe exits. The summary section at line 396 reads $total_size which is still 0 from its initialization at line 340.

Severity: BLOCKING Certainty: FACT — provable from bash subshell semantics. cmd | while read; do var=...; done runs the while body in a subshell. Variables set inside do not propagate back. Evidence:

  • Line 340: local total_size=0
  • Line 345: printf … | jq … | while IFS= read -r file_json; do — pipe creates subshell
  • Line 349: total_size=$((total_size + fsize)) — modified inside subshell, lost
  • Line 396: total_human="$(hf_human_size "$total_size")" — reads 0 Relevant files/lines: bin/pos-ai-hf:340-398 Approved scope reference: Architect Decision 6 (Output Contract) specifies multi-file summary as "📥 Downloaded: meta-llama/Llama-3.1-8B-Instruct (7 files, 4.7 GB)" — the size should be the correct total. Why it matters: Every multi-file download (the common case for large models) will print "0 B" as the total size. This is a user-visible incorrect output and directly violates the Architect's output contract. Test mask: The test harness (Test 22, line 151) checks for [0-9] B) which matches "0 B)" — the test passes on the bug. The test needs to check the actual expected sum (618 + 8500000000 + 9000000 + 5000 + 200 + 500 = 8509017318 bytes ≈ "8.5 GB"). Suggested fix: Replace the pipe with process substitution (while IFS= read -r file_json; do … done < <(printf '%s' "$filtered_files" | jq -c '.[]')) to keep the loop in the main shell, or accumulate total_size via a temp file or another jq pass on $filtered_files before the loop.

Step 2: Missing Disk Space Pre-Flight Check

[FAIL — REQUIRED]

Finding: The architecture (Decision 5, "Key implementation details" table, and Decision 7, "Error matrix" row) specifies a pre-flight disk space check: df available space vs estimated total (from /tree/ endpoint), with a warning when space is critically low. The implementation has no disk space check at all.

Severity: REQUIRED Certainty: FACT — no df or stat-based space check anywhere in the file (confirmed by grep). Relevant files/lines: bin/pos-ai-hf — absent between line 333 (file count check) and line 337 (mkdir). Approved scope reference: Architect Decision 5: "Disk space | Pre-flight check: df available space vs estimated total (from /tree/ endpoint)" and Decision 7: "Disk space | df pre-flight | warn "Low disk space: need {N} GB, only {M} GB available" then continue (user's call)". Why it matters: Downloading a 7-8 GB model on a near-full disk is a waste of time and leaves partial files. The architecture explicitly chose a non-blocking warning (not an error) — the user makes the final call. Without this, users discover the problem only after curl fails mid-file. Suggested fix: Before the download loop, sum the sizes from $filtered_files (this also solves the subshell bug if using jq for the sum), compare with df --output=avail "$target_dir", and warn if insufficient. This is a ~5 line addition.


Step 3: Missing hf_repo_files API Fallback

[FAIL — REQUIRED]

Finding: The architecture specifies hf_repo_files() should call /api/models/{ns}/{repo}/tree/{branch}/ for file sizes, and fall back to /api/models/{ns}/{repo} for file list if the tree endpoint fails. The implementation only calls the tree endpoint (line 198) with no fallback.

Severity: REQUIRED Certainty: FACT — line 198: local endpoint="/models/${ns}/${repo}/tree/${branch}" followed by a single hf_api "$endpoint" call. No fallback logic. Relevant files/lines: bin/pos-ai-hf:188-200 Approved scope reference: Architect Decision 5 (hf_repo_files() signature): "Calls: GET /api/models/{ns}/{repo}/tree/{branch}/ for sizes, falls back to /api/models/{ns}/{repo} for file list" Why it matters: Some HF repositories (e.g., datasets, some model repos) may not respond to the /tree/ endpoint (404 or empty). The fallback to /api/models/{ns}/{repo} provides a file list (without sizes) so the user can still download. Without it, those repos fail entirely with a 404 error. Suggested fix: Wrap the tree call in a conditional; on 404, call /api/models/{ns}/{repo} and construct a minimal [{"rfilename": "<name>", "size": 0}] array from the siblings array. size being 0 is acceptable (displays as "0 B") since the primary goal is getting the download list.


Step 4: Rate-Limit Extra HEAD Request

[NOTE — SUGGESTED]

Finding: Line 158 makes a second curl -sI HEAD request specifically to extract the Retry-After header value after receiving a 429. This is an extra HTTP call that could itself be rate-limited, and the Retry-After header was already present in the original request's response (line 151 uses -w '%{http_code}' but does not capture response headers).

Severity: SUGGESTED Certainty: FACT — line 158: retry_after="$(curl -sI -H "${auth_header:-}" "$url" 2>/dev/null | grep -i 'retry-after:' | tr -d '\r' | awk '{print $2}')" Relevant files/lines: bin/pos-ai-hf:156-161 Approved scope reference: Architect Decision 5: "Rate limiting | Sleep 1s between files; on 429, wait Retry-After header value or 60s default" Why it matters: Minor inefficiency. When already rate-limited, making another request is suboptimal. Could use curl -sS -D - (dump headers to stdout) in the original request to capture Retry-After directly, or simply default to 60s without the extra call. Suggested fix: Change the original curl in hf_api() to use -D - (or a header dump file) so the Retry-After header is available from the first response without a second call.


Step 5: Error Message Format Deviation

[NOTE — SUGGESTED]

Finding: The error message for invalid repo format at line 195 (hf_repo_files) says "Invalid repo format: use namespace/model-name" while the Architect specified "Invalid repo format: use namespace/model-name" at Decision 7. However, line 308 (cmd_download) also says the same message. The Architect's error matrix entry says err "Invalid repo format: use namespace/model-name" — which matches. This is consistent.

However, the Architect's error matrix says the model-not-found message should reference the full repo-id (e.g., err "Model not found: {repo-id}"), while the implementation at line 175 constructs the message from the API endpoint: err "Model not found: ${endpoint#/api/models/}". The stripped endpoint value is the same as repo-id when the endpoint is /models/{ns}/{repo}, but if the endpoint is /models/{ns}/{repo}/tree/{branch}, the stripped value would be {ns}/{repo}/tree/{branch} — which is confusing.

Severity: SUGGESTED Certainty: HYPOTHESIS — only manifests when 404 is returned from /tree/{branch} endpoint (which strips to {ns}/{repo}/tree/{branch} in the message). The normal /models/{ns}/{repo} path produces the correct repo-id in the message. Relevant files/lines: bin/pos-ai-hf:175 Why it matters: Minor UX: a 404 from the tree endpoint would show a confusing path in the error message instead of the clean repo-id. The Architect's spec just says {repo-id}. Suggested fix: Capture the repo_id and pass it to hf_api or handle the error at the caller level where repo_id is available. Or, in hf_api, accept an optional display-name parameter for error messages.


Architect Compliance

Decision Implemented? Notes
Decision 1: File location bin/pos-ai-hf [YES] Created at correct path
Decision 2: Subcommands (download, search, list, remove) [YES] All 4 implemented
Decision 3: Config scope extends ai [YES] # POS_CONFIG: ai | ai.env — correct
Decision 4: Download directory layout [YES] <namespace>-<model-name>/ under XDG data dir, .hf-meta metadata
Decision 5: Download logic [PARTIAL] Download flow correct; missing disk space check; missing API fallback
Decision 6: Output contract [PARTIAL] Emojis, paths correct; multi-file size is always 0 (subshell bug)
Decision 7: Error handling [PARTIAL] All error matrix cases handled; missing disk space pre-flight
Decision 8: Deps/lint compliance [YES] POS headers correct, deps before help, source chain
Decision 9: Testing strategy [YES] Stub-PATH harness exists at expected location with 46 tests

Deviations from Architect design:

  1. Disk space pre-flight check not implemented (architect-specified, REQUIRED).
  2. hf_repo_files API fallback not implemented (architect-specified, REQUIRED).
  3. Rate-limit handling uses extra HEAD request instead of extracting from original response (architect did not specify implementation detail — minor deviation).
  4. Config loader is an inline pattern rather than copying from bin/pos-ai (functionally equivalent, not a deviation in behavior).

Verification Verified

Claim Evidence Status
Builder: "46/46 tests green" Test harness exists at /tmp/opencode/hf-test/run-tests.sh with 45 numbered tests visible (tests 1-45). Could not execute to confirm count. UNVERIFIED
Builder: "make gen && make check OK" Generated files (AGENT_Context line 66/283/613, completions line 6) contain correct ai-hf entries. STRONG INFERENCE
Builder: "make lint 0 FAIL, 0 WARN" All conventions verified by static analysis (POS headers, deps guards, source chain, exec bits claimed). UNVERIFIED
Builder: "bash -n OK" Cannot execute. No syntax errors visible by manual inspection. UNVERIFIED
Builder: "POS.md updated" Git diff confirms: ai File line updated (line 58), command table + detail block added (lines 103-112). FACT
Builder: "No changes to bin/pos, bin/pos-ai, preinstall.sh, lib/common.sh" git ls-files confirms these files tracked; grep confirms no hf-related changes. FACT

Verification Unverified

Claim Reason
bash -n passes Sandbox prevents execution
make gen && make check passes Sandbox prevents execution
make lint 0 FAIL 0 WARN Sandbox prevents execution
Tool is executable (100755) Sandbox prevents ls -la
Test harness 46/46 passes Sandbox prevents execution
Config file ai.env chmod 600 Controlled by postinstall.sh, not this tool

Scope Compliance

In-scope (confirmed):

  • bin/pos-ai-hf created with all 4 subcommands
  • DOC/POS.md ai hf row + detail block
  • Generated docs/completions updated via make gen
  • AGENT_TODO.md Done entry

Out-of-scope (confirmed NOT present):

  • No ollama integration
  • No parallel downloads
  • No changes to bin/pos-ai, bin/pos, preinstall.sh, lib/common.sh
  • No new config scope (extends ai)
  • No INTERACTIVE_CMDS change

Unexpected changes: None detected.


Remaining Uncertainty

  1. Whether bash -n, make gen, make check, and make lint actually pass — Builder claims they do, and generated artifacts are consistent with this, but execution was blocked.
  2. Whether the 46 test cases actually all pass — the test harness exists with the correct structure, but Test 22 masks the subshell bug (checks for [0-9] B) which matches "0 B)").
  3. Whether the tool is truly chmod 100755 — Builder claims it is, file is in git index.
  4. Live network behavior against real huggingface.co — only stub-tested, not end-to-end verified.

Builder

Reason: The 1 BLOCKING finding (subshell variable loss) and 2 REQUIRED findings (disk space check, API fallback) are all within the approved scope and well-understood fixes. The Builder should:

  1. Fix the subshell bug (replace pipe with process substitution or pre-compute sum via jq)
  2. Add the disk space pre-flight check (~5 lines, df + warn)
  3. Add the hf_repo_files fallback to /api/models/{ns}/{repo} on 404
  4. Update Test 22 to check actual expected size value instead of regex [0-9] B)
  5. Re-run bash -n, tests, and gates

Changes Made by Reviewer

None — read-only review.