Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions docs/EXEMPTION-MECHANISMS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,43 @@ Suppresses matching findings from the gate, downgrades severity if
* One-off PR-scoped suppression. Baseline edits are merged to main; they
affect all subsequent PRs. See Layer 3.

=== 2a: `.hypatia-ignore` — scoped scanner suppression

**File:** `.hypatia-ignore` at the calling repo's root.

**Consumers — two, and they match differently:**

1. **The Hypatia scanner** (`hypatia/lib/hypatia/scanner_suppression.ex`).
Each line is `<rule_module>/<rule_type>:<path-fragment>`,
`<rule_module>/*:<path-fragment>`, or a bare `<path-fragment>` (any
rule). The fragment is a **substring** of the repo-relative path.
2. **The governance gate** (`governance-reusable.yml`, rules
`banned_language_file` and `banned_config_file`). It matches the
**whole line** `<rule>:<exact-repo-relative-path>` (`grep -qxF`).
A directory fragment or bare-path line quiets the scanner but not the
gate. That is deliberate: the gate is the stricter reader, and
listing each path in full means a newly added banned file still fails.

`<rule_module>` is the string a finding is **emitted** under, which is not
always the module that defines the rule: content-pattern findings
(`hardcoded_tmp`, `http_in_docs`, …) are emitted as `content_patterns/…`
although defined in `CicdRules`. Copy the id from the alert, never from
the source file; a wrong module string matches nothing, silently.

**Where it sits in the suppression ladder** (prefer the highest rung
that fits): inline directive at the call site →
`.hypatia-ignore` (a whole file or directory) →
`.hypatia-baseline.json` (acknowledged debt, Layer 2).

**Don't use for:** hiding a new finding in freshly-authored code, or
wildcarding a directory of banned-language files — list each path, so the
ledger can only shrink.

== Layer 3: Per-PR exemptions (NOT YET IMPLEMENTED)

There is currently no supported per-PR exemption mechanism. PR authors
attempting one (e.g. by inventing a `.hypatia-ignore` file) will find
nothing reads it, and the gate will still fail. **Do not invent new
There is currently no supported per-PR exemption mechanism. `.hypatia-ignore`
(Layer 2a) is committed to the default branch like any other file, so it
is not per-PR either. **Do not invent new
file conventions.** If you need per-PR exemption, file an issue against
`hyperpolymath/standards` proposing a designed mechanism.

Expand Down Expand Up @@ -151,11 +183,15 @@ A formal proposal should pick one and document it explicitly.

These have been attempted and should be refused at review:

* `.hypatia-ignore` (any format) — never read by anything. Reject; point
the author at `.hypatia-baseline.json`.
* Adding `pragma: ignore-rule` comments in source. Hypatia scans by AST
and ignores comments; suppression has to be data-driven, not
source-comment driven.
* A `.hypatia-ignore` line naming a directory, a wildcard, or a rule
module the finding is not emitted under. It either silences more than
it says (scanner) or matches nothing (gate). Require one full path per
line — see Layer 2a.
* Invented comment pragmas (`pragma: ignore-rule`, `noqa`, …). Only the
documented directives are read: `hypatia: allow <module>/<type> -- reason`
(or `hypatia:ignore <type>`) on the same or preceding line, by the
scanner; and `hypatia:ignore <rule>` in a file's first 8 lines, by the
governance gate. Anything else is ignored silently.
* Setting `continue-on-error: true` on the governance gate. This makes
CI lie. Use a baseline entry with `severity_override: advisory`
instead — keeps the finding visible while unblocking the gate.
Expand Down
18 changes: 10 additions & 8 deletions docs/UX-standards/QUICKSTART.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ APP_NAME="MyApp" # Your application name
REPO_DIR="/path/to/repo" # Repository directory
COMMAND="cargo run" # Command to start your app
URL="http://localhost:3000" # Web URL (if applicable)
PID_FILE="/tmp/myapp-server.pid" # Process ID file
LOG_FILE="/tmp/myapp-server.log" # Log file
PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/myapp/server.pid"
LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/myapp/server.log"
# Never /tmp: see launcher-standard.adoc §Best Practices > Security.
MODE="${1:---auto}" # Default mode
----

Expand Down Expand Up @@ -137,7 +138,7 @@ chmod +x /path/to/your/repo/scripts/warmup-*.sh
----
# Launcher starts server and opens browser
start_server() {
nohup npm run dev > /tmp/app.log 2>&1 &
nohup npm run dev > "$LOG_FILE" 2>&1 & # LOG_FILE from Step 2 -- never /tmp
wait_for_server 10
open_browser
}
Expand Down Expand Up @@ -173,12 +174,13 @@ Terminal=true # CLI tools need terminal
[source,bash]
----
# Use nohup for background processes
nohup /path/to/service > /tmp/service.log 2>&1 &
echo $! > /tmp/service.pid
# PID_FILE / LOG_FILE as in Step 2 -- never /tmp (predictable name, shared dir)
nohup /path/to/service > "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"

# Check if running
is_running() {
[ -f /tmp/service.pid ] && kill -0 $(cat /tmp/service.pid) 2>/dev/null
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
}
----

Expand All @@ -196,10 +198,10 @@ sudo chmod 444 ~/.local/share/applications/<app-name>.desktop
----

=== "Server not starting"
1. Check logs: `tail -50 /tmp/<app-name>.log`
1. Check logs: `tail -50 "${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/<app-name>/server.log"`
2. Run diagnostics: `<app-name>-dustfile.sh --diagnose`
3. Try repair: `<app-name>-dustfile.sh --repair`
4. LLM assistance: `hypatia diagnose --app <app-name> --log /tmp/<app-name>.log`
4. LLM assistance: `hypatia diagnose --app <app-name> --log "${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/<app-name>/server.log"`

=== "Browser not opening"
1. Check if server is running: `curl -v http://localhost:PORT`
Expand Down
2 changes: 1 addition & 1 deletion docs/UX-standards/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Launchers must provide clear, actionable feedback:
[ ] Implement `run_diagnostics()` function
[ ] Add proper error handling for browser launching
[ ] Provide clear user feedback at each step
[ ] Log to predictable locations (`/tmp/app-name.log`)
[ ] Log to the XDG state dir (`$XDG_STATE_HOME/launch-scaffolder/<app>/server.log`); never `/tmp` — see launcher-standard.adoc §Security
[ ] Handle missing dependencies gracefully
[ ] Provide manual fallback instructions

Expand Down
6 changes: 4 additions & 2 deletions docs/UX-standards/comprehensive-launcher-template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ REPO_DIR="/path/to/repo" # Repository directory
COMMAND="command to run" # Command to execute
URL="http://localhost:PORT" # URL if web app (empty if not)
ICON_SOURCE="$REPO_DIR/assets/icon-256.png" # Source icon for --integ (optional)
PID_FILE="/tmp/${APP_NAME,,}-server.pid" # PID file
LOG_FILE="/tmp/${APP_NAME,,}-server.log" # Log file
# Never /tmp for pid/log (CWE-377: predictable shared path) — see launcher-standard.adoc
PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/${APP_NAME,,}/server.pid"
LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME,,}/server.log"
MODE="${1:---auto}" # Default mode
FORCE="false" # --force flag (used by --integ)
[[ "${2:-}" == "--force" ]] && FORCE="true"
Expand Down Expand Up @@ -178,6 +179,7 @@ start_server() {

# Start in background with nohup to prevent process from being killed
cd "$REPO_DIR"
mkdir -p -m 0700 "$(dirname "$PID_FILE")" "$(dirname "$LOG_FILE")"
nohup $COMMAND >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"

Expand Down
8 changes: 5 additions & 3 deletions docs/UX-standards/dustfile-template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ set -euo pipefail
# ============================================================================
APP_NAME="TemplateApp"
APP_DIR="/path/to/app"
PID_FILE="/tmp/${APP_NAME,,}-server.pid"
LOG_FILE="/tmp/${APP_NAME,,}-server.log"
# Never /tmp for pid/log (CWE-377: predictable shared path) — see launcher-standard.adoc
PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/${APP_NAME,,}/server.pid"
LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME,,}/server.log"
PORT=4000
COMMAND="command to run"

Expand Down Expand Up @@ -139,6 +140,7 @@ repair_server_not_starting() {

# Try to start with more verbose logging
cd "$APP_DIR"
mkdir -p -m 0700 "$(dirname "$PID_FILE")" "$(dirname "$LOG_FILE")"
nohup $COMMAND --verbose >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"

Expand Down Expand Up @@ -261,4 +263,4 @@ esac
if command -v feedback-o-tron >/dev/null 2>&1; then
feedback-o-tron --event "dustfile:used" \
--app "$APP_NAME" --mode "$MODE" 2>/dev/null || true
fi
fi
6 changes: 4 additions & 2 deletions docs/UX-standards/e-grade-launcher-template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ APP_NAME="TemplateApp"
APP_DIR="/path/to/app"
APP_PORT=4000
APP_URL="http://localhost:$APP_PORT"
LOG_FILE="/tmp/${APP_NAME,,}-launcher.log"
# Never /tmp (CWE-377: predictable shared path) — see launcher-standard.adoc
LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME,,}/launcher.log"

# ============================================================================
# CORE FUNCTIONS - Standard E-grade functionality
Expand All @@ -36,6 +37,7 @@ start_server() {

# Use nohup for reliable background process management
cd "$APP_DIR"
mkdir -p -m 0700 "$(dirname "$LOG_FILE")"
nohup command_to_start_server >"$LOG_FILE" 2>&1 &

# Wait for server to be ready
Expand Down Expand Up @@ -151,4 +153,4 @@ case "$MODE" in
start_server
open_browser
;;
esac
esac
23 changes: 12 additions & 11 deletions docs/UX-standards/launcher-standard.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ REPO_DIR="/path/to/repo" # Repository directory
COMMAND="command to run" # Startup command
URL="http://localhost:PORT" # Web URL (if applicable)
# PID file in XDG_RUNTIME_DIR (mode 0700, user-scoped) — falls back to
# $TMPDIR (macOS) then /tmp (last resort). See §Best Practices > Security.
PID_FILE="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/${APP_NAME}-server.pid"
# XDG_STATE_HOME, NEVER $TMPDIR or /tmp. See §Best Practices > Security.
PID_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/${APP_NAME}"
PID_FILE="${PID_DIR}/server.pid"
# Log file in XDG_STATE_HOME (defaults to $HOME/.local/state per spec).
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/${APP_NAME}"
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME}"
LOG_FILE="${LOG_DIR}/server.log"
mkdir -p "$LOG_DIR"
mkdir -p -m 0700 "$PID_DIR" "$LOG_DIR"
MODE="${1:---auto}" # Default mode
----

Expand Down Expand Up @@ -395,13 +396,13 @@ system". The `--disinteg` mode is its exact inverse.

Everything `--integ` created, plus:

* The PID file (`$XDG_RUNTIME_DIR/<app>-server.pid`, or the resolved equivalent — see §Best Practices > Security)
* The PID file (`$XDG_RUNTIME_DIR/launch-scaffolder/<app>/server.pid`, or the resolved equivalent — see §Best Practices > Security)
* Any `.bat` fallback shortcuts written when PowerShell wasn't available

It deliberately **does not** remove:

* `~/.config/<app>/` — user preferences survive reinstall
* `$XDG_STATE_HOME/<app>/` (defaults to `$HOME/.local/state/<app>/`) — logs stay for post-mortem
* `$XDG_STATE_HOME/launch-scaffolder/<app>/` (defaults to `$HOME/.local/state/launch-scaffolder/<app>/`) — logs stay for post-mortem
* The source repository at `REPO_DIR`

The removal instructions for those are printed after `--disinteg` so the user
Expand Down Expand Up @@ -614,10 +615,10 @@ Exec=launcher.sh --auto

=== Debugging Checklist

1. Check log file: `tail -f "$LOG_FILE"` (resolves to `$XDG_STATE_HOME/<app>/server.log`)
1. Check log file: `tail -f "$LOG_FILE"` (resolves to `$XDG_STATE_HOME/launch-scaffolder/<app>/server.log`)
2. Verify process: `ps aux | grep app-name`
3. Test URL manually: `curl -v http://localhost:PORT`
4. Check PID file: `cat "$PID_FILE"` (resolves to `$XDG_RUNTIME_DIR/<app>-server.pid`)
4. Check PID file: `cat "$PID_FILE"` (resolves to `$XDG_RUNTIME_DIR/launch-scaffolder/<app>/server.pid`)
5. Test browser opening: `xdg-open http://localhost:PORT`
6. Verify dependencies: `command -v required-command`
7. Check port availability: `ss -tlnp | grep PORT`
Expand Down Expand Up @@ -665,7 +666,7 @@ APP_NAME="AppName"
[ ] Implement `wait_for_server()` with reasonable timeout
[ ] Add proper error handling and user feedback
[ ] Provide clear success/failure messages
[ ] Log to XDG state dir (`$XDG_STATE_HOME/<app>/server.log`, defaults to `$HOME/.local/state/<app>/server.log`); never use `/tmp/<app>.log`
[ ] Log to XDG state dir (`$XDG_STATE_HOME/launch-scaffolder/<app>/server.log`, defaults to `$HOME/.local/state/launch-scaffolder/<app>/server.log`); never use `/tmp/<app>.log`
[ ] Handle browser launch failures gracefully
[ ] Provide manual fallback instructions
[ ] Implement `--start`, `--stop`, `--status` modes
Expand All @@ -676,7 +677,7 @@ APP_NAME="AppName"
== Best Practices

=== Logging
- Log to `$XDG_STATE_HOME/<app>/server.log` (defaults to `$HOME/.local/state/<app>/server.log` per XDG spec). Per-user, survives reboot, not world-writable.
- Log to `$XDG_STATE_HOME/launch-scaffolder/<app>/server.log` (defaults to `$HOME/.local/state/launch-scaffolder/<app>/server.log` per XDG spec). Per-user, survives reboot, not world-writable.
- Never log to `/tmp/<app>.log`. Predictable names in a world-writable dir are a symlink-attack target on shared hosts — see §Best Practices > Security.
- Include timestamps for long-running processes
- Rotate logs if they grow large
Expand All @@ -696,7 +697,7 @@ APP_NAME="AppName"
- Optimize startup sequence

=== Security
- **PID files MUST go in `$XDG_RUNTIME_DIR`** (Linux) / `$TMPDIR` (macOS), not `/tmp`. `$XDG_RUNTIME_DIR` is mode `0700` and user-scoped per the XDG Base Directory spec; `$TMPDIR` on macOS is `/var/folders/.../T` (per-user). `/tmp` is world-writable: an attacker on a shared host can pre-create `/tmp/<app>-server.pid` containing their own PID, after which the launcher's `is_running()` returns true and `stop_server()` will `kill <attacker-pid>` — DoS or signal-handling abuse vector. The fallback ladder `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}` exists only as a last resort for hosts that set neither (rare).
- **PID files MUST go in `$XDG_RUNTIME_DIR`**, falling back to `$XDG_STATE_HOME` (default `$HOME/.local/state`) — never `$TMPDIR` or `/tmp`. `$XDG_RUNTIME_DIR` is mode `0700` and user-scoped per the XDG Base Directory spec; the `launch-scaffolder/<app>/` subdirectory is created `0700` by the launcher. `/tmp` is world-writable: an attacker on a shared host can pre-create `/tmp/<app>-server.pid` containing their own PID, after which the launcher's `is_running()` returns true and `stop_server()` will `kill <attacker-pid>` — DoS or signal-handling abuse vector. The ladder is `${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/<app>/server.pid`. `mktemp` is not an alternative: a PID file must be re-findable by the next invocation, and an unpredictable name cannot be.
- **Log files MUST go in `$XDG_STATE_HOME`** for the same reason — never `/tmp/<app>.log`.
- Use predictable but unique PID file *names within the chosen dir* (not in `/tmp`).
- Clean up PID files on exit
Expand Down
16 changes: 11 additions & 5 deletions launcher/launcher-standard_praxis.deed
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,18 @@
;; ------------------------------------------------------------------- runtime
(runtime
:background nohup
;; PID files live in the user's runtime-state dir -- wiped on logout,
;; user-scoped (mode 0700 per XDG), no symlink-attack target.
:pid-file-pattern "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/{app-name}-server.pid"
;; PID files live in the user's runtime dir -- wiped on logout, user-scoped
;; (mode 0700 per XDG), no symlink-attack target. The fallback is
;; XDG_STATE_HOME, NEVER TMPDIR or /tmp: a world-writable directory with a
;; name predictable from {app-name} lets another user pre-create the file
;; and choose which PID `stop` kills (CWE-377). mktemp is not an option
;; either -- a pid file must be re-findable by the next invocation.
;; The launch-scaffolder/{app-name} subdir is created 0700 by the launcher.
:pid-file-pattern "${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/{app-name}/server.pid"
;; Logs go to XDG_STATE_HOME. Per-user, survives reboot, not
;; world-writable; the {app-name} subdir isolates each launcher's logs.
:log-file-pattern "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/server.log"
;; world-writable; the launch-scaffolder/{app-name} subdir isolates each
;; launcher's logs and keeps them beside its pid fallback.
:log-file-pattern "${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/{app-name}/server.log"
;; URL-readiness polling after start. All three timing values are env-var
;; overridable so operators can tune without re-minting the launcher.
:wait-for-url-timeout-seconds 15
Expand Down
2 changes: 1 addition & 1 deletion launcher/soft-attach.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ hp_soft_attach_event() {
}

# CLI mode (not sourced): provide a thin wrapper for ad-hoc invocation.
# ./soft-attach.sh run "hypatia diagnose --app foo --log /tmp/foo.log"
# ./soft-attach.sh run "hypatia diagnose --app foo --log ~/.local/state/launch-scaffolder/foo/server.log"
# ./soft-attach.sh event feedback-o-tron launcher:start_failed
# ./soft-attach.sh present hypatia
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
Expand Down
Loading