Files
stack/docs/scratchpads/559-560-wrapper-eval-login-20260620.md
jason.woltje feb0d8a58b
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix(framework/tools): wrapper body-safety + login-resolution hardening (#559, #560)
#559 — Markdown body safety / eval removal:
- Add test-issue-create-body-safety.sh: feeds a hostile Markdown body
  ($(...), backticks, quotes, $vars, pipes) through issue-create.sh and
  asserts no command substitution runs and the body reaches tea verbatim.
- Convert issue-comment.sh from unquoted $(get_gitea_repo_args) word-splitting
  to an argv array with an explicit loud login-resolution error.
- Confirmed: zero eval usages remain across tools/git/*.sh; the other
  body-carrying wrappers (issue-create, pr-create, issue-edit, issue-assign)
  already use argv arrays.

#560 — host-derived Gitea login + loud failure:
- detect-platform.sh: add print_gitea_login_diagnostic and emit it on the
  get_gitea_login_for_host failure path (stderr only) — names the unresolved
  host, lists available tea logins, and gives the GITEA_LOGIN override +
  tea-login-add fix. Replaces the previous silent failure.
- Extend test-gitea-login-resolution.sh: assert the diagnostic fires and lists
  logins, login is derived from origin host for both mosaicstack and usc (scoped
  second tea mock), and a valid GITEA_LOGIN override is honored.

Also gitignore the .mosaic-test-work/ shell-harness scratch dir.
Scope: wrapper surface only. All wrapper test harnesses pass locally.
2026-06-20 04:51:54 -05:00

5.2 KiB

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_hostfind_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.shnew (#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.