link-policy.sh
link-policy.shBrowse 3 files
3,578 tokens
13,512 bytes
Token encoding: o200k_base
Snapshot 6f118af
← Back to SKILL.md
1#!/usr/bin/env bash2# Shared, ownership-safe skill-link policy for the Claude and Codex wrappers.3#4# This file is bundled inside the signed cmux-cua resource and sourced by both5# wrappers. It deliberately has no launch side effects until6# cmux_cua_skill_reconcile is called by a cmux-owned session.7 8 9cmux_cua_skill_readlink() {10 /usr/bin/readlink "$1" 2>/dev/null || /bin/readlink "$1" 2>/dev/null11}12 13cmux_cua_skill_lexical_normalize() {14 local path="$1"15 [[ "$path" = /* ]] || return 116 local normalized="/"17 local remaining="${path#/}"18 local component19 while [[ -n "$remaining" ]]; do20 if [[ "$remaining" == */* ]]; then21 component="${remaining%%/*}"22 remaining="${remaining#*/}"23 else24 component="$remaining"25 remaining=""26 fi27 case "$component" in28 ""|.) continue ;;29 ..)30 if [[ "$normalized" != "/" ]]; then31 normalized="${normalized%/*}"32 [[ -n "$normalized" ]] || normalized="/"33 fi34 ;;35 *)36 if [[ "$normalized" == "/" ]]; then37 normalized="/$component"38 else39 normalized="$normalized/$component"40 fi41 ;;42 esac43 done44 printf '%s' "$normalized"45}46 47cmux_cua_skill_resolve_link_target() {48 local link="$1"49 local target="$2"50 local resolved51 if [[ "$target" = /* ]]; then52 resolved="$target"53 else54 resolved="${link%/*}/$target"55 fi56 cmux_cua_skill_lexical_normalize "$resolved"57}58 59cmux_cua_skill_canonical_document() {60 local path="$1"61 [[ "$path" = /* ]] || return 162 if [[ -f "$path" ]]; then63 local parent="${path%/*}"64 local basename="${path##*/}"65 local canonical_parent66 canonical_parent="$(cd "$parent" 2>/dev/null && pwd -P)" || canonical_parent=""67 if [[ -n "$canonical_parent" ]]; then68 printf '%s/%s' "$canonical_parent" "$basename"69 return 070 fi71 fi72 cmux_cua_skill_lexical_normalize "$path"73}74 75cmux_cua_skill_frontmatter_name() {76 local file="$1"77 [[ -f "$file" ]] || return 178 local awk_bin=/usr/bin/awk79 [[ -x "$awk_bin" ]] || awk_bin=awk80 "$awk_bin" '81 NR == 1 && $0 ~ /^---[[:space:]]*$/ { in_frontmatter = 1; next }82 in_frontmatter && $0 ~ /^---[[:space:]]*$/ { exit }83 in_frontmatter && $0 ~ /^name:[[:space:]]*/ {84 value = $085 sub(/^name:[[:space:]]*/, "", value)86 gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)87 print value88 exit89 }90 ' "$file" 2>/dev/null91}92 93cmux_cua_skill_name_is() {94 local file="$1"95 local expected="$2"96 local value97 value="$(cmux_cua_skill_frontmatter_name "$file")" || return 198 value="${value#\"}"99 value="${value%\"}"100 value="${value#\'}"101 value="${value%\'}"102 [[ "$value" == "$expected" ]]103}104 105cmux_cua_skill_target_is_managed() {106 # A resource suffix, app name, or dangling path alone is not ownership107 # proof. A link is managed only when its existing bundle has cmux's bundle108 # identifier, lives in a known cmux install/build root, and is owned by109 # root or the current user. Removed bundles have no verifiable owner or110 # identity and are deliberately preserved for manual review.111 local target="$1"112 local resource_name113 case "$target" in114 */Contents/Resources/cmux-cua) resource_name=cmux-cua ;;115 */Contents/Resources/cmux-computer-use) resource_name=cmux-computer-use ;;116 */Contents/Resources/codex-cua) resource_name=codex-cua ;;117 *) return 1 ;;118 esac119 120 local app_bundle="${target%/Contents/Resources/$resource_name}"121 [[ "$app_bundle" == *.app ]] || return 1122 local home="${HOME:-}"123 local app_owner124 app_owner="$(/usr/bin/stat -f '%u' "$app_bundle" 2>/dev/null || true)"125 [[ "$app_owner" == "0" || "$app_owner" == "$(/usr/bin/id -u)" ]] || return 1126 127 case "$app_bundle" in128 /Applications/cmux*.app|/Applications/Utilities/cmux*.app|\129 "$home/Applications"/cmux*.app|\130 "$home/Library/Developer/Xcode/DerivedData"/cmux-*/Build/Products/*/cmux*.app|\131 /tmp/cmux-*/Build/Products/*/cmux*.app)132 ;;133 *)134 return 1135 ;;136 esac137 138 local info_plist="$app_bundle/Contents/Info.plist"139 if [[ -f "$info_plist" && -x /usr/libexec/PlistBuddy ]]; then140 local bundle_id141 bundle_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$info_plist" 2>/dev/null)" || return 1142 [[ "$bundle_id" == com.cmuxterm.* ]]143 return $?144 fi145 return 1146}147 148cmux_cua_skill_link_is_managed() {149 local link="$1"150 [[ -L "$link" ]] || return 1151 local target resolved152 target="$(cmux_cua_skill_readlink "$link")" || return 1153 [[ -n "$target" ]] || return 1154 resolved="$(cmux_cua_skill_resolve_link_target "$link" "$target")" || resolved="$target"155 cmux_cua_skill_target_is_managed "$resolved" || cmux_cua_skill_target_is_managed "$target"156}157 158cmux_cua_skill_remove_managed_link() {159 local link="$1"160 if cmux_cua_skill_link_is_managed "$link"; then161 # rm acts on the link itself because the target was validated and the162 # operand is an absolute path. Never recurse or follow the target.163 /bin/rm -f "$link" 2>/dev/null || true164 if [[ "${CMUX_CUA_DIAGNOSTICS:-0}" == 1 && ! -L "$link" ]]; then165 printf 'cmux-cua: managed-link-retired path=%q\n' "$link" >&2166 fi167 return 0168 fi169 return 1170}171 172cmux_cua_skill_link_state() {173 local link="$1"174 if cmux_cua_skill_link_is_managed "$link"; then175 printf '%s' managed176 elif [[ -L "$link" || -e "$link" ]]; then177 printf '%s' user178 else179 printf '%s' missing180 fi181}182 183cmux_cua_skill_project_candidate_is_collision() {184 local candidate="$1"185 local source_document="$2"186 [[ -f "$candidate" ]] || return 1187 cmux_cua_skill_name_is "$candidate" cmux-cua || return 1188 local candidate_canonical source_canonical189 candidate_canonical="$(cmux_cua_skill_canonical_document "$candidate")" || return 1190 source_canonical="$(cmux_cua_skill_canonical_document "$source_document")" || source_canonical="$source_document"191 [[ "$candidate_canonical" != "$source_canonical" ]]192}193 194cmux_cua_skill_document_is_user_global() {195 local candidate="$1"196 local provider="$2"197 local home="${HOME:-}"198 local codex_home="${CODEX_HOME:-$home/.codex}"199 local candidate_normalized200 candidate_normalized="$(cmux_cua_skill_lexical_normalize "$candidate" 2>/dev/null)" \201 || candidate_normalized="$candidate"202 local candidate_root_canonical=""203 local candidate_root="${candidate%/cmux-cua/SKILL.md}"204 if [[ -d "$candidate_root" ]]; then205 candidate_root_canonical="$(cd "$candidate_root" 2>/dev/null && pwd -P)"206 fi207 208 local -a global_roots209 if [[ "$provider" == codex ]]; then210 global_roots=("$home/.agents/skills" "$codex_home/skills")211 else212 global_roots=("$home/.claude/skills")213 fi214 215 local global_root global_normalized216 for global_root in "${global_roots[@]}"; do217 [[ "$global_root" = /* ]] || continue218 global_normalized="$(cmux_cua_skill_lexical_normalize "$global_root" 2>/dev/null)" \219 || continue220 if [[ "$candidate_normalized" == "$global_normalized/cmux-cua/SKILL.md" ]]; then221 return 0222 fi223 local global_root_canonical=""224 if [[ -d "$global_root" ]]; then225 global_root_canonical="$(cd "$global_root" 2>/dev/null && pwd -P)"226 fi227 [[ -n "$candidate_root_canonical" \228 && "$candidate_root_canonical" == "$global_root_canonical" ]] \229 && return 0230 done231 return 1232}233 234cmux_cua_skill_project_has_collision() {235 local cwd="$1"236 local provider="$2"237 local source_document="$3"238 [[ "$cwd" = /* ]] || return 1239 local dir240 dir="$(cd "$cwd" 2>/dev/null && pwd -P)" || return 1241 local depth=0242 local candidate243 while [[ "$dir" = /* && $depth -lt 64 ]]; do244 # User-global roots are not project roots. Compare the lexical path245 # before following a candidate symlink so a global link cannot be246 # mistaken for a project collision while walking `~/projects/foo` (or247 # a custom CODEX_HOME nested under another directory).248 if [[ "$provider" == codex ]]; then249 local -a candidates=(250 "$dir/.agents/skills/cmux-cua/SKILL.md"251 "$dir/.codex/skills/cmux-cua/SKILL.md"252 )253 for candidate in "${candidates[@]}"; do254 cmux_cua_skill_document_is_user_global "$candidate" "$provider" && continue255 if cmux_cua_skill_project_candidate_is_collision "$candidate" "$source_document"; then256 return 0257 fi258 done259 else260 candidate="$dir/.claude/skills/cmux-cua/SKILL.md"261 if ! cmux_cua_skill_document_is_user_global "$candidate" "$provider" \262 && cmux_cua_skill_project_candidate_is_collision "$candidate" "$source_document"; then263 return 0264 fi265 fi266 267 # Match Codex's project-root boundary: once a repository marker is268 # found, do not inspect unrelated parent checkouts.269 [[ -e "$dir/.git" ]] && break270 [[ "$dir" == "/" ]] && break271 dir="${dir%/*}"272 [[ -n "$dir" ]] || dir=/273 depth=$((depth + 1))274 done275 return 1276}277 278cmux_cua_skill_remove_legacy_links() {279 local home="$1"280 local provider="$2"281 local codex_home="${CODEX_HOME:-$home/.codex}"282 local root link name283 if [[ "$provider" == codex ]]; then284 for root in "$home/.agents/skills" "$codex_home/skills"; do285 [[ "$root" = /* ]] || continue286 for name in cmux-computer-use codex-cua; do287 link="$root/$name"288 cmux_cua_skill_remove_managed_link "$link" || true289 done290 done291 else292 # Older Claude wrappers wrote the legacy name into Codex's shared root.293 for root in "$home/.claude/skills" "$home/.agents/skills"; do294 [[ "$root" = /* ]] || continue295 for name in cmux-computer-use codex-cua; do296 link="$root/$name"297 cmux_cua_skill_remove_managed_link "$link" || true298 done299 done300 fi301}302 303cmux_cua_skill_reconcile() {304 local provider="$1"305 local source_dir="$2"306 local cwd="$3"307 local install_requested="$4"308 local home="${HOME:-}"309 [[ "$home" = /* && "$source_dir" = /* ]] || return 1310 local source_document="$source_dir/SKILL.md"311 [[ -f "$source_document" ]] || return 1312 313 local project_collision=0314 if [[ "$install_requested" == 1 ]] \315 && cmux_cua_skill_project_has_collision "$cwd" "$provider" "$source_document"; then316 project_collision=1317 fi318 cmux_cua_skill_remove_legacy_links "$home" "$provider"319 320 local skills_root destination state legacy_root legacy_state321 if [[ "$provider" == codex ]]; then322 skills_root="$home/.agents/skills"323 legacy_root="${CODEX_HOME:-$home/.codex}/skills"324 else325 skills_root="$home/.claude/skills"326 legacy_root=""327 fi328 destination="$skills_root/cmux-cua"329 state="$(cmux_cua_skill_link_state "$destination")"330 local user_path_collision=0331 local blocked_path=""332 if [[ "$state" == user ]]; then333 user_path_collision=1334 blocked_path="$destination"335 fi336 if [[ -n "$legacy_root" && "$legacy_root" != "$skills_root" ]]; then337 legacy_state="$(cmux_cua_skill_link_state "$legacy_root/cmux-cua")"338 if [[ "$legacy_state" == user ]]; then339 user_path_collision=1340 [[ -n "$blocked_path" ]] || blocked_path="$legacy_root/cmux-cua"341 fi342 else343 legacy_state=missing344 fi345 346 if [[ "$install_requested" == 1 && "$user_path_collision" == 1 \347 && "${CMUX_CUA_DIAGNOSTICS:-0}" == 1 ]]; then348 printf 'cmux-cua: skill-install=blocked-user-path path=%q\n' "$blocked_path" >&2349 fi350 if [[ "$install_requested" == 1 && "$project_collision" == 1 \351 && "${CMUX_CUA_DIAGNOSTICS:-0}" == 1 ]]; then352 printf 'cmux-cua: skill-install=blocked-project-collision path=%q\n' "$cwd" >&2353 fi354 355 if [[ "$install_requested" == 1 \356 && "$project_collision" != 1 \357 && "$user_path_collision" != 1 ]]; then358 /bin/mkdir -p "$skills_root" 2>/dev/null || true359 if [[ "$state" == missing ]]; then360 /bin/ln -s "$source_dir" "$destination" 2>/dev/null || true361 elif [[ "$state" == managed ]]; then362 /bin/ln -sfn "$source_dir" "$destination" 2>/dev/null || true363 fi364 elif [[ "$state" == managed ]]; then365 # This is a per-launch preference, not an install receipt. Without366 # opt-in (or beside a project collision), retire only verified367 # app-managed links. Normal user-installed skills are never removed.368 cmux_cua_skill_remove_managed_link "$destination" || true369 fi370 # The deprecated root must not contribute a second managed discovery path,371 # including when the canonical destination has never been installed.372 if [[ "$legacy_state" == managed ]]; then373 cmux_cua_skill_remove_managed_link "$legacy_root/cmux-cua" || true374 fi375 return 0376}377 