|
| 1 | +#!/usr/bin/env bash |
| 2 | +# share-filter.sh - stream filter for `opencode run --share` invocations. |
| 3 | +# |
| 4 | +# Captures the session share link (https://opncd.ai/share/...) from the |
| 5 | +# output stream WITHOUT letting the raw URL reach the public Actions log, |
| 6 | +# then re-publishes it RSA-OAEP-encrypted so an admin holding the private |
| 7 | +# key can recover it (decrypt_share_link.py in the repo root). |
| 8 | +# |
| 9 | +# Architecture note: the awk stage NEVER spawns subprocesses (it only |
| 10 | +# captures/masks/writes files) - encryption happens in this bash script |
| 11 | +# after the stream ends. Spawning openssl from inside awk is fragile on |
| 12 | +# MSYS/Windows (argument conversion mangles "-pkeyopt name:value") and |
| 13 | +# gains nothing: the encrypted block lands at the end of the stream, |
| 14 | +# plus a ::notice:: annotation and the step-summary files. |
| 15 | +# |
| 16 | +# Usage: |
| 17 | +# opencode run --share - < prompt.txt | bash /tmp/share-filter.sh |
| 18 | +# (the calling step must set `set -o pipefail` to preserve the exit code) |
| 19 | +# |
| 20 | +# Env: |
| 21 | +# SHARE_LINK_PUBKEY PEM public key (repo secret SHARE_LINK_PUBKEY). |
| 22 | +# Optional - when unset/invalid the URL is still |
| 23 | +# captured and masked, just not encrypted. |
| 24 | +# SHARE_CTX_THREAD e.g. "PR #162" / "Issue #9" (optional) |
| 25 | +# SHARE_CTX_HEAD commit SHA this run reviewed (optional) |
| 26 | +# SHARE_CTX_DETAIL e.g. "FOLLOW-UP (last reviewed abc..)" (optional) |
| 27 | +# GITHUB_REPOSITORY / GITHUB_RUN_ID / GITHUB_ACTOR - default CI env. |
| 28 | +# |
| 29 | +# Outputs: |
| 30 | +# stdout: passthrough log; the link line is replaced by a capture notice, |
| 31 | +# the encrypted block is emitted at end of stream. |
| 32 | +# ::add-mask::<url> prevents any later accidental re-echo |
| 33 | +# ::notice:: prominent annotation carrying the blob |
| 34 | +# $TMP/share-link.enc / $TMP/share-link.ctx - for the step summary |
| 35 | +set -euo pipefail |
| 36 | + |
| 37 | +TMP="${RUNNER_TEMP:-/tmp}" |
| 38 | +ENC_OUT="$TMP/share-link.enc" |
| 39 | +CTX_OUT="$TMP/share-link.ctx" |
| 40 | +URL_OUT="$TMP/share-link.url" |
| 41 | + |
| 42 | +# ---- stage 1: stream filter (pure awk, no subprocesses) ------------------- |
| 43 | +awk -v url_out="$URL_OUT" -v ctx_out="$CTX_OUT" \ |
| 44 | + -v repo="${GITHUB_REPOSITORY:-unknown}" -v run="${GITHUB_RUN_ID:-unknown}" \ |
| 45 | + -v actor="${GITHUB_ACTOR:-unknown}" \ |
| 46 | + -v thread="${SHARE_CTX_THREAD:-}" -v head="${SHARE_CTX_HEAD:-}" \ |
| 47 | + -v detail="${SHARE_CTX_DETAIL:-}" ' |
| 48 | +BEGIN { captured = 0 } |
| 49 | +{ |
| 50 | + if ($0 ~ /opncd\.ai\/share\//) { |
| 51 | + line = $0 |
| 52 | + esc = sprintf("%c", 27) |
| 53 | + gsub(esc "\\[[0-9;]*m", "", line) # strip ANSI color codes |
| 54 | + if (match(line, /https:\/\/opncd\.ai\/share\/[A-Za-z0-9_-]+/)) { |
| 55 | + url = substr(line, RSTART, RLENGTH) |
| 56 | + printf "::add-mask::%s\n", url |
| 57 | + if (captured++) { |
| 58 | + print "~ [share link repeated - already captured above]" |
| 59 | + next |
| 60 | + } |
| 61 | + print "~ [share link captured - encrypted block at end of stream]" |
| 62 | + printf "%s\n", url > url_out |
| 63 | + ctx = "context: " repo |
| 64 | + if (thread != "") ctx = ctx " | " thread |
| 65 | + if (head != "") ctx = ctx " | head " head |
| 66 | + if (detail != "") ctx = ctx " | " detail |
| 67 | + ctx = ctx " | run " run " | by " actor |
| 68 | + printf "%s\n", ctx > ctx_out |
| 69 | + next |
| 70 | + } |
| 71 | + } |
| 72 | + print |
| 73 | +} |
| 74 | +' |
| 75 | + |
| 76 | +# ---- stage 2: encrypt (bash-side, retry-tolerant) ------------------------- |
| 77 | +if [ ! -s "$URL_OUT" ]; then |
| 78 | + exit 0 # nothing captured |
| 79 | +fi |
| 80 | + |
| 81 | +CTX="$(cat "$CTX_OUT" 2>/dev/null || true)" |
| 82 | + |
| 83 | +PUBKEY_FILE="" |
| 84 | +if [ -n "${SHARE_LINK_PUBKEY:-}" ]; then |
| 85 | + PUBKEY_FILE="$TMP/share-link.pub.pem" |
| 86 | + printf '%s\n' "$SHARE_LINK_PUBKEY" > "$PUBKEY_FILE" |
| 87 | +fi |
| 88 | + |
| 89 | +encrypt_url() { # $1=url $2=pubkey-file -> base64 blob on stdout |
| 90 | + printf '%s' "$1" \ |
| 91 | + | openssl pkeyutl -encrypt -pubin -inkey "$2" \ |
| 92 | + -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \ |
| 93 | + 2>/dev/null \ |
| 94 | + | openssl base64 -A |
| 95 | + echo # terminate for $( ) |
| 96 | +} |
| 97 | + |
| 98 | +URL="$(cat "$URL_OUT")" |
| 99 | +BLOB="" |
| 100 | +if [ -n "$PUBKEY_FILE" ]; then |
| 101 | + # retry loop: fresh files can be briefly unreadable (AV scanners, slow |
| 102 | + # network filesystems) - up to ~3s total |
| 103 | + for _ in 1 2 3 4 5 6 7 8 9 10; do |
| 104 | + BLOB="$(encrypt_url "$URL" "$PUBKEY_FILE")" |
| 105 | + [ -n "$BLOB" ] && break |
| 106 | + sleep 0.3 |
| 107 | + done |
| 108 | +fi |
| 109 | + |
| 110 | +if [ -n "$BLOB" ]; then |
| 111 | + printf '~ [share link encrypted] MRB1.%s\n' "$BLOB" |
| 112 | + printf '%s\n' "$CTX" |
| 113 | + printf '::notice::title=Mirrobot share link (encrypted)::MRB1.%s\n' "$BLOB" |
| 114 | + printf 'MRB1.%s\n' "$BLOB" > "$ENC_OUT" |
| 115 | +else |
| 116 | + echo "~ [share link captured - NOT encrypted: no valid SHARE_LINK_PUBKEY secret]" |
| 117 | + printf '%s\n' "$CTX" |
| 118 | + echo "::notice::title=Mirrobot share link::captured and masked; set the SHARE_LINK_PUBKEY secret to enable encryption" |
| 119 | + printf '(masked)\n' > "$ENC_OUT" |
| 120 | +fi |
| 121 | +printf '%s\n' "$CTX" > "$CTX_OUT" |
| 122 | +rm -f "$URL_OUT" |
0 commit comments