Compare commits

..
Author SHA1 Message Date
Mos ea806262c8 test(gateway): size the enrollment clamp tolerance to CI jitter, not to a fast machine
ci/woodpecker/pr/ci Pipeline was successful
Closes #1090.

`clamps ttlSeconds to 900` asserted a wall-clock delta against a fixed 100 ms
budget:

    expect(expiresMs - before).toBeLessThanOrEqual(900_000 + 100);

That budget covers everything between `Date.now()` and the service computing
`expiresAt`. On a loaded CI agent — this step runs alongside a Postgres service
container and several other workspaces — it came back at 900_106 and failed by
6 ms (#1090, observed on pipeline 2258).

The property under test is CLAMPING: a 9999s request must come back as 900s.
The gap between clamped and unclamped is 9_099_000 ms, so the tolerance only has
to exceed scheduler jitter to stay discriminating:

    clamped   ~900_000 ms   <= 905_000 ✓   < 1_000_000 ✓
    unclamped ~9_999_000 ms <= 905_000 ✗   < 1_000_000 ✗

A 5s allowance consumes 0.055% of that margin, and an unclamped implementation
still misses by three orders of magnitude.

Also pins the clamp explicitly with `toBeLessThan(1_000_000)`, so the property
is asserted independently of any timing allowance — widening the jitter budget
later cannot silently weaken it.

NOT DONE: I could not execute vitest here (no node_modules; a full install is
minutes). The discrimination argument above is arithmetic, not an executed
red/green. A reviewer should run the suite, and if it is cheap, confirm that
removing the clamp in the service still fails this test.
2026-08-07 00:18:50 -05:00
Mos f744f32214 feat(tools/git): explain tea's misleading user does not exist error (stale token, not a missing account) (#1086)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
2026-08-07 05:07:40 +00:00
Mos 8ff7aac0ca fix(tools/git): detect-platform died silently outside a repo, taking every wrapper with it (#1089)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
2026-08-07 04:26:36 +00:00
be-coder-08andMos 80a45b1e1c feat(pr-merge): preserve linked authors in squash messages (#1066)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: be-coder-08 <[email protected]>
2026-08-06 05:36:59 +00:00
14 changed files with 817 additions and 738 deletions
@@ -245,9 +245,21 @@ describe('EnrollmentService.createToken', () => {
const after = Date.now();
const expiresMs = new Date(result.expiresAt).getTime();
// Should be at most 900s from now
expect(expiresMs - before).toBeLessThanOrEqual(900_000 + 100);
// The property under test is CLAMPING: a 9999s request must come back as 900s.
// The gap between clamped and unclamped is 9_099_000 ms, so the tolerance below
// only has to exceed CI scheduling jitter — it does not need to be tight to keep
// the assertion discriminating. A 5s allowance consumes 0.05% of that margin and
// an unclamped result still misses by three orders of magnitude.
//
// It was 100ms and failed on a loaded agent at 900_106 — 6ms over (#1090). A
// wall-clock budget sized to a fast machine is a flake, not a tighter test.
const CI_JITTER_MS = 5_000;
expect(expiresMs - before).toBeLessThanOrEqual(900_000 + CI_JITTER_MS);
expect(expiresMs - after).toBeGreaterThanOrEqual(0);
// Explicitly pin the clamp itself, independent of any timing allowance:
// unclamped (9999s) would exceed this by ~9_099_000 ms.
expect(expiresMs - before).toBeLessThan(1_000_000);
});
});
+30 -14
View File
@@ -15,7 +15,7 @@ Add an optional, identity-checked Gitea squash message to `pr-merge.sh` so genui
1. `Do` remains hardcoded to `squash`; no provider/repository default may select merge style.
2. A verified trailer uses a PR commit's linked `author.login` and that same commit's author email. No `/users/{login}` primary-email lookup occurs. Recorded rationale: this asks only what the provider can answer.
3. A commit with `author.login` null blocks before merge, prints both the null provider fact and commit email fact, and names the escalation principal.
4. The BLOCK arm must be observed firing; a normal single-author API payload remains exactly `{ "Do": "squash" }`.
4. The BLOCK arm must be observed firing; a normal canonical single-author API payload remains explicit squash plus its reviewed `head_commit_id`.
5. Every provider mutation is read back from the provider; no real PR is merged during tests.
## Derived interface decisions
@@ -25,18 +25,19 @@ Add an optional, identity-checked Gitea squash message to `pr-merge.sh` so genui
- Do not expose `MergeTitleField` separately. When trailers exist, set it from the provider PR title and set `MergeMessageField` only to construction-generated trailers. This preserves one provider source for the title and avoids an unrelated caller-controlled degree of freedom.
- Preserve first-commit order and emit one trailer per distinct non-poster `author.login`, using that first linked commit's own email.
## Plan
## Canonical delivery plan
1. Version the currently deployed wrapper byte-for-byte under `infra/fleet/tools/git/pr-merge.sh`.
2. Pre-register verified, null-login BLOCK, unchanged single-author, and hardcoded-squash tests; observe RED before implementation.
3. Implement authenticated commit enumeration, construction-only trailers, message fields on REST, and force REST when trailers are requested.
4. Copy the exact final versioned bytes to the deployed wrapper; verify hashes match.
5. Run focused and baseline checks, static/security review, identity-bound commit, queue guard plus direct Woodpecker terminal enumeration, push, self-post PR, and provider poster read-back. Stop at push/PR; do not merge.
1. Port the capability into the installed source of truth, `packages/mosaic/framework/tools/git/pr-merge.sh`; do not retain `infra/fleet/tools/git` as a second copy.
2. Preserve canonical `--expect-head`, exact head branch/repository/SHA queue inspection, Gitea atomic head pinning, GitHub `--match-head-commit`, and delete-after-merge semantics.
3. Do not port the deployed-only `--skip-queue-guard` bypass. Add the focused harness to the canonical framework-shell suite and re-establish RED/GREEN on the packaged baseline.
4. Deliver through a reviewed package release followed by `mosaic update` with its default framework reseed. The installer snapshots, manifest-syncs framework-owned `tools/**`, and rolls back on failure.
5. Before either estate relies on the change, require installed/package hash equality, `MergeMessageField` presence, and a green focused harness. Release/reseed ownership is currently unassigned and blocks activation after source merge.
## Evidence
- RED against the byte-identical deployed baseline (`sha256 08a65e8584c5…`): rc 1 with eight named failures. The wrapper rejected `--co-author-trailers`; the null-login path emitted none of the required BLOCK facts/principal; and both verified/ordinary API paths failed the stdin-config credential assertion (ordinary path exposed the fixture token through curl argv). Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-message-field-red.log`.
- GREEN after implementation: verified linked multi-author payload, null-login BLOCK, required named principal, unchanged ordinary `{ "Do": "squash" }` payload, hard non-squash refusal, stdin-config token transport, and absence of `/users` lookup all passed. Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-message-field-green.log`.
- GREEN on the deployed-baseline candidate: verified linked multi-author payload, null-login BLOCK, required named principal, explicit squash, stdin-config token transport, and absence of `/users` lookup all passed. Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-message-field-green.log`.
- RED against canonical packaged baseline `c581ef48…`: rc 1 with 32 assertions. It rejects the new option, and the first harness version did not satisfy canonical head branch/repository/SHA metadata. Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-packaged-baseline-red.log`. The port adapts the fixture rather than weakening canonical head controls.
- Provider capability probe against `git.mosaicstack.dev`: authenticated `be-coder-08` POST to deliberately nonexistent PR `2147483647` with both message fields returned JSON HTTP 404; the unauthenticated same request returned JSON HTTP 401 (not the charter's predicted 403). The authenticated-vs-unauthenticated differential proves write authorization resolved while no mergeable subject existed. `tl-mosaic` ruled the literal non-load-bearing: preserve the observed 404/401 pair and do not manufacture a 403 case. No cause was inferred and no real PR was targeted.
- Provider-generated trailer behavior is not treated as exclusive or absent. The wrapper's VERIFIED/BLOCK decision binds each requested non-poster trailer to commit `author.login` plus that commit's email; it does not assume `MergeMessageField` is the squash's only trailer source. The poster is omitted from the constructed list because the resulting squash author already records the poster; any additional provider-generated trailer is outside this change's unmeasured mechanism.
- An early candidate SHA-256 `5de32876990e4f26920448cb3220cc7f1146d558b4dd2bc1ee1a2abee2f2cbe6` passed the initial harness, then author-side review found credential-fallback and argv-exposure defects. The live deployed wrapper was atomically restored to baseline SHA-256 `08a65e8584c52c6d41ea1c686f8b95585c21e4b37320a2447eba09359a0e02c1`; the remediated candidate remains only in the worktree.
@@ -45,20 +46,20 @@ Add an optional, identity-checked Gitea squash message to `pr-merge.sh` so genui
1. Token and Basic Auth now use stdin curl configuration, not argv. PR title, contributor email, and the JSON payload also remain out of child argv.
2. Each credential attempt binds commit inspection and merge. A token failure during either inspection or mutation causes Basic fallback to repeat inspection before mutation; the payload pins the inspected `head_commit_id`.
3. Focused tests cover both fallback seams, metadata/credential argv absence, null-login BLOCK, explicit squash, unchanged ordinary payload, and retained log-safe provider diagnostics.
3. Focused tests cover token-resolution fail-closed behavior, both HTTP-401 fallback seams, metadata/credential argv absence, null-login BLOCK, explicit squash, canonical reviewed-head binding, unchanged ordinary payload, and retained log-safe provider diagnostics. Token-resolution RED: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-token-resolution-red.log`.
4. Codex review rounds 35 requested retained provider error text, log-safe provider diagnostics, fail-closed credential fallback, stable value-option parsing, and PR-title trailer-injection prevention. These are remediated with regression assertions. A post-remediation independent review is still required.
5. **Accepted linkage limitation:** `author.login` resolution proves that the commit address maps to a registered provider account. It does not prove that the named principal authored the commit because Git author metadata is self-asserted. This gate checks attribution linkage, not authorship; commit signing is out of scope and currently unadopted. Coordinators explicitly ruled that this does not add a third state.
6. Codex's sandbox could not execute the harness because its checkout was read-only; that environmental limitation is recorded separately from host-side test results.
## Disposable provider fixture still required
## Disposable provider fixture acceptance
- Use a retained scratch repository only, with two branch authors and `author != committer` on at least one commit.
- Arm A supplies a message-field trailer and classifies the landed squash object as `APPENDS`, `OVERWRITES`, or `REPLACES`; its absence control must also be demonstrated.
- Arm B includes a registered foreign branch trailer and classifies it as `SURVIVES` or `DROPPED`; verify that identity through an existing commit whose `author.login` resolves. Demonstrate an absence control.
- Arm A supplies a message-field trailer for one non-poster; record whether that value lands without forcing the partial-pair result into under-specified `APPENDS`/`REPLACES` labels. Demonstrate an absence control.
- Arm B includes a registered trailer for a different non-poster on a branch commit; record whether it survives or drops. Verify identity through an existing commit whose `author.login` resolves and demonstrate an absence control.
- Parse landed trailers key-agnostically with `^[A-Za-z-]+-[Bb]y:` and record generated poster pair presence/absence plus resulting poster attribution.
- Record `/users/<login>` status and raw email only as non-gating estate telemetry. Never read `active`, `visibility`, or any profile field as an identity gate.
- Use distinct principals: poster `be-coder-08`, merger `Mos`, Arm A `be-coder-07`, and Arm B `be-coder-06`. Capture every trailer-shaped line verbatim and in order. Zero trailer lines means the generator did not fire and the run is `VOID`, not evidence that either arm dropped.
- Report the same read-back evidence to `mos-claude` on socket `default` and `tl-mosaic` on socket `mosaic-fleet`. Stop on `OVERWRITES`, `REPLACES`, or any poster-attribution regression.
- Report the same read-back evidence to `mos-claude` on socket `default` and `tl-mosaic` on socket `mosaic-fleet`. Report values rather than mechanism inferences and stop on any poster-attribution regression.
## Fixture preflight
@@ -77,7 +78,22 @@ Add an optional, identity-checked Gitea squash message to `pr-merge.sh` so genui
2. `Co-authored-by: be-coder-08 <[email protected]>`
- Arm A supplied field value (`be-coder-07`) landed. Arm B branch trailer (`be-coder-06`) dropped. Both fabricated absence controls remained absent. No `Co-committed-by:` line landed.
- The candidate payload construction explicitly excludes the poster and supplied only the Arm A `be-coder-07` line. Therefore the landed poster line was provider-generated, not candidate-composed. The raw result supports `FIELD LANDS`, `BRANCH DROPS`, and `POSTER GENERATED`; it does not support a claim that candidate code supplied the poster. Evidence: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-fixture-readback.log` and the retained provider object.
- Retained fixture PR `#2` measured the N=2 shape needed by `#1030`: supplied `be-coder-07` then `be-coder-06`; both landed in that order, followed by the provider-generated poster line. No truncation or dedup occurred at N=2. Resulting squash: `39db9d13aed0…`.
## Current hold point
No delivery branch push or PR exists. The deployed wrapper remains on its original baseline bytes. Implementation and disposable end-to-end read-back are green; final review disposition, commit/rebase, and delivery gates remain.
PR `mosaicstack/stack#1066` is open. Its first frozen head `f4b162fa…` was terminal-green in Woodpecker `mosaic` pipeline `#2225`, but that evidence becomes stale when the canonical port moves the head. The deployed wrapper remains baseline `08a65e85…`; no manual copy will occur. Canonical port tests, commit amendment, rebase, one guarded force-with-lease, exact-head CI, and new independent review remain. Even after source merge, activation remains blocked on an assigned package-release/reseed owner and installed-byte read-back.
## Security review 96 remediation
Exact reviewed predecessor head: `1ceb11058f64dd7f4a817ceb2124f980a1c4dd23`.
RED-first focused harness produced 10 named failures: all curl calls lacked size/time/connect bounds; raw ESC email reached mutation; oversized and stalled curl failures were discarded and reached mutation; nonempty Basic output with resolver rc 91 authorized mutation.
Security remediation:
- Removed the cross-principal HTTP-401 Basic fallback. Both inspection-401 and merge-401 paths now refuse without Basic resolution or mutation; `get_gitea_basic_auth` references in the merge subject are 0.
- Applied `--max-filesize`, `--max-time`, and `--connect-timeout` to all 3/3 provider curl sites and fail closed on curl transport rc at all 3/3 sites.
- Required linked email bytes to be ASCII and printable before constructing `MergeMessageField`; guarded construction sites 1/1.
GREEN: message-field, exact-head, empty-UID/API, queue branch/repository/SHA, bash syntax, ShellCheck, and diff check pass. R7 total-removal mutants went RED: email guard 3 rows; bound switches 1 row; transport-rc guards 4 rows; HTTP-401 refusal 3 rows. R7 bound: mutants prove total removal only; explicit denominators above prove site coverage.
-623
View File
@@ -1,623 +0,0 @@
#!/bin/bash
# pr-merge.sh - Merge pull requests on Gitea or GitHub
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--co-author-trailers --escalate-to PRINCIPAL]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=packages/mosaic/framework/tools/git/detect-platform.sh
source "$SCRIPT_DIR/detect-platform.sh"
# Default values
PR_NUMBER=""
MERGE_METHOD="squash"
DELETE_BRANCH=false
SKIP_QUEUE_GUARD=false
DRY_RUN=false
CO_AUTHOR_TRAILERS=false
ESCALATE_TO=""
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Merge a pull request on the current repository (Gitea or GitHub).
Options:
-n, --number NUMBER PR number to merge (required)
-m, --method METHOD Merge method: squash only (default: squash)
-d, --delete-branch Delete the head branch after merge
--skip-queue-guard Skip CI queue guard wait before merge
--dry-run Run metadata/login preflight without merging
--co-author-trailers Build verified trailers from linked PR commit authors
--escalate-to NAME Named principal for an unresolved-author BLOCK
-h, --help Show this help message
Examples:
$(basename "$0") -n 42 # Merge PR #42
$(basename "$0") -n 42 -m squash # Squash merge
$(basename "$0") -n 42 -d # Squash merge and delete branch
$(basename "$0") -n 42 --skip-queue-guard # Skip queue guard wait
$(basename "$0") -n 42 --co-author-trailers --escalate-to tl-mosaic
EOF
exit "${1:-1}"
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-n|--number)
PR_NUMBER="$2"
shift 2
;;
-m|--method)
MERGE_METHOD="$2"
shift 2
;;
-d|--delete-branch)
DELETE_BRANCH=true
shift
;;
--skip-queue-guard)
SKIP_QUEUE_GUARD=true
shift
;;
--dry-run)
DRY_RUN=true
SKIP_QUEUE_GUARD=true
shift
;;
--co-author-trailers)
CO_AUTHOR_TRAILERS=true
shift
;;
--escalate-to)
if [[ $# -lt 2 ]]; then
echo "Error: --escalate-to requires one principal name." >&2
exit 1
fi
ESCALATE_TO="$2"
shift 2
;;
-h|--help)
usage 0
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
if [[ -z "$PR_NUMBER" ]]; then
echo "Error: PR number is required (-n)" >&2
usage
fi
if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "Error: Invalid PR number '$PR_NUMBER'. PR number must contain digits only." >&2
exit 1
fi
if [[ "$MERGE_METHOD" != "squash" ]]; then
echo "Error: Mosaic policy enforces squash merge only. Received '$MERGE_METHOD'." >&2
exit 1
fi
if [[ "$CO_AUTHOR_TRAILERS" == true && -z "$ESCALATE_TO" ]]; then
echo "Error: --co-author-trailers requires --escalate-to with a named principal." >&2
exit 1
fi
if [[ -n "$ESCALATE_TO" && ! "$ESCALATE_TO" =~ ^[A-Za-z0-9_.-]+$ ]]; then
echo "Error: --escalate-to must be one exact principal name." >&2
exit 1
fi
if [[ "$CO_AUTHOR_TRAILERS" != true && -n "$ESCALATE_TO" ]]; then
echo "Error: --escalate-to is valid only with --co-author-trailers." >&2
exit 1
fi
PR_METADATA="$("$SCRIPT_DIR/pr-metadata.sh" -n "$PR_NUMBER")"
BASE_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("baseRefName") or "").strip())')"
PR_TITLE="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("title") or "").strip())')"
PR_AUTHOR="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("author") or ""; print((value.get("login") or "").strip() if isinstance(value, dict) else str(value).strip())')"
if [[ "$BASE_BRANCH" != "main" ]]; then
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2
exit 1
fi
if [[ "$SKIP_QUEUE_GUARD" != true ]]; then
"$SCRIPT_DIR/ci-queue-wait.sh" \
--purpose merge \
-B "$BASE_BRANCH" \
-t "${MOSAIC_CI_QUEUE_TIMEOUT_SEC:-900}" \
-i "${MOSAIC_CI_QUEUE_POLL_SEC:-15}"
fi
PLATFORM=$(detect_platform)
OWNER=$(get_repo_owner)
REPO=$(get_repo_name)
is_known_tea_empty_identity_failure() {
local error_file="$1"
python3 - "$error_file" <<'PY'
import re
import sys
with open(sys.argv[1], encoding="utf-8", errors="replace") as handle:
error = handle.read()
known_empty_identity = re.search(
r"user does not exist.*\[.*uid:\s*0,\s*name:\s*\]",
error,
flags=re.IGNORECASE | re.DOTALL,
)
raise SystemExit(0 if known_empty_identity else 1)
PY
}
write_curl_auth_config() {
local mode="$1" credential="$2"
printf '%s' "$credential" | python3 -c '
import sys
mode = sys.argv[1]
credential = sys.stdin.read()
if not credential or any(char in credential for char in "\r\n"):
raise SystemExit(1)
escaped = credential.replace("\\", "\\\\").replace("\"", "\\\"")
if mode == "token":
print(f"header = \"Authorization: token {escaped}\"")
elif mode == "basic":
print(f"user = \"{escaped}\"")
else:
raise SystemExit(1)
' "$mode"
}
LAST_GITEA_HTTP_CODE="000"
LAST_GITEA_ERROR=""
MERGE_TEMP_DIRS=()
format_gitea_error_response() {
local response_file="$1"
python3 - "$response_file" <<'PY'
import json
import sys
with open(sys.argv[1], "rb") as handle:
raw = handle.read(65536)
try:
response = json.loads(raw.decode("utf-8", errors="replace"))
except (UnicodeDecodeError, json.JSONDecodeError):
message = "non-JSON response omitted"
else:
if isinstance(response, dict):
message = response.get("message") or response.get("error")
if not message and response.get("errors") is not None:
message = json.dumps(response["errors"], separators=(",", ":"))
else:
message = None
if not message:
message = "JSON response contained no error message"
message = str(message)
if len(message) > 500:
message = message[:500] + "..."
print(ascii(message))
PY
}
cleanup_merge_temp_dirs() {
local path
for path in "${MERGE_TEMP_DIRS[@]}"; do
[[ -n "$path" ]] && rm -rf -- "$path"
done
}
trap cleanup_merge_temp_dirs EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
fetch_gitea_pr_head() {
local host="$1" auth_mode="$2" credential="$3" work_root="$4"
local response_file raw_code api_url
response_file=$(mktemp "$work_root/pr-merge-pr.XXXXXX")
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
raw_code=$(write_curl_auth_config "$auth_mode" "$credential" | \
curl -sS -K - -w '%{http_code}' -o "$response_file" \
-H "User-Agent: curl/8" "$api_url" || true)
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$response_file")
rm -f "$response_file"
return 1
fi
if ! python3 - "$response_file" <<'PY'
import json
import re
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
pull = json.load(handle)
head = pull.get("head") if isinstance(pull, dict) else None
sha = str(head.get("sha") or "") if isinstance(head, dict) else ""
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
raise SystemExit(1)
print(sha)
PY
then
echo "Error: Gitea PR response has no valid head SHA; refusing merge." >&2
rm -f "$response_file"
return 1
fi
rm -f "$response_file"
}
fetch_gitea_pr_commits() {
local host="$1" auth_mode="$2" credential="$3" work_root="$4"
local page page_file combined_file merged_file raw_code page_count api_url
mkdir -p "$work_root"
combined_file=$(mktemp "$work_root/pr-merge-commits.XXXXXX")
printf '[]' > "$combined_file"
page=1
while true; do
page_file=$(mktemp "$work_root/pr-merge-commits-page.XXXXXX")
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/commits?limit=50&page=${page}"
raw_code=$(write_curl_auth_config "$auth_mode" "$credential" | \
curl -sS -K - -w '%{http_code}' -o "$page_file" \
-H "User-Agent: curl/8" "$api_url" || true)
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$page_file")
rm -f "$page_file" "$combined_file"
return 1
fi
if ! page_count=$(python3 - "$page_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
page = json.load(handle)
if not isinstance(page, list):
raise SystemExit(1)
print(len(page))
PY
); then
echo "Error: Gitea PR commits response is not a JSON array; refusing merge." >&2
rm -f "$page_file" "$combined_file"
return 1
fi
merged_file=$(mktemp "$work_root/pr-merge-commits-merged.XXXXXX")
if ! python3 - "$combined_file" "$page_file" > "$merged_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
combined = json.load(handle)
with open(sys.argv[2], encoding="utf-8") as handle:
page = json.load(handle)
json.dump(combined + page, sys.stdout, separators=(",", ":"))
PY
then
echo "Error: Could not combine paginated PR commit metadata; refusing merge." >&2
rm -f "$page_file" "$combined_file" "$merged_file"
return 1
fi
mv "$merged_file" "$combined_file"
rm -f "$page_file"
if [[ "$page_count" -lt 50 ]]; then
break
fi
page=$((page + 1))
if [[ "$page" -gt 1000 ]]; then
echo "Error: PR commit pagination exceeded 1000 pages; refusing merge." >&2
rm -f "$combined_file"
return 1
fi
done
cat "$combined_file"
rm -f "$combined_file"
}
# LIMITATION: author.login resolution proves the commit address maps to a registered account.
# It does NOT prove the named principal authored the commit — git author metadata is self-asserted.
# This gate checks ATTRIBUTION LINKAGE, not AUTHORSHIP. Commit signing is out of scope and unadopted.
build_coauthor_message_fields() {
local commits_file="$1" context_file="$2" head_file="$3"
python3 - "$commits_file" "$context_file" "$head_file" <<'PY'
import json
import re
import sys
commits_path, context_path, head_path = sys.argv[1:]
with open(commits_path, encoding="utf-8") as handle:
commits = json.load(handle)
head_sha = open(head_path, encoding="utf-8").read().strip()
context_parts = open(context_path, "rb").read().split(b"\0")
if len(context_parts) != 4 or context_parts[-1] != b"":
raise SystemExit(1)
poster, title, principal = (part.decode("utf-8") for part in context_parts[:3])
if not isinstance(commits, list) or not commits:
print(
f"BLOCK: provider returned no PR commits; author identity is unmeasurable. "
f"Refusing merge; escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not poster:
print(
f"BLOCK: PR poster login is empty; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not re.fullmatch(r"[0-9a-fA-F]{40}", head_sha):
print(
f"BLOCK: inspected PR head SHA is invalid; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
seen = set()
trailers = []
head_seen = False
for item in commits:
if not isinstance(item, dict):
print(f"BLOCK: malformed PR commit metadata; escalate to named principal '{principal}'.", file=sys.stderr)
raise SystemExit(75)
sha = str(item.get("sha") or "<unknown>")
if sha == head_sha:
head_seen = True
commit = item.get("commit") if isinstance(item.get("commit"), dict) else {}
commit_author = commit.get("author") if isinstance(commit.get("author"), dict) else {}
email = str(commit_author.get("email") or "").strip()
provider_author = item.get("author") if isinstance(item.get("author"), dict) else {}
login = str(provider_author.get("login") or "").strip()
if not login:
diagnostic_email = email or "<missing>"
print(
f"BLOCK: commit {sha!r} has author.login=NULL while "
f"commit.author.email={diagnostic_email!r}; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not re.fullmatch(r"[A-Za-z0-9_.-]+", login) or not re.fullmatch(r"[^<>\s]+@[^<>\s]+", email):
print(
f"BLOCK: commit {sha!r} has unusable linked identity "
f"author.login={login!r}, commit.author.email={email!r}; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if login == poster or login in seen:
continue
seen.add(login)
trailers.append(f"Co-authored-by: {login} <{email}>")
if not head_seen:
print(
f"BLOCK: inspected PR head is absent from commit enumeration; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not trailers:
print(json.dumps({"head_commit_id": head_sha}, separators=(",", ":")))
raise SystemExit(0)
if not title:
print(
f"BLOCK: PR title is empty; refusing merge; escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not title.isprintable() or re.match(r"^[A-Za-z-]+-[Bb]y:", title):
print(
f"BLOCK: PR title is not one printable, non-trailer line; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
print(json.dumps({
"head_commit_id": head_sha,
"MergeTitleField": title,
"MergeMessageField": "\n".join(trailers),
}, separators=(",", ":")))
PY
}
merge_gitea_api_attempt() {
local host="$1" auth_mode="$2" credential="$3"
local api_url attempt_dir body_file raw_code commits_file fields_file context_file head_file payload_file work_root attempt_rc
LAST_GITEA_ERROR=""
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/merge"
work_root="${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
mkdir -p "$work_root"
attempt_dir=$(mktemp -d "$work_root/pr-merge-attempt.XXXXXX")
chmod 0700 "$attempt_dir"
MERGE_TEMP_DIRS+=("$attempt_dir")
body_file=$(mktemp "$attempt_dir/api-response.XXXXXX")
fields_file=$(mktemp "$attempt_dir/message-fields.XXXXXX")
payload_file=$(mktemp "$attempt_dir/payload.XXXXXX")
printf '{}' > "$fields_file"
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
commits_file=$(mktemp "$work_root/pr-merge-commits-input.XXXXXX")
context_file=$(mktemp "$work_root/pr-merge-message-context.XXXXXX")
head_file=$(mktemp "$work_root/pr-merge-head-input.XXXXXX")
printf '%s\0%s\0%s\0' "$PR_AUTHOR" "$PR_TITLE" "$ESCALATE_TO" > "$context_file"
if fetch_gitea_pr_head "$host" "$auth_mode" "$credential" "$attempt_dir" > "$head_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
if fetch_gitea_pr_commits "$host" "$auth_mode" "$credential" "$attempt_dir" > "$commits_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
if build_coauthor_message_fields "$commits_file" "$context_file" "$head_file" > "$fields_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
rm -f "$commits_file" "$context_file" "$head_file"
fi
if ! python3 - "$fields_file" > "$payload_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
fields = json.load(handle)
payload = {"Do": "squash"}
payload.update(fields)
if payload.get("Do") != "squash" or set(payload) - {"Do", "head_commit_id", "MergeTitleField", "MergeMessageField"}:
raise SystemExit(1)
print(json.dumps(payload, separators=(",", ":")))
PY
then
rm -f "$body_file" "$fields_file" "$payload_file"
return 1
fi
rm -f "$fields_file"
raw_code=$(write_curl_auth_config "$auth_mode" "$credential" | \
curl -sS -K - -w '%{http_code}' -o "$body_file" \
-X POST -H "User-Agent: curl/8" \
-H 'Content-Type: application/json' \
--data-binary "@$payload_file" "$api_url" || true)
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$body_file")
fi
rm -f "$body_file" "$payload_file"
rm -rf -- "$attempt_dir"
[[ "$raw_code" =~ ^2 ]]
}
merge_gitea_with_api() {
local host="$1" token basic_auth attempt_rc
token=$(get_gitea_token "$host" || true)
if [[ -n "$token" ]]; then
if merge_gitea_api_attempt "$host" token "$token"; then
return 0
else
attempt_rc=$?
fi
if [[ "$attempt_rc" -eq 75 ]]; then
return 75
fi
if [[ "$LAST_GITEA_HTTP_CODE" != "401" ]]; then
echo "Error: Gitea API merge failed with token credential (HTTP ${LAST_GITEA_HTTP_CODE:-000}).${LAST_GITEA_ERROR:+ Provider response: $LAST_GITEA_ERROR} Basic Auth fallback is allowed only after HTTP 401." >&2
return 1
fi
echo "Token credential received HTTP 401; retrying inspection and merge with configured Basic Auth." >&2
fi
basic_auth=$(get_gitea_basic_auth "$host" || true)
if [[ -n "$basic_auth" ]]; then
if merge_gitea_api_attempt "$host" basic "$basic_auth"; then
return 0
else
attempt_rc=$?
fi
if [[ "$attempt_rc" -eq 75 ]]; then
return 75
fi
fi
if [[ -z "$token" && -z "$basic_auth" ]]; then
echo "Error: No Gitea credential is available for the merge operation." >&2
else
echo "Error: Gitea API merge failed for all configured credentials (last HTTP ${LAST_GITEA_HTTP_CODE:-000}).${LAST_GITEA_ERROR:+ Provider response: $LAST_GITEA_ERROR}" >&2
fi
return 1
}
if [[ "$DRY_RUN" == true ]]; then
if [[ "$PLATFORM" == "gitea" ]]; then
HOST=$(get_remote_host) || {
echo "Error: Cannot determine host from origin remote URL" >&2
exit 1
}
TEA_LOGIN="$(get_gitea_login_for_host "$HOST" || true)"
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
echo "Dry run: would verify PR commit authors and merge PR #$PR_NUMBER on $HOST with authenticated Gitea API message fields (base=$BASE_BRANCH, method=squash)."
elif [[ -n "$TEA_LOGIN" ]]; then
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with tea login '$TEA_LOGIN' (base=$BASE_BRANCH, method=squash)."
else
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with authenticated Gitea API fallback (base=$BASE_BRANCH, method=squash)."
fi
else
echo "Dry run: would merge PR #$PR_NUMBER on $PLATFORM (base=$BASE_BRANCH, method=squash)."
fi
exit 0
fi
case "$PLATFORM" in
github)
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
echo "Error: --co-author-trailers currently requires the Gitea REST message-field contract." >&2
exit 1
fi
cmd=(gh pr merge "$PR_NUMBER" --squash)
[[ "$DELETE_BRANCH" == true ]] && cmd+=(--delete-branch)
"${cmd[@]}"
;;
gitea)
HOST=$(get_remote_host) || {
echo "Error: Cannot determine host from origin remote URL" >&2
exit 1
}
TEA_LOGIN="$(get_gitea_login_for_host "$HOST" || true)"
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
merge_gitea_with_api "$HOST"
elif [[ -n "$TEA_LOGIN" ]]; then
mkdir -p "${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
TEA_ERROR_FILE=$(mktemp "${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}/pr-merge-tea-error.XXXXXX")
if tea pr merge "$PR_NUMBER" --style squash --repo "$OWNER/$REPO" --login "$TEA_LOGIN" 2> "$TEA_ERROR_FILE"; then
rm -f "$TEA_ERROR_FILE"
elif is_known_tea_empty_identity_failure "$TEA_ERROR_FILE"; then
cat "$TEA_ERROR_FILE" >&2
echo "Known tea empty identity failure detected; using authenticated Gitea API merge fallback." >&2
rm -f "$TEA_ERROR_FILE"
merge_gitea_with_api "$HOST"
else
cat "$TEA_ERROR_FILE" >&2
rm -f "$TEA_ERROR_FILE"
exit 1
fi
else
echo "No tea login configured for $HOST; using authenticated Gitea API merge fallback." >&2
merge_gitea_with_api "$HOST"
fi
# Delete branch after merge if requested
if [[ "$DELETE_BRANCH" == true ]]; then
echo "Note: Branch deletion after merge may need to be done separately with tea" >&2
fi
;;
*)
echo "Error: Could not detect git platform" >&2
exit 1
;;
esac
echo "PR #$PR_NUMBER merged successfully"
@@ -5,7 +5,10 @@
detect_platform() {
local remote_url
remote_url=$(git remote get-url origin 2>/dev/null)
# `|| true` is load-bearing under `set -e`: outside a git repo this returns 128 and
# kills the CALLER before the -z check below can run, so the error message that is
# already written here was unreachable. Same idiom as get_gitea_repo_args() below.
remote_url=$(git remote get-url origin 2>/dev/null) || true
if [[ -z "$remote_url" ]]; then
echo "error: not a git repository or no origin remote" >&2
@@ -39,7 +42,10 @@ detect_platform() {
get_repo_info() {
local remote_url
remote_url=$(git remote get-url origin 2>/dev/null)
# `|| true` is load-bearing under `set -e`: outside a git repo this returns 128 and
# kills the CALLER before the -z check below can run, so the error message that is
# already written here was unreachable. Same idiom as get_gitea_repo_args() below.
remote_url=$(git remote get-url origin 2>/dev/null) || true
if [[ -z "$remote_url" ]]; then
echo "error: not a git repository or no origin remote" >&2
@@ -240,6 +246,21 @@ PY
} >&2
}
# Explain tea's most misleading failure. `user does not exist [uid: 0, name: ]` reads
# as a missing account; it almost always means a REVOKED OR STALE TOKEN. `tea login`
# keeps its OWN COPY of the token, so rotating the credential store does not update it.
# Diagnostic only -- stderr, no control flow, no exit.
explain_tea_user_does_not_exist() {
cat >&2 <<'MSG'
NOTE: `user does not exist [uid: 0, name: ]` from tea usually means a REVOKED OR STALE TOKEN,
not a missing account. A `tea login` stores its OWN COPY of the token; rotating the
credential store does NOT update it.
CHECK: the login's cached copy (`tea login list` -- read the FULL table, never `| head`),
then re-register that login against the current token.
DO NOT probe capability with a mutating request; a POST is the action, not a check.
MSG
}
get_gitea_login_for_host() {
local host="${1:-}"
local login
@@ -156,6 +156,7 @@ case "$PLATFORM" in
exit 0
fi
echo "Warning: tea issue create failed, trying Gitea API fallback..." >&2
{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true
fi
gitea_issue_create_api
;;
@@ -71,6 +71,7 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
exit 0
fi
echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2
{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true
fi
gitea_issue_view_api
else
@@ -219,6 +219,7 @@ case "$PLATFORM" in
exit 0
fi
echo "Warning: tea pr create failed, trying Gitea API fallback..." >&2
{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true
gitea_pr_create_api
;;
*)
+469 -59
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# pr-merge.sh - Merge pull requests on Gitea or GitHub
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d]
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--co-author-trailers --escalate-to PRINCIPAL]
set -euo pipefail
@@ -14,6 +14,8 @@ MERGE_METHOD="squash"
DELETE_BRANCH=false
DRY_RUN=false
EXPECT_HEAD=""
CO_AUTHOR_TRAILERS=false
ESCALATE_TO=""
usage() {
cat <<EOF
@@ -27,12 +29,16 @@ Options:
-d, --delete-branch Delete the head branch after merge
--dry-run Run metadata/login preflight without merging
--expect-head SHA Refuse unless the PR head matches this full commit SHA
--co-author-trailers Build verified trailers from linked PR commit authors
--escalate-to NAME Named principal for an unresolved-author BLOCK
-h, --help Show this help message
Examples:
$(basename "$0") -n 42 # Merge PR #42
$(basename "$0") -n 42 -m squash # Squash merge
$(basename "$0") -n 42 -d # Squash merge and delete branch
$(basename "$0") -n 42 --expect-head 0123456789abcdef0123456789abcdef01234567
$(basename "$0") -n 42 --co-author-trailers --escalate-to tl-mosaic
EOF
exit "${1:-1}"
}
@@ -57,9 +63,25 @@ while [[ $# -gt 0 ]]; do
shift
;;
--expect-head)
if [[ $# -lt 2 ]]; then
echo "Error: --expect-head requires one full commit SHA." >&2
exit 1
fi
EXPECT_HEAD="$2"
shift 2
;;
--co-author-trailers)
CO_AUTHOR_TRAILERS=true
shift
;;
--escalate-to)
if [[ $# -lt 2 ]]; then
echo "Error: --escalate-to requires one principal name." >&2
exit 1
fi
ESCALATE_TO="$2"
shift 2
;;
-h|--help)
usage 0
;;
@@ -88,17 +110,30 @@ if [[ -n "$EXPECT_HEAD" && ! "$EXPECT_HEAD" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: --expect-head must be a full 40-character hexadecimal commit SHA." >&2
exit 1
fi
if [[ "$CO_AUTHOR_TRAILERS" == true && -z "$ESCALATE_TO" ]]; then
echo "Error: --co-author-trailers requires --escalate-to with a named principal." >&2
exit 1
fi
if [[ -n "$ESCALATE_TO" && ! "$ESCALATE_TO" =~ ^[A-Za-z0-9_.-]+$ ]]; then
echo "Error: --escalate-to must be one exact principal name." >&2
exit 1
fi
if [[ "$CO_AUTHOR_TRAILERS" != true && -n "$ESCALATE_TO" ]]; then
echo "Error: --escalate-to is valid only with --co-author-trailers." >&2
exit 1
fi
PR_METADATA="$("$SCRIPT_DIR/pr-metadata.sh" -n "$PR_NUMBER")"
BASE_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("baseRefName") or "").strip())')"
HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')"
HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')"
HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')"
PR_TITLE="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("title") or "").strip())')"
PR_AUTHOR="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("author") or ""; print((value.get("login") or "").strip() if isinstance(value, dict) else str(value).strip())')"
if [[ "$BASE_BRANCH" != "main" ]]; then
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2
exit 1
fi
if [[ -z "$HEAD_BRANCH" || -z "$HEAD_REPO" || ! "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: Could not resolve the PR head branch, repository, and full commit SHA for queue inspection." >&2
exit 1
@@ -122,70 +157,442 @@ PLATFORM=$(detect_platform)
OWNER=$(get_repo_owner)
REPO=$(get_repo_name)
merge_gitea_with_api() {
local host="$1" api_url token basic_auth body_file raw_code payload
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/merge"
mkdir -p "${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
body_file=$(mktemp "${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}/pr-merge-api-response.XXXXXX")
payload=$(python3 - "$HEAD_SHA" "$DELETE_BRANCH" <<'PY'
write_curl_auth_config() {
local mode="$1" credential="$2"
printf '%s' "$credential" | python3 -c '
import sys
mode = sys.argv[1]
credential = sys.stdin.read()
if not credential or any(char in credential for char in "\r\n"):
raise SystemExit(1)
escaped = credential.replace("\\", "\\\\").replace("\"", "\\\"")
if mode == "token":
print(f"header = \"Authorization: token {escaped}\"")
elif mode == "basic":
print(f"user = \"{escaped}\"")
else:
raise SystemExit(1)
' "$mode"
}
LAST_GITEA_HTTP_CODE="000"
LAST_GITEA_ERROR=""
MERGE_TEMP_DIRS=()
GITEA_CURL_MAX_BYTES="${MOSAIC_GITEA_CURL_MAX_BYTES:-1048576}"
GITEA_CURL_MAX_TIME="${MOSAIC_GITEA_CURL_MAX_TIME_SEC:-30}"
GITEA_CURL_CONNECT_TIMEOUT="${MOSAIC_GITEA_CURL_CONNECT_TIMEOUT_SEC:-10}"
for bound in "$GITEA_CURL_MAX_BYTES" "$GITEA_CURL_MAX_TIME" "$GITEA_CURL_CONNECT_TIMEOUT"; do
if [[ ! "$bound" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: Gitea curl bounds must be positive integers; refusing request." >&2
exit 1
fi
done
GITEA_CURL_BOUNDS=(
--max-filesize "$GITEA_CURL_MAX_BYTES"
--max-time "$GITEA_CURL_MAX_TIME"
--connect-timeout "$GITEA_CURL_CONNECT_TIMEOUT"
)
format_gitea_error_response() {
local response_file="$1"
python3 - "$response_file" <<'PY'
import json
import sys
head_sha, delete_branch = sys.argv[1:]
with open(sys.argv[1], "rb") as handle:
raw = handle.read(65536)
try:
response = json.loads(raw.decode("utf-8", errors="replace"))
except (UnicodeDecodeError, json.JSONDecodeError):
message = "non-JSON response omitted"
else:
if isinstance(response, dict):
message = response.get("message") or response.get("error")
if not message and response.get("errors") is not None:
message = json.dumps(response["errors"], separators=(",", ":"))
else:
message = None
if not message:
message = "JSON response contained no error message"
message = str(message)
if len(message) > 500:
message = message[:500] + "..."
print(ascii(message))
PY
}
cleanup_merge_temp_dirs() {
local path
for path in "${MERGE_TEMP_DIRS[@]}"; do
[[ -n "$path" ]] && rm -rf -- "$path"
done
}
trap cleanup_merge_temp_dirs EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
fetch_gitea_pr_head() {
local host="$1" auth_mode="$2" credential="$3" work_root="$4"
local response_file raw_code api_url auth_config curl_rc
response_file=$(mktemp "$work_root/pr-merge-pr.XXXXXX")
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
if ! auth_config=$(write_curl_auth_config "$auth_mode" "$credential"); then
echo "Error: Could not construct Gitea authentication config; refusing request." >&2
rm -f "$response_file"
return 1
fi
raw_code=$(curl -sS -K - "${GITEA_CURL_BOUNDS[@]}" -w '%{http_code}' -o "$response_file" \
-H "User-Agent: curl/8" "$api_url" <<<"$auth_config")
curl_rc=$?
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ "$curl_rc" -ne 0 ]]; then
LAST_GITEA_ERROR="curl transport failed (rc=$curl_rc)"
rm -f "$response_file"
return 1
fi
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$response_file")
rm -f "$response_file"
return 1
fi
if ! python3 - "$response_file" <<'PY'
import json
import re
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
pull = json.load(handle)
head = pull.get("head") if isinstance(pull, dict) else None
sha = str(head.get("sha") or "") if isinstance(head, dict) else ""
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
raise SystemExit(1)
print(sha)
PY
then
echo "Error: Gitea PR response has no valid head SHA; refusing merge." >&2
rm -f "$response_file"
return 1
fi
rm -f "$response_file"
}
fetch_gitea_pr_commits() {
local host="$1" auth_mode="$2" credential="$3" work_root="$4"
local page page_file combined_file merged_file raw_code page_count api_url auth_config curl_rc
mkdir -p "$work_root"
if ! auth_config=$(write_curl_auth_config "$auth_mode" "$credential"); then
echo "Error: Could not construct Gitea authentication config; refusing request." >&2
return 1
fi
combined_file=$(mktemp "$work_root/pr-merge-commits.XXXXXX")
printf '[]' > "$combined_file"
page=1
while true; do
page_file=$(mktemp "$work_root/pr-merge-commits-page.XXXXXX")
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/commits?limit=50&page=${page}"
raw_code=$(curl -sS -K - "${GITEA_CURL_BOUNDS[@]}" -w '%{http_code}' -o "$page_file" \
-H "User-Agent: curl/8" "$api_url" <<<"$auth_config")
curl_rc=$?
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ "$curl_rc" -ne 0 ]]; then
LAST_GITEA_ERROR="curl transport failed (rc=$curl_rc)"
rm -f "$page_file" "$combined_file"
return 1
fi
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$page_file")
rm -f "$page_file" "$combined_file"
return 1
fi
if ! page_count=$(python3 - "$page_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
page = json.load(handle)
if not isinstance(page, list):
raise SystemExit(1)
print(len(page))
PY
); then
echo "Error: Gitea PR commits response is not a JSON array; refusing merge." >&2
rm -f "$page_file" "$combined_file"
return 1
fi
merged_file=$(mktemp "$work_root/pr-merge-commits-merged.XXXXXX")
if ! python3 - "$combined_file" "$page_file" > "$merged_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
combined = json.load(handle)
with open(sys.argv[2], encoding="utf-8") as handle:
page = json.load(handle)
json.dump(combined + page, sys.stdout, separators=(",", ":"))
PY
then
echo "Error: Could not combine paginated PR commit metadata; refusing merge." >&2
rm -f "$page_file" "$combined_file" "$merged_file"
return 1
fi
mv "$merged_file" "$combined_file"
rm -f "$page_file"
if [[ "$page_count" -lt 50 ]]; then
break
fi
page=$((page + 1))
if [[ "$page" -gt 1000 ]]; then
echo "Error: PR commit pagination exceeded 1000 pages; refusing merge." >&2
rm -f "$combined_file"
return 1
fi
done
cat "$combined_file"
rm -f "$combined_file"
}
# LIMITATION: author.login resolution proves the commit address maps to a registered account.
# It does NOT prove the named principal authored the commit — git author metadata is self-asserted.
# This gate checks ATTRIBUTION LINKAGE, not AUTHORSHIP. Commit signing is out of scope and unadopted.
build_coauthor_message_fields() {
local commits_file="$1" context_file="$2" head_file="$3"
python3 - "$commits_file" "$context_file" "$head_file" <<'PY'
import json
import re
import sys
commits_path, context_path, head_path = sys.argv[1:]
with open(commits_path, encoding="utf-8") as handle:
commits = json.load(handle)
head_sha = open(head_path, encoding="utf-8").read().strip()
context_parts = open(context_path, "rb").read().split(b"\0")
if len(context_parts) != 4 or context_parts[-1] != b"":
raise SystemExit(1)
poster, title, principal = (part.decode("utf-8") for part in context_parts[:3])
if not isinstance(commits, list) or not commits:
print(
f"BLOCK: provider returned no PR commits; author identity is unmeasurable. "
f"Refusing merge; escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not poster:
print(
f"BLOCK: PR poster login is empty; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not re.fullmatch(r"[0-9a-fA-F]{40}", head_sha):
print(
f"BLOCK: inspected PR head SHA is invalid; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
seen = set()
trailers = []
head_seen = False
for item in commits:
if not isinstance(item, dict):
print(f"BLOCK: malformed PR commit metadata; escalate to named principal '{principal}'.", file=sys.stderr)
raise SystemExit(75)
sha = str(item.get("sha") or "<unknown>")
if sha == head_sha:
head_seen = True
commit = item.get("commit") if isinstance(item.get("commit"), dict) else {}
commit_author = commit.get("author") if isinstance(commit.get("author"), dict) else {}
email = str(commit_author.get("email") or "").strip()
provider_author = item.get("author") if isinstance(item.get("author"), dict) else {}
login = str(provider_author.get("login") or "").strip()
if not login:
diagnostic_email = email or "<missing>"
print(
f"BLOCK: commit {sha!r} has author.login=NULL while "
f"commit.author.email={diagnostic_email!r}; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if (
not email.isascii()
or not email.isprintable()
or not re.fullmatch(r"[A-Za-z0-9_.-]+", login)
or not re.fullmatch(r"[^<>\s]+@[^<>\s]+", email)
):
print(
f"BLOCK: commit {sha!r} has unusable linked identity "
f"author.login={login!r}, commit.author.email={email!r}; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if login == poster or login in seen:
continue
seen.add(login)
trailers.append(f"Co-authored-by: {login} <{email}>")
if not head_seen:
print(
f"BLOCK: inspected PR head is absent from commit enumeration; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not trailers:
print("{}")
raise SystemExit(0)
if not title:
print(
f"BLOCK: PR title is empty; refusing merge; escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not title.isprintable() or re.match(r"^[A-Za-z-]+-[Bb]y:", title):
print(
f"BLOCK: PR title is not one printable, non-trailer line; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
print(json.dumps({
"MergeTitleField": title,
"MergeMessageField": "\n".join(trailers),
}, separators=(",", ":")))
PY
}
merge_gitea_api_attempt() {
local host="$1" auth_mode="$2" credential="$3"
local api_url attempt_dir body_file raw_code commits_file fields_file context_file head_file payload_file work_root attempt_rc auth_config curl_rc
LAST_GITEA_HTTP_CODE="000"
LAST_GITEA_ERROR=""
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/merge"
work_root="${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
mkdir -p "$work_root"
attempt_dir=$(mktemp -d "$work_root/pr-merge-attempt.XXXXXX")
chmod 0700 "$attempt_dir"
MERGE_TEMP_DIRS+=("$attempt_dir")
body_file=$(mktemp "$attempt_dir/api-response.XXXXXX")
fields_file=$(mktemp "$attempt_dir/message-fields.XXXXXX")
payload_file=$(mktemp "$attempt_dir/payload.XXXXXX")
printf '{}' > "$fields_file"
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
commits_file=$(mktemp "$attempt_dir/pr-merge-commits-input.XXXXXX")
context_file=$(mktemp "$attempt_dir/pr-merge-message-context.XXXXXX")
head_file=$(mktemp "$attempt_dir/pr-merge-head-input.XXXXXX")
printf '%s\0%s\0%s\0' "$PR_AUTHOR" "$PR_TITLE" "$ESCALATE_TO" > "$context_file"
if fetch_gitea_pr_head "$host" "$auth_mode" "$credential" "$attempt_dir" > "$head_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
if [[ "$(<"$head_file")" != "$HEAD_SHA" ]]; then
echo "BLOCK: authenticated PR head moved from reviewed $HEAD_SHA to $(<"$head_file"); refusing merge; escalate to named principal '$ESCALATE_TO'." >&2
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return 75
fi
if fetch_gitea_pr_commits "$host" "$auth_mode" "$credential" "$attempt_dir" > "$commits_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
if build_coauthor_message_fields "$commits_file" "$context_file" "$head_file" > "$fields_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
rm -f "$commits_file" "$context_file" "$head_file"
fi
if ! python3 - "$fields_file" "$HEAD_SHA" "$DELETE_BRANCH" > "$payload_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
fields = json.load(handle)
head_sha, delete_branch = sys.argv[2:]
payload = {"Do": "squash", "head_commit_id": head_sha}
if delete_branch == "true":
payload["delete_branch_after_merge"] = True
payload.update(fields)
allowed = {"Do", "head_commit_id", "delete_branch_after_merge", "MergeTitleField", "MergeMessageField"}
if payload.get("Do") != "squash" or set(payload) - allowed:
raise SystemExit(1)
print(json.dumps(payload, separators=(",", ":")))
PY
)
token=$(get_gitea_token "$host" || true)
if [[ -n "$token" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" \
-X POST \
-H "User-Agent: curl/8" \
-H "Authorization: token $token" \
-H 'Content-Type: application/json' \
-d "$payload" \
"$api_url" || true)
if [[ "$raw_code" =~ ^2 ]]; then
rm -f "$body_file"
return 0
fi
then
rm -f "$body_file" "$fields_file" "$payload_file"
return 1
fi
rm -f "$fields_file"
basic_auth=$(get_gitea_basic_auth "$host" || true)
if [[ -n "$basic_auth" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" \
-X POST \
-u "$basic_auth" \
-H "User-Agent: curl/8" \
-H 'Content-Type: application/json' \
-d "$payload" \
"$api_url" || true)
if [[ "$raw_code" =~ ^2 ]]; then
rm -f "$body_file"
return 0
fi
if ! auth_config=$(write_curl_auth_config "$auth_mode" "$credential"); then
echo "Error: Could not construct Gitea authentication config; refusing request." >&2
rm -f "$body_file" "$payload_file"
return 1
fi
raw_code=$(curl -sS -K - "${GITEA_CURL_BOUNDS[@]}" -w '%{http_code}' -o "$body_file" \
-X POST -H "User-Agent: curl/8" \
-H 'Content-Type: application/json' \
--data-binary "@$payload_file" "$api_url" <<<"$auth_config")
curl_rc=$?
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ "$curl_rc" -ne 0 ]]; then
LAST_GITEA_ERROR="curl transport failed (rc=$curl_rc)"
rm -f "$body_file" "$payload_file"
rm -rf -- "$attempt_dir"
return 1
fi
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$body_file")
fi
rm -f "$body_file" "$payload_file"
rm -rf -- "$attempt_dir"
[[ "$raw_code" =~ ^2 ]]
}
python3 - "${raw_code:-000}" "$body_file" <<'PY' >&2
import json
import sys
code, path = sys.argv[1], sys.argv[2]
try:
with open(path, encoding="utf-8", errors="replace") as handle:
raw = handle.read(500)
data = json.loads(raw) if raw else {}
message = data.get("message") or data.get("error") or raw or "empty response"
except Exception:
try:
message = open(path, encoding="utf-8", errors="replace").read(500) or "empty response"
except Exception:
message = "unreadable response"
print(f"Error: Gitea API merge failed with HTTP {code}: {message}")
PY
rm -f "$body_file"
merge_gitea_with_api() {
local host="$1" token attempt_rc
if ! token=$(get_gitea_token "$host"); then
echo "Error: Could not resolve the required Gitea token; refusing merge without changing principals." >&2
return 1
fi
if [[ -z "$token" ]]; then
echo "Error: Required Gitea token resolved empty; refusing merge without changing principals." >&2
return 1
fi
if merge_gitea_api_attempt "$host" token "$token"; then
return 0
else
attempt_rc=$?
fi
if [[ "$attempt_rc" -eq 75 ]]; then
return 75
fi
if [[ "$LAST_GITEA_HTTP_CODE" != "401" ]]; then
echo "Error: Gitea API merge failed with the identity-bound token (HTTP ${LAST_GITEA_HTTP_CODE:-000}).${LAST_GITEA_ERROR:+ Provider response: $LAST_GITEA_ERROR}" >&2
return 1
fi
echo "Error: Gitea API rejected the identity-bound token with HTTP 401; refusing cross-principal credential fallback." >&2
return 1
}
@@ -195,11 +602,10 @@ if [[ "$DRY_RUN" == true ]]; then
echo "Error: Cannot determine host from origin remote URL" >&2
exit 1
}
TEA_LOGIN="$(get_gitea_login_for_host "$HOST" || true)"
if [[ -n "$TEA_LOGIN" ]]; then
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with tea login '$TEA_LOGIN' (base=$BASE_BRANCH, method=squash)."
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
echo "Dry run: would verify PR commit authors and merge PR #$PR_NUMBER on $HOST with authenticated Gitea API message fields (base=$BASE_BRANCH, method=squash)."
else
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with authenticated Gitea API fallback (base=$BASE_BRANCH, method=squash)."
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with the authenticated exact-head Gitea API path (base=$BASE_BRANCH, method=squash)."
fi
else
echo "Dry run: would merge PR #$PR_NUMBER on $PLATFORM (base=$BASE_BRANCH, method=squash)."
@@ -209,6 +615,10 @@ fi
case "$PLATFORM" in
github)
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
echo "Error: --co-author-trailers currently requires the Gitea REST message-field contract." >&2
exit 1
fi
cmd=(gh pr merge "$PR_NUMBER" --squash --match-head-commit "$HEAD_SHA")
[[ "$DELETE_BRANCH" == true ]] && cmd+=(--delete-branch)
"${cmd[@]}"
@@ -219,7 +629,7 @@ case "$PLATFORM" in
exit 1
}
# Gitea's API head_commit_id is an atomic compare-and-merge precondition.
# tea cannot express it, so exact-head merges use the authenticated API path.
# tea cannot express it, so every Gitea merge uses the authenticated API path.
merge_gitea_with_api "$HOST"
;;
*)
@@ -0,0 +1,58 @@
#!/bin/bash
# Regression: detect_platform / get_repo_info must FAIL LOUDLY outside a git repo,
# not kill the caller silently.
#
# Both functions already contained the right error path:
# if [[ -z "$remote_url" ]]; then echo "error: not a git repository..." >&2; return 1; fi
# but under `set -e` -- which every wrapper in this directory uses -- the preceding
# assignment `remote_url=$(git remote get-url origin 2>/dev/null)` returns git's 128
# outside a repo and terminates the CALLER first. The message was unreachable.
#
# Observed cost: pr-review.sh invoked from a non-repo cwd exits 128 with NO stdout and
# NO stderr, even when -r/--repo and -H/--host are supplied -- the flags documented as
# "skips git-remote inference". Two reviewer seats hit this and correctly reported
# `blocked` with no diagnostic to report.
#
# The control that matters is the LOUD one: asserting "rc != 0" passes on the broken
# build too, because 128 is also non-zero. The test must assert the MESSAGE.
set -uo pipefail
fail=0
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
run_outside() { # $1=function name -> "rc:sawmessage"
local fn="$1" out rc
out=$( cd "$TMP" && bash -c "set -e; source '$HERE/detect-platform.sh'; $fn" 2>&1 ); rc=$?
printf '%s:%s' "$rc" "$(grep -qi 'not a git repository' <<<"$out" && echo yes || echo no)"
}
check() { if [ "$2" = "$3" ]; then echo " PASS $1 ($2)"; else echo " FAIL $1: got $2, want $3"; fail=1; fi; }
# $TMP must not be inside a git repo. Do not SKIP on failure: be-coder-07 showed the
# original SKIP exited 0, so pointing TMPDIR beneath a git worktree made this test PASS
# against unchanged main. A skip that exits 0 is indistinguishable from a pass.
# GIT_CEILING_DIRECTORIES stops git walking above $TMP, making the condition hold
# regardless of where TMPDIR lives, rather than merely detecting when it does not.
# GIT_CEILING_DIRECTORIES is matched against the PHYSICAL path -- a symlinked TMPDIR
# (/tmp is commonly one) makes the logical path never match, and the ceiling silently
# does nothing. Resolve it before exporting.
TMP="$(cd "$TMP" && pwd -P)"
export GIT_CEILING_DIRECTORIES="$TMP"
if ( cd "$TMP" && git rev-parse --git-dir >/dev/null 2>&1 ); then
echo " FAIL scratch dir is inside a git repo even with GIT_CEILING_DIRECTORIES set;"
echo " the outside-a-repo precondition cannot be established -- refusing to report a result"
exit 1
fi
echo "== outside a git repo: rc=1 AND the diagnostic is emitted =="
check "detect_platform" "$(run_outside detect_platform)" "1:yes"
check "get_repo_info" "$(run_outside get_repo_info)" "1:yes"
echo "== inside a git repo the functions still work =="
git init -q "$TMP/repo" 2>/dev/null
git -C "$TMP/repo" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git 2>/dev/null
out=$( cd "$TMP/repo" && bash -c "set -e; source '$HERE/detect-platform.sh'; detect_platform" 2>&1 ); rc=$?
if [ "$rc" -eq 0 ] && grep -qi 'gitea' <<<"$out"; then echo " PASS detect_platform in-repo (rc=0, $out)"
else echo " FAIL detect_platform in-repo: rc=$rc out=$out"; fail=1; fi
[ "$fail" -eq 0 ] && echo "OK detect-platform fails loudly outside a repo" || echo "FAILED"
exit "$fail"
@@ -0,0 +1,64 @@
#!/bin/bash
# Regression: the tea-failure diagnostic must be STATUS-NEUTRAL.
#
# Found by be-coder-08 reviewing PR #1086. At all three call sites the diagnostic is emitted
# immediately BEFORE the Gitea API fallback. Written as the last command of an && list:
# declare -F explain_... >/dev/null && explain_...
# under `set -e` a FAILING diagnostic exits and the fallback never runs -- a diagnostic that
# suppresses the recovery path it exists to explain. It misbehaves ONLY when the helper is
# PRESENT, so the helper-absent path (pre-#1086 behaviour) keeps working and reads as a
# passing control.
#
# TWO DEFECTS IN THE FIRST VERSION OF THIS TEST, both found by be-coder-08:
# 1. `out=$( ... ) 2>"$errto"` applies the redirection to the ASSIGNMENT, not to the
# command substitution, so the probe's stderr was never actually pointed at /dev/full
# and the /dev/full rows proved nothing. Verified: `out=$(echo x >&2) 2>/dev/full`
# leaks to the terminal and returns 0; the redirect must be INSIDE the substitution.
# 2. `eval "$CONSTRUCT"` changes `set -e` semantics for a bare && list, so the probe did
# not exercise the construct as the shipped file executes it. It now writes the line
# into a real script and runs it -- same parse, same set -e rules, no eval.
# The construct is still LIFTED FROM THE SHIPPED FILE: retyping the fixed form makes the
# probe pass on a build whose real call sites still carry the bare && form.
set -uo pipefail
fail=0
GIT_DIR_UNDER_TEST="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
probe() { # $1=present|absent $2=stderr target $3=source file -> "rc:fallback"
local helper="$1" errto="$2" src="$3" construct script out rc
construct=$(grep -m1 'explain_tea_user_does_not_exist' "$GIT_DIR_UNDER_TEST/$src" | sed 's/^[[:space:]]*//')
[ -n "$construct" ] || { printf 'no-construct:no'; return; }
script="$TMP/probe.sh"
{
echo '#!/bin/bash'
echo 'set -e'
echo 'explain_tea_user_does_not_exist() { echo "diagnostic" >&2; }'
[ "$helper" = absent ] && echo 'unset -f explain_tea_user_does_not_exist'
echo "$construct" # the shipped line, parsed by a real shell
echo 'echo FALLBACK_REACHED'
} > "$script"
# redirect INSIDE the substitution so the subshell's stderr really is $errto
out=$( bash "$script" 2>"$errto" ); rc=$?
printf '%s:%s' "$rc" "$(grep -q FALLBACK_REACHED <<<"$out" && echo yes || echo no)"
}
check() { if [ "$2" = "$3" ]; then echo " PASS $1 ($2)"; else echo " FAIL $1: got $2, want $3"; fail=1; fi; }
echo "== diagnostic must not alter exit status or skip the fallback =="
# /dev/full makes every stderr write fail -- the real-world shape is a closed or full fd.
for src in pr-create.sh issue-view.sh issue-create.sh; do
check "$src stderr OK / helper present" "$(probe present /dev/null "$src")" "0:yes"
check "$src stderr OK / helper absent " "$(probe absent /dev/null "$src")" "0:yes"
check "$src stderr FAILING / helper present" "$(probe present /dev/full "$src")" "0:yes"
check "$src stderr FAILING / helper absent " "$(probe absent /dev/full "$src")" "0:yes"
done
echo "== all three call sites use the status-neutral form =="
for f in pr-create.sh issue-view.sh issue-create.sh; do
p="$GIT_DIR_UNDER_TEST/$f"
grep -q '{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true' "$p" \
&& echo " PASS $f guarded" || { echo " FAIL $f: diagnostic is not status-neutral"; fail=1; }
done
[ "$fail" -eq 0 ] && echo "OK diagnostic is status-neutral" || echo "FAILED"
exit "$fail"
@@ -51,22 +51,23 @@ for arg in "$@"; do
prev=""
continue
fi
if [[ "$prev" == "-d" ]]; then
if [[ "$prev" == "data" ]]; then
post_data="$arg"
[[ "$post_data" == @* ]] && post_data=$(<"${post_data#@}")
prev=""
continue
fi
if [[ "$arg" == "-o" ]]; then
prev="-o"
if [[ "$prev" == "config" ]]; then
[[ "$arg" == "-" ]] && cat >/dev/null
prev=""
continue
fi
if [[ "$arg" == "-d" ]]; then
prev="-d"
continue
fi
if [[ "$arg" == "-w" ]]; then
write_code=true
fi
case "$arg" in
-o) prev="-o" ;;
-d|--data|--data-binary) prev="data" ;;
-K|--config) prev="config" ;;
-w) write_code=true ;;
esac
done
emit_response() {
local body="$1"
@@ -36,13 +36,30 @@ cat > "$WORK_DIR/gitea/curl" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
payload=""
for ((i=1; i<=$#; i++)); do
if [[ "${!i}" == "-d" ]]; then
j=$((i + 1))
payload="${!j}"
fi
out_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--data|--data-binary)
payload="$2"
[[ "$payload" == @* ]] && payload=$(<"${payload#@}")
shift 2
;;
-o)
out_file="$2"
shift 2
;;
-K|--config)
[[ "$2" == "-" ]] && cat >/dev/null
shift 2
;;
-w|-X|-H)
shift 2
;;
*) shift ;;
esac
done
printf '%s' "$payload" > "${MOSAIC_MERGE_PAYLOAD_LOG:?}"
[[ -n "$out_file" ]] && printf '{}' > "$out_file"
printf '200'
SH
chmod +x "$WORK_DIR/gitea/curl"
@@ -32,13 +32,16 @@ get_repo_name() { printf 'widgets\n'; }
get_remote_host() { printf 'git.example.test\n'; }
get_gitea_token() {
printf 'resolved\n' >> "${MOSAIC_TEST_TOKEN_RESOLUTION_LOG:?}"
if [[ "${MOSAIC_TEST_TOKEN_AVAILABLE:-true}" != "true" ]]; then
return 1
fi
printf 'fixture-token\n'
}
get_gitea_basic_auth() {
printf 'resolved\n' >> "${MOSAIC_TEST_BASIC_RESOLUTION_LOG:?}"
if [[ "${MOSAIC_TEST_BASIC_AVAILABLE:-false}" == "true" ]]; then
printf 'fixture-user:fixture-password\n'
return 0
return "${MOSAIC_TEST_BASIC_RC:-0}"
fi
return 1
}
@@ -52,7 +55,13 @@ if [[ "${MOSAIC_TEST_TITLE_MODE:-safe}" == "injection" ]]; then
else
title='Preserve both branch authors'
fi
printf '{"number":42,"title":"%s","author":"poster","baseRefName":"main"}\n' "$title"
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
verified) head_sha=2222222222222222222222222222222222222222 ;;
null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;;
single) head_sha=1111111111111111111111111111111111111111 ;;
*) echo "unknown commits mode" >&2; exit 2 ;;
esac
printf '{"number":42,"title":"%s","author":"poster","baseRefName":"main","headRefName":"feature/fixture","headRefOid":"%s","headRepository":"acme/widgets"}\n' "$title" "$head_sha"
SH
cat > "$case_dir/ci-queue-wait.sh" <<'SH'
@@ -90,6 +99,9 @@ out_file=""
data=""
config=""
auth_mode="none"
has_max_filesize=0
has_max_time=0
has_connect_timeout=0
while [[ $# -gt 0 ]]; do
case "$1" in
-o)
@@ -116,6 +128,18 @@ while [[ $# -gt 0 ]]; do
fi
shift 2
;;
--max-filesize)
has_max_filesize=1
shift 2
;;
--max-time)
has_max_time=1
shift 2
;;
--connect-timeout)
has_connect_timeout=1
shift 2
;;
-H|--header|-u|--user)
if [[ "$2" == *"fixture-token"* ]]; then
: > "${MOSAIC_TEST_TOKEN_ARGV_MARKER:?}"
@@ -143,6 +167,7 @@ elif [[ "$config" == *"user = \"fixture-user:fixture-password\""* ]]; then
: > "${MOSAIC_TEST_BASIC_CONFIG_MARKER:?}"
fi
printf '%s %s %s\n' "$method" "$auth_mode" "$url" >> "${MOSAIC_TEST_CURL_LOG:?}"
printf '%s:%s:%s\n' "$has_max_filesize" "$has_max_time" "$has_connect_timeout" >> "${MOSAIC_TEST_CURL_BOUNDS_LOG:?}"
case "$url" in
*/pulls/42)
@@ -152,6 +177,9 @@ case "$url" in
single) head_sha=1111111111111111111111111111111111111111 ;;
*) echo "unknown commits mode" >&2; exit 2 ;;
esac
if [[ "${MOSAIC_TEST_HEAD_MODE:-stable}" == "moved" ]]; then
head_sha=4444444444444444444444444444444444444444
fi
body="{\"head\":{\"sha\":\"$head_sha\"}}"
code=200
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then
@@ -162,7 +190,11 @@ case "$url" in
*/pulls/42/commits*)
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
verified)
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
if [[ "${MOSAIC_TEST_EMAIL_MODE:-safe}" == "escape" ]]; then
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"alice+\u001b[[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
else
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
fi
;;
null-login)
body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Unresolved Author","email":"[email protected]\n\u001b[31m"}},"author":null}]'
@@ -216,6 +248,10 @@ else
printf '%s' "$body"
fi
printf '%s' "$code"
case "${MOSAIC_TEST_CURL_FAILURE:-none}" in
oversize) exit 63 ;;
stalled) exit 28 ;;
esac
SH
chmod +x "$case_dir/detect-platform.sh" "$case_dir/pr-metadata.sh" \
@@ -228,6 +264,7 @@ run_case() {
shift 2
MOSAIC_TEST_COMMITS_MODE="$mode" \
MOSAIC_TEST_CURL_LOG="$case_dir/curl.log" \
MOSAIC_TEST_CURL_BOUNDS_LOG="$case_dir/curl-bounds.log" \
MOSAIC_TEST_MERGE_PAYLOAD="$case_dir/merge-payload.json" \
MOSAIC_TEST_TOKEN_ARGV_MARKER="$case_dir/token-in-argv" \
MOSAIC_TEST_BASIC_ARGV_MARKER="$case_dir/basic-in-argv" \
@@ -273,40 +310,98 @@ fi
if grep -q '/users/' "$verified_dir/curl.log" 2>/dev/null; then
fail "verified path performed a forbidden second /users lookup"
fi
if grep -qv '^1:1:1$' "$verified_dir/curl-bounds.log"; then
fail "verified path did not apply size/max-time/connect-time bounds to every provider download"
fi
# Token rejection during inspection must fall back to Basic Auth, then repeat
# BOTH inspection and merge with that one Basic credential handle.
# A linked email containing a terminal escape must block before mutation.
escape_email_dir=$(make_case escape-email)
set +e
escape_email_output=$(MOSAIC_TEST_EMAIL_MODE=escape run_case "$escape_email_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
escape_email_rc=$?
set -e
[[ "$escape_email_rc" -ne 0 ]] || fail "control-byte email unexpectedly passed"
[[ "$escape_email_output" == *"unusable linked identity"* ]] || fail "control-byte email refusal lost its diagnostic"
[[ ! -e "$escape_email_dir/merge-payload.json" ]] || fail "control-byte email reached the merge API"
# Curl transfer and duration failures must remain failures even with HTTP 200.
for failure_mode in oversize stalled; do
failure_dir=$(make_case "curl-$failure_mode")
set +e
failure_output=$(MOSAIC_TEST_CURL_FAILURE="$failure_mode" run_case "$failure_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
failure_rc=$?
set -e
[[ "$failure_rc" -ne 0 ]] || fail "curl $failure_mode failure was discarded: $failure_output"
[[ ! -e "$failure_dir/merge-payload.json" ]] || fail "curl $failure_mode failure reached the merge API"
done
# The authenticated head is re-read under the mutation credential but cannot
# replace the canonical preflight/review head. A move blocks before enumeration
# or mutation even though the provider returned a valid new SHA.
moved_dir=$(make_case moved-head)
set +e
moved_output=$(MOSAIC_TEST_HEAD_MODE=moved \
run_case "$moved_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
moved_rc=$?
set -e
[[ "$moved_rc" -ne 0 ]] || fail "moved authenticated head unexpectedly passed"
[[ "$moved_output" == *"authenticated PR head moved from reviewed"* ]] || fail "moved head refusal lost its diagnostic"
[[ "$moved_output" == *"tl-mosaic"* ]] || fail "moved head refusal omitted the named escalation principal"
[[ ! -e "$moved_dir/merge-payload.json" ]] || fail "moved head refusal reached the merge API"
moved_sequence=$(awk '{print $1 ":" $2}' "$moved_dir/curl.log" | paste -sd, -)
[[ "$moved_sequence" == "GET:token" ]] || fail "moved head refusal performed post-move inspection/mutation (calls=$moved_sequence)"
# Token resolution failure is not an authentication response. It must fail
# closed instead of borrowing a Basic credential under a different principal.
token_missing_dir=$(make_case token-missing)
set +e
token_missing_output=$(MOSAIC_TEST_TOKEN_AVAILABLE=false MOSAIC_TEST_BASIC_AVAILABLE=true \
run_case "$token_missing_dir" single 2>&1)
token_missing_rc=$?
set -e
[[ "$token_missing_rc" -ne 0 ]] || fail "missing token unexpectedly borrowed Basic Auth"
[[ "$token_missing_output" == *"required Gitea token"* ]] || fail "missing token refusal lost its diagnostic"
[[ ! -e "$token_missing_dir/basic-resolution.log" ]] || fail "missing token resolved Basic Auth after identity failure"
[[ ! -e "$token_missing_dir/curl.log" ]] || fail "missing token reached a provider request"
# A failed Basic resolver must never use its nonempty output or reach mutation.
basic_rc_dir=$(make_case basic-resolver-rc)
set +e
basic_rc_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_BASIC_RC=91 MOSAIC_TEST_FALLBACK_MODE=inspection \
run_case "$basic_rc_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
basic_rc_rc=$?
set -e
[[ "$basic_rc_rc" -ne 0 ]] || fail "failed Basic resolver output unexpectedly authorized a merge: $basic_rc_output"
[[ ! -e "$basic_rc_dir/merge-payload.json" ]] || fail "failed Basic resolver reached the merge API"
# HTTP 401 never changes principals: inspection rejection fails closed without
# resolving or attempting Basic Auth.
fallback_inspect_dir=$(make_case fallback-inspection)
set +e
fallback_inspect_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=inspection \
run_case "$fallback_inspect_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
fallback_inspect_rc=$?
set -e
[[ "$fallback_inspect_rc" -eq 0 ]] || fail "inspection fallback expected rc=0, got rc=$fallback_inspect_rc: $fallback_inspect_output"
[[ -s "$fallback_inspect_dir/merge-payload.json" ]] || fail "inspection fallback did not reach the merge API"
[[ -e "$fallback_inspect_dir/basic-via-config" ]] || fail "inspection fallback did not transport Basic Auth through stdin config"
[[ ! -e "$fallback_inspect_dir/basic-in-argv" ]] || fail "inspection fallback exposed Basic Auth in curl argv"
[[ ! -e "$fallback_inspect_dir/metadata-in-argv" ]] || fail "inspection fallback exposed PR metadata in child argv"
[[ "$(wc -l < "$fallback_inspect_dir/token-resolution.log")" -eq 1 ]] || fail "inspection fallback did not resolve token exactly once"
[[ "$(wc -l < "$fallback_inspect_dir/basic-resolution.log")" -eq 1 ]] || fail "inspection fallback did not resolve Basic Auth exactly once"
[[ "$fallback_inspect_rc" -ne 0 ]] || fail "inspection token rejection unexpectedly changed principals"
[[ "$fallback_inspect_output" == *"refusing cross-principal credential fallback"* ]] || fail "inspection token rejection lost its refusal diagnostic"
[[ ! -e "$fallback_inspect_dir/basic-resolution.log" ]] || fail "inspection token rejection resolved Basic Auth"
[[ ! -e "$fallback_inspect_dir/merge-payload.json" ]] || fail "inspection token rejection reached merge mutation"
inspect_sequence=$(awk '{print $1 ":" $2}' "$fallback_inspect_dir/curl.log" | paste -sd, -)
[[ "$inspect_sequence" == "GET:token,GET:basic,GET:basic,POST:basic" ]] || fail "inspection fallback was not bound per credential (calls=$inspect_sequence)"
[[ "$inspect_sequence" == "GET:token" ]] || fail "inspection rejection made unexpected provider calls (calls=$inspect_sequence)"
# Token rejection at merge is a separate seam: Basic fallback must re-inspect
# instead of reusing evidence gathered under the rejected token.
# Token rejection at merge likewise fails closed without cross-principal retry.
fallback_merge_dir=$(make_case fallback-merge)
set +e
fallback_merge_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=merge \
run_case "$fallback_merge_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
fallback_merge_rc=$?
set -e
[[ "$fallback_merge_rc" -eq 0 ]] || fail "merge fallback expected rc=0, got rc=$fallback_merge_rc: $fallback_merge_output"
[[ -s "$fallback_merge_dir/merge-payload.json" ]] || fail "merge fallback did not reach a successful merge API payload"
[[ -e "$fallback_merge_dir/basic-via-config" ]] || fail "merge fallback did not transport Basic Auth through stdin config"
[[ ! -e "$fallback_merge_dir/basic-in-argv" ]] || fail "merge fallback exposed Basic Auth in curl argv"
[[ ! -e "$fallback_merge_dir/metadata-in-argv" ]] || fail "merge fallback exposed PR metadata in child argv"
[[ "$fallback_merge_rc" -ne 0 ]] || fail "merge token rejection unexpectedly changed principals"
[[ "$fallback_merge_output" == *"refusing cross-principal credential fallback"* ]] || fail "merge token rejection lost its refusal diagnostic"
[[ ! -e "$fallback_merge_dir/basic-resolution.log" ]] || fail "merge token rejection resolved Basic Auth"
[[ ! -e "$fallback_merge_dir/merge-payload.json" ]] || fail "merge token rejection recorded a successful payload"
merge_sequence=$(awk '{print $1 ":" $2}' "$fallback_merge_dir/curl.log" | paste -sd, -)
[[ "$merge_sequence" == "GET:token,GET:token,POST:token,GET:basic,GET:basic,POST:basic" ]] || fail "merge fallback reused cross-credential evidence (calls=$merge_sequence)"
[[ "$merge_sequence" == "GET:token,GET:token,POST:token" ]] || fail "merge rejection made unexpected provider calls (calls=$merge_sequence)"
# BLOCK path: a commit email exists but author.login is null. It must name both
# facts, name the escalation principal, and never reach the merge endpoint.
@@ -364,6 +459,7 @@ set -e
[[ "$error_output" == *"branch policy rejected"* ]] || fail "provider error response was discarded"
[[ "$error_output" == *'\n\x1b[31m'* ]] || fail "provider error response did not escape control characters"
[[ "$error_output" != *$'\033'* ]] || fail "provider error response emitted a raw terminal escape"
[[ "$error_output" != *"Basic Auth fallback"* ]] || fail "provider error advertised removed Basic Auth fallback"
[[ ! -e "$error_dir/basic-resolution.log" ]] || fail "HTTP 409 policy denial incorrectly triggered Basic Auth fallback"
# Authorization denials likewise fail closed instead of changing principals.
@@ -375,6 +471,7 @@ forbidden_rc=$?
set -e
[[ "$forbidden_rc" -ne 0 ]] || fail "HTTP 403 authorization denial unexpectedly passed"
[[ "$forbidden_output" == *"HTTP 403"* ]] || fail "authorization denial omitted the HTTP status"
[[ "$forbidden_output" != *"Basic Auth fallback"* ]] || fail "authorization denial advertised removed Basic Auth fallback"
[[ ! -e "$forbidden_dir/basic-resolution.log" ]] || fail "HTTP 403 authorization denial incorrectly triggered Basic Auth fallback"
# The BLOCK destination cannot be generic or inferred after failure: opting in
@@ -416,7 +513,10 @@ else
import json
import sys
payload = json.load(open(sys.argv[1], encoding="utf-8"))
assert payload == {"Do": "squash"}, payload
assert payload == {
"Do": "squash",
"head_commit_id": "1111111111111111111111111111111111111111",
}, payload
PY
fi
[[ -e "$single_dir/auth-via-config" ]] || fail "ordinary path did not authenticate curl through stdin config"
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
},
"dependencies": {
"@mosaicstack/brain": "workspace:*",