mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
The traceback test located the path by its first `/`. A Windows temp path keeps the forward slash it was built with, so the search landed three quarters of the way along the path and the expected span was 63 columns off. Look for the whole path instead. The detection itself was right all along; only the expectation was wrong, which is why the Windows job was already red before the review fixes landed. The executable check reads the local filesystem from `ui::`, which the host boundary forbids on sight. It is only reachable once `host_id.is_local()` has answered, so it goes on the allowlist with that as its reason. Claude-Session: https://claude.ai/code/session_01NE3M5Q94Jyxmj5Rdm9bcg4
186 lines
7.5 KiB
Bash
Executable File
186 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# The GUI must not touch the filesystem or git directly.
|
|
#
|
|
# Once a workspace can live on a remote machine, a path held by `ui::` or
|
|
# `terminal::` is not necessarily a path on *this* box, and `std::path`'s
|
|
# fs-backed APIs quietly answer for the wrong machine:
|
|
# `canonicalize` walks the local filesystem, `is_absolute` says `false` for
|
|
# `/home/me` on Windows, `read_dir` lists the client's disk. Everything that may
|
|
# be looking at a workspace path has to go through `ui::host_ops` / the `Host`
|
|
# trait, which routes to the local disk or the far side as appropriate.
|
|
#
|
|
# This script enforces that. It is deliberately *not* the raw grep from the
|
|
# contract: run bare, that grep reports 42 hits on a clean tree — not one of them
|
|
# git, all of them modules whose paths are local by construction — and a guard
|
|
# that always fails is a guard everyone learns to ignore. Two things fix it:
|
|
#
|
|
# 1. Test bodies are cut off properly. The contract's `grep -v '#\[cfg(test)\]'`
|
|
# only removes the attribute line itself, leaving the whole test module
|
|
# behind it in scope; here the scan stops at the `#[cfg(test)] mod …` that
|
|
# opens the trailing test region.
|
|
# 2. An explicit allowlist, keyed on (file, pattern) rather than whole files, so
|
|
# a module exempted for its `.is_absolute()` still trips on a new
|
|
# `std::fs::`. Every entry carries the reason its paths cannot be remote.
|
|
#
|
|
# Adding an entry is a deliberate act: if the path could ever be a workspace
|
|
# path, the answer is `Host`, not an allowlist line.
|
|
#
|
|
# Usage: bash .github/scripts/check-host-boundary.sh
|
|
# Exit 0 = clean, 1 = violations (printed to stderr).
|
|
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/../.."
|
|
|
|
ROOTS=(src/ui src/terminal)
|
|
|
|
# The forbidden constructs, as extended-regex alternatives.
|
|
PATTERN='std::fs::|Command::new\("git"\)|\.canonicalize\(\)|\.is_absolute\(\)'
|
|
|
|
# Allowlist entries are `<path>|<literal pattern>`, one per line. `<path>|*`
|
|
# exempts a file wholesale (used only for the boundary module itself). The
|
|
# pattern side is matched as a plain substring of the offending line.
|
|
ALLOW=$(
|
|
cat <<'EOF'
|
|
# The host boundary itself: every routed filesystem call lands here by design.
|
|
src/ui/host_ops.rs|*
|
|
|
|
# Themes and presets are app-owned files under the local config dir
|
|
# (`themes_dir()`), never a workspace path — a remote workspace does not carry
|
|
# the user's color schemes with it.
|
|
src/ui/presets.rs|std::fs::
|
|
src/ui/presets.rs|.is_absolute()
|
|
src/ui/app.rs|std::fs::create_dir_all
|
|
|
|
# Reading a private key off *this* machine to hash it into a keychain account
|
|
# (`core::keychain`). The key is the client's credential; the far side never
|
|
# sees the file, only the resulting auth.
|
|
src/ui/ssh_prompt.rs|std::fs::read
|
|
src/ui/ssh_connect.rs|std::fs::read
|
|
src/ui/settings.rs|std::fs::read
|
|
|
|
# Shell history lives in the local user's home (`~/.zsh_history` &co.) and backs
|
|
# this app's own history search. A remote pane's history is the remote shell's
|
|
# business, read over the wire, not through here.
|
|
src/terminal/history.rs|std::fs::
|
|
|
|
# Completion is already remote-aware: a remote pane is signalled by `cwd: None`,
|
|
# which disables exactly these local-filesystem candidate sources in favour of
|
|
# `remote_path_request` / `remote_path_candidates`. The `$PATH` scan is likewise
|
|
# this machine's `$PATH`, deliberately withheld from remote panes.
|
|
src/terminal/completion.rs|std::fs::read_dir
|
|
src/terminal/completion.rs|.is_absolute()
|
|
|
|
# Bundled completion specs shipped in `assets/completions`, resolved from the
|
|
# app bundle / dev manifest dir. Program data, not user or workspace data.
|
|
src/terminal/signature.rs|std::fs::read_to_string
|
|
|
|
# Opening a path scraped out of terminal output resolves it against the local
|
|
# cwd — a local-pane affordance; `resolve_existing_path` declines when there is
|
|
# no local cwd.
|
|
src/terminal/search.rs|.is_absolute()
|
|
|
|
# Clipboard-image paste stages the bytes into the OS temp dir so an agent TUI
|
|
# can be handed a path. Always `std::env::temp_dir()` on this machine.
|
|
src/terminal/view.rs|std::fs::create_dir_all
|
|
src/terminal/view.rs|std::fs::write
|
|
|
|
# Asking whether a file would be *launched* rather than shown before handing it
|
|
# to the desktop opener. Only reachable behind `host_id.is_local()` — a path on
|
|
# another machine takes the earlier arm and never gets here.
|
|
src/ui/code_editor.rs|std::fs::metadata
|
|
|
|
# The source side of a file drop. What the desktop hands over is by
|
|
# construction a path on the desktop's own machine, so reading it is a local
|
|
# read even when the tree being dropped on is remote — the destination side of
|
|
# that copy goes through `Host`, and the one `std::fs::copy` that touches a
|
|
# destination sits inside a branch already gated on `host.id().is_local()`.
|
|
src/ui/file_copy.rs|std::fs::
|
|
EOF
|
|
)
|
|
|
|
# Where the trailing `#[cfg(test)] mod …` starts, if any. Line numbers are
|
|
# preserved because only the tail is dropped.
|
|
body_of() {
|
|
local file=$1 cut
|
|
# `attr` starts unset, which awk reads as 0 — so a file whose *first* line
|
|
# is `mod something` used to match `attr == NR - 1` and cut the body at
|
|
# line 0, leaving `head -n -1` to error out and the file to be scanned as
|
|
# empty. Two files opened that way, and the guard was blind to both.
|
|
cut=$(awk '
|
|
BEGIN { attr = -1 }
|
|
/^#\[cfg\(test\)\]$/ { attr = NR }
|
|
/^mod [A-Za-z_]/ { if (attr == NR - 1) { print NR - 1; exit } }
|
|
' "$file")
|
|
if [ -n "$cut" ]; then
|
|
head -n "$((cut - 1))" "$file"
|
|
else
|
|
cat "$file"
|
|
fi
|
|
}
|
|
|
|
allowed() {
|
|
local file=$1 line=$2 entry path pat
|
|
while IFS= read -r entry; do
|
|
case "$entry" in '' | '#'*) continue ;; esac
|
|
path=${entry%%|*}
|
|
pat=${entry#*|}
|
|
[ "$path" = "$file" ] || continue
|
|
if [ "$pat" = '*' ] || [ "${line#*"$pat"}" != "$line" ]; then
|
|
return 0
|
|
fi
|
|
done <<<"$ALLOW"
|
|
return 1
|
|
}
|
|
|
|
# A guard that scans nothing reports success, which is the one failure mode worse
|
|
# than a noisy guard. Fail loudly if a root has moved out from under us.
|
|
for root in "${ROOTS[@]}"; do
|
|
if [ ! -d "$root" ]; then
|
|
echo "check-host-boundary: scan root '$root' does not exist (moved? renamed?)" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
violations=0
|
|
scanned=0
|
|
while IFS= read -r file; do
|
|
scanned=$((scanned + 1))
|
|
while IFS= read -r hit; do
|
|
lineno=${hit%%:*}
|
|
text=${hit#*:}
|
|
# Prose, not code.
|
|
case "$(printf '%s' "$text" | sed 's/^[[:space:]]*//')" in '//'*) continue ;; esac
|
|
if allowed "$file" "$text"; then
|
|
continue
|
|
fi
|
|
printf '%s:%s:%s\n' "$file" "$lineno" "$text" >&2
|
|
violations=$((violations + 1))
|
|
done < <(body_of "$file" | grep -nE "$PATTERN" || true)
|
|
done < <(find "${ROOTS[@]}" -name '*.rs' | sort)
|
|
|
|
if [ "$scanned" -lt 10 ]; then
|
|
echo "check-host-boundary: only $scanned files scanned — the roots look wrong" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [ "$violations" -ne 0 ]; then
|
|
cat >&2 <<'EOF'
|
|
|
|
--------------------------------------------------------------------------------
|
|
The GUI reached the filesystem/git directly.
|
|
|
|
A path in `ui::` or `terminal::` may belong to a remote workspace, where these
|
|
calls answer for the wrong machine. Route it through `ui::host_ops` / `Host`
|
|
instead (`Host::read_dir`, `join`, `is_absolute`, `canonicalize`, …).
|
|
|
|
If the path genuinely cannot be remote — app config, bundled assets, the local
|
|
temp dir — add it to the allowlist in this script with the reason why.
|
|
--------------------------------------------------------------------------------
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
echo "host boundary clean: $scanned files in ${ROOTS[*]}, no direct fs/git calls"
|