# Wrapper hardening fold-in: #559 (eval removal) + #560 (host-derived login) **Branch:** `fix/wrapper-hardening-tls-credpath-cicwait` (PR #551) **Worker:** coderlite0 (Sonnet lane) · coordinated by mos-claude **Date:** 2026-06-20 **Scope:** `packages/mosaic/framework/tools/git/*.sh` only ## What the issues asked for vs. what was already landed Both issues were largely satisfied by prior merged work; this fold-in closes the remaining gaps (regression tests + a loud diagnostic + one residual word-split site) rather than re-implementing finished functionality. ### #559 — remove `eval` from issue-create.sh (and siblings) - `eval`-based command construction was already removed across the wrapper surface (landed in #549). A full scan of `tools/git/*.sh` finds **zero** `eval` usages. - `issue-create.sh`, `pr-create.sh`, `issue-edit.sh`, `issue-assign.sh` already build their `tea`/`gh` invocations as argv arrays (`CMD=(...)`, `"${CMD[@]}"`), so Markdown bodies pass through verbatim. - **Residual found & fixed:** `issue-comment.sh` still used unquoted `$(get_gitea_repo_args)` word-splitting (the comment body itself was already safely quoted, so no injection bug — but it was the inconsistent, fragile pattern #559 targets, and it failed silently when no login resolved). Converted to an argv array with an explicit, loud login-resolution error. - **Added regression test:** `test-issue-create-body-safety.sh` — feeds a hostile Markdown body (`$(touch SENTINEL)`, backticks, single/double quotes, `$HOME`/`${PATH}`, pipes/`&&`/`;`) through `issue-create.sh` and asserts (1) no command substitution executes (sentinel file never created) and (2) the `--description` `tea` receives is byte-for-byte the original body. ### #560 — auto-detect Gitea `--login` from repo origin host - Centralized host→login resolution already exists in `detect-platform.sh` (`get_gitea_login_for_host` → `find_tea_login_for_host`, matching `urlparse(url).hostname`). Every wrapper routes through it (or `get_gitea_login` / `get_gitea_login_for_repo_override`); **no wrapper hardcodes `${GITEA_LOGIN:-mosaicstack}`**. Explicit `GITEA_LOGIN` wins only when it matches the host (`tea_login_matches_host`), so stale overrides are rejected. - **Gap fixed — silent failure → loud diagnostic:** the failure path of `get_gitea_login_for_host` returned non-zero with no message. Added `print_gitea_login_diagnostic`, emitted to **stderr** on resolution failure: names the unresolved host, lists available tea logins (name + host), and gives the `GITEA_LOGIN` override + `tea login add` fix. Stderr-only, so it never contaminates stdout (the resolved login name) or the log-grep assertions in the existing harnesses. Callers with an API fallback (pr-merge, issue-close, pr-create, issue-create) still follow with their own "using API fallback" line, giving a clear "no login → fallback" trail. - **Extended test:** `test-gitea-login-resolution.sh` now also asserts (a) the loud diagnostic fires and lists available logins for an unresolved host, (b) login is derived from origin host for **both** instances (mosaicstack + usc) via a scoped second `tea` mock, and (c) a valid `GITEA_LOGIN` override is honored. The scoped mock keeps the existing API-fallback assertions (which require mosaicstack to have _no_ tea login) valid. ## Files changed (wrapper surface only) - `detect-platform.sh` — add `print_gitea_login_diagnostic`; call it on the `get_gitea_login_for_host` failure path. - `issue-comment.sh` — argv array + loud login-resolution error (was unquoted `$(get_gitea_repo_args)`). - `test-issue-create-body-safety.sh` — **new** (#559 regression). - `test-gitea-login-resolution.sh` — extended (#560 diagnostic + both-host + override). ## Verification All wrapper harnesses pass locally: - `test-issue-create-body-safety.sh` — PASS - `test-gitea-login-resolution.sh` — PASS - `test-pr-merge-gitea-empty-uid.sh` — PASS - `test-pr-metadata-gitea.sh` — PASS - `test-lane-brief-pr-linkage.sh` — PASS ## Open items flagged to mos-claude (orchestrator decisions) 1. **CHANGELOG absent.** The task said "update CHANGELOG (append-only), keep the existing #550/#551 entry." No CHANGELOG file exists anywhere in the repo, and #550/#551 are not recorded in one. **ASSUMPTION:** documenting #559/#560 in this scratchpad + the PR description (`Closes #559 Closes #560`) follows the repo's actual convention (`docs/scratchpads/`). Did not invent a new CHANGELOG structure. 2. **`docs/TASKS.md` is orchestrator single-writer.** It carries a "Workers read but never modify" banner. As a worker I did **not** edit it; task tracking is via the linked Gitea issues #559/#560 + this scratchpad. Orchestrator may add a rollup row if desired. 3. **Wrapper `test-*.sh` are not CI-wired.** `.woodpecker/ci.yml` runs `pnpm typecheck/lint/format:check/test` (`turbo run test`); the framework dir has no `package.json`, so these shell harnesses run **locally/manually only** — they do not gate the PR in Woodpecker. **ASSUMPTION:** out of scope to wire a shell-test step into CI in this PR (would broaden the diff beyond the wrapper surface). Flagging for a follow-up if the fleet wants these gated.