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
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,29 @@ Never redirect or capture that terminal output. Fresh SSH and install-only
installations leave Direct disabled. Existing installations with no connection
option preserve their connection state and credentials without prompting.

The installer uses an existing `~/.tfrc` by default and adds the Imp capture
hook to it.

If TinyFugue uses a different startup file:
The installer uses an existing `~/.tfrc` by default. If it is absent and both
stdin and stdout are interactive, it asks for the explicit path of an existing
startup file; it never creates one or guesses a custom file. For a deterministic
selection (required without an interactive terminal when the default is absent):

```bash
./install.sh --tf-startup /path/to/your/tinyfugue/startup-file
./install.sh --tf-startup "/path/to/your TinyFugue/startup-file"
```

The installer does not create a missing TinyFugue startup file.
An invalid or missing explicit file fails early. When startup inspection is safe,
the installer backs up the selected file and prepends
`/load ~/.config/imp/capture.tf` before its original commands. Duplicate detection
covers one level of literal absolute or `~/` `/load` paths. An existing exact hook
is preserved without rewriting; its ordering is not verified. Recognized unsafe
includes, BOM-prefixed files, and legacy plain-world lists are left unchanged
for manual setup. Arbitrary startup macros cannot be fully inspected.
Confirm that your actual launch loads this file before any connection/login:
`tf -f FILE` uses `FILE`, not the normal personal startup file. If managing the
hook manually, put the load line first in a macro-format startup file. For a
legacy plain-world list, put it after all world definitions but before connecting,
or convert the definitions to `/addworld` first. Do not blindly prepend a command:
that ends TinyFugue's legacy loader mode. Installer success alone does not
establish runtime startup ordering.

Check the required services:

Expand Down Expand Up @@ -158,8 +171,11 @@ loginctl enable-linger "$USER"

Otherwise an administrator can enable lingering for your account.

After installing the hook, restart TinyFugue and connect to the MUD so Imp sees
a fresh login and world-selection context.
For an already-running TinyFugue, run `/load ~/.config/imp/capture.tf` now;
installing files does not reload that process. A matching normalized checkpoint
can recover previously captured identity without reconnecting, but cannot recover
identity the hook never captured. Otherwise, restart with the hook loaded early
and make a fresh identity-bearing MUD login.

For custom TinyFugue layouts, detailed verification, diagnostics, upgrades, or
Direct WSS deployment, see [VPS deployment](deploy/README.md).
Expand Down
71 changes: 47 additions & 24 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,52 @@ the existing connection state. Existing Imp runtime/configuration paths,
credentials, helper links, and user-service state all count as prior
installation evidence; one missing file does not make an install fresh.

The installer uses the existing `~/.tfrc` by default. Imp backs it up before
adding:
### TinyFugue startup file

The installer uses an existing `~/.tfrc` by default. When startup inspection is
safe, Imp backs it up, then prepends this line before all original commands:

```text
/load ~/.config/imp/capture.tf
```

If `~/.tfrc` does not exist, the installer stops rather than creating it.
TinyFugue can fall back to startup files relative to its working directory, so
creating a new `~/.tfrc` could otherwise change which configuration TinyFugue
loads.

If TinyFugue uses a different startup file, provide that existing file
explicitly:
The original bytes remain after the new line; ownership and permissions are
preserved. Normal installation rollback restores the original content. Duplicate
detection covers one level of literal absolute or `~/` `/load` paths. An existing
exact hook is retained without rewriting; its ordering is not established.

Recognized relative, deeper, or computed includes and `/eval` or `/require`
commands make inspection uncertain. BOM-prefixed files and legacy plain-world
lists are also not rewritten. In these cases the installer leaves the selected
file unchanged, reports no insertion, and requires manual early-loading setup.
Arbitrary startup macros cannot be fully inspected; this is not a TinyFugue
configuration parser.
Only column-zero `;` and `#` are treated as comments, matching TinyFugue's
startup reader. Indented comment markers are not valid comments; they leave the
file unchanged and require manual review, including in legacy world lists.

If `~/.tfrc` is absent and both stdin and stdout are interactive, the installer
asks for an explicit path to an existing startup file. It never creates one or
guesses a custom file. TinyFugue can fall back to working-directory startup files,
so creating `~/.tfrc` could change which configuration it loads. Without an
interactive terminal, select the existing file explicitly:

```bash
./install.sh --tf-startup /path/to/active/tinyfugue/startup-file
./install.sh --tf-startup "/path/to/active TinyFugue/startup-file"
```

This also covers custom layouts and TinyFugue launches that use `-f FILE`.
`--tf-startup` is deterministic: an invalid or nonexistent file fails early,
without falling back to another file. For `tf -f FILE`, select that actual
`FILE`, not an assumed `~/.tfrc`.

Always confirm that your actual TinyFugue invocation loads the selected file
before any world connects or logs in. Installer success does not verify runtime
invocation or ordering of an existing hook. For manual setup, copy the supplied
hook to `~/.config/imp/capture.tf` and put the load line above first in a
macro-format startup file, ahead of connection/login commands. For a legacy
plain-world list, place the hook after all world definitions but before any
connection/login command, or convert those definitions to `/addworld` first.
Do not blindly prepend a command: it ends TF's legacy loader mode.

The installer:

Expand Down Expand Up @@ -148,9 +174,9 @@ that old release directory in place; a failed activation restores the prior
units, links, and `current` selection, so rollback returns to the old release
with its historical runtime prerequisites.

For an already-running TinyFugue using the old hook, run
`/load ~/.config/imp/capture.tf` once after installing this update, before
testing live restart recovery. Installing the hook file does not reload TF.
For any already-running TinyFugue, run `/load ~/.config/imp/capture.tf` once
after installing this update, before testing live restart recovery. Installing
the hook file does not reload TF.
The updated hook owns one reload-safe `imp-feed-watch` quote job, using the
stable CLI link above (packaged role `imp-server feed-watch`); it checks the
private readiness marker once per second with bounded reads and exits when
Expand All @@ -159,9 +185,11 @@ TF closes its stdout pipe.
After feed restart, readiness is not authority: the live TF hook reasserts its
actual foreground context or null selection, never blindly trusting a saved
selection. Matching normalized checkpoint values return as stale until fresh
authoritative GMCP arrives. No MUD reconnect is required while cached identity
survives. The checkpoint is ephemeral: reboot or checkpoint loss still requires
fresh identity-bearing login GMCP under the documented TinyFugue prerequisites.
authoritative GMCP arrives. No MUD reconnect is required while matching cached
identity survives. This cannot recover identity that was never captured because
the hook loaded too late. The checkpoint is ephemeral: reboot or checkpoint loss
still requires fresh identity-bearing login GMCP under the documented TinyFugue
prerequisites, with the hook loaded before login.

Configuration and state directories are kept private to the user.

Expand Down Expand Up @@ -748,13 +776,8 @@ The installer copies the shipped hook to:
~/.config/imp/capture.tf
```

By default the installer adds the Imp `/load` line idempotently to an existing
`~/.tfrc`. With `--tf-startup FILE`, it instead backs up and updates the
explicitly selected existing startup file.

TinyFugue's documented personal configuration is `$HOME/.tfrc`, but TinyFugue
also supports alternate startup files. Operators using a different startup
file should pass that same file to `--tf-startup`.
See [TinyFugue startup file](#tinyfugue-startup-file) for existing-file selection,
safe automatic insertion, and cases requiring manual early-loading setup.

Load the hook before anything in that startup path can connect or log in to
the MUD. AVATAR sends the full identity-bearing `Char.Status` only once, during
Expand Down
124 changes: 100 additions & 24 deletions deploy/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,74 @@ if ((BEHIND_NAT)) && [[ "$CONNECTION" != direct ]]; then
die "--behind-nat is supported only with --connection direct"
fi

if [[ ! -f "$TF_STARTUP" ]]; then
if ((TF_STARTUP_EXPLICIT)); then
die "TinyFugue startup file does not exist: $TF_STARTUP"
if [[ ! -e "$TF_STARTUP" && ! -L "$TF_STARTUP" ]] && ((!TF_STARTUP_EXPLICIT)); then
if [[ -t 0 && -t 1 ]]; then
printf '\nDefault TinyFugue startup file does not exist: %s\n' "$TF_STARTUP"
read -r -p 'Enter an existing TinyFugue startup file: ' TF_STARTUP ||
die "TinyFugue startup selection interrupted"
case "$TF_STARTUP" in '~/'*) TF_STARTUP="$HOME/${TF_STARTUP:2}" ;; esac
else
die "default TinyFugue startup file does not exist: $TF_STARTUP; use --tf-startup FILE for an existing startup file"
fi

die "default TinyFugue startup file does not exist: $TF_STARTUP; use --tf-startup FILE for a different startup file"
fi
[[ -f "$TF_STARTUP" ]] || die "TinyFugue startup must be an existing regular file: $TF_STARTUP"
[[ -O "$TF_STARTUP" && -r "$TF_STARTUP" && -w "$TF_STARTUP" ]] ||
die "TinyFugue startup must be owned by the current user, readable and writable: $TF_STARTUP"

LOAD_LINE='/load ~/.config/imp/capture.tf'
TF_DIRECT_HOOKS=0
TF_EXACT_HOOK=0
TF_NESTED_HOOKS=0
TF_UNCERTAIN=0
TF_BOM=0
TF_LEGACY=0
TF_INCLUDES=()
TF_NEWLINE=$'\n'
# shortcut: inspect only one level of literal /load paths; complex TF programs need operator review.
inspect_tf_startup() {
local file="$1" depth="$2" line first=1 include
while IFS= read -r line || [[ -n "$line" ]]; do
if ((!depth)) && [[ "${line%$'\r'}" == "$LOAD_LINE" ]]; then TF_EXACT_HOOK=1; fi
if ((first)); then
if [[ "$line" == $'\xef\xbb\xbf'* ]]; then TF_BOM=1; TF_UNCERTAIN=1; fi
if ((!depth)) && [[ "$line" == *$'\r' ]]; then TF_NEWLINE=$'\r\n'; fi
line="${line#$'\xef\xbb\xbf'}"
first=0
fi
line="${line%$'\r'}"
# TF checks column-zero comments before stripping whitespace (src/command.c).
case "$line" in ';'*|'#'*) continue ;; esac
line="${line#"${line%%[![:space:]]*}"}"
case "$line" in
'') continue ;;
/eval|/eval[[:space:]]*|/require|/require[[:space:]]*) TF_UNCERTAIN=1; continue ;;
/*) ;;
*) TF_LEGACY=1; TF_UNCERTAIN=1; continue ;;
esac
if [[ "$line" == /load[[:space:]]* ]]; then
include="${line#/load}"
include="${include#"${include%%[![:space:]]*}"}"
case "$include" in
'~/'*) include="$HOME/${include:2}" ;;
/*) ;;
*) TF_UNCERTAIN=1; continue ;;
esac
if [[ "$include" == "$CONFIG_DIR/capture.tf" || "$include" -ef "$CONFIG_DIR/capture.tf" ]]; then
if ((depth)); then
TF_NESTED_HOOKS=$((TF_NESTED_HOOKS + 1))
else
TF_DIRECT_HOOKS=$((TF_DIRECT_HOOKS + 1))
fi
elif ((depth)) || [[ ! -f "$include" || ! -r "$include" ]]; then
TF_UNCERTAIN=1
else
TF_INCLUDES+=("$include")
fi
fi
done < "$file"
}
inspect_tf_startup "$TF_STARTUP" 0
for include in "${TF_INCLUDES[@]}"; do inspect_tf_startup "$include" 1; done

DIRECT_HAS_CREDENTIAL=0
if [[ -e "$CONFIG_DIR/gateway.env" || -L "$CONFIG_DIR/gateway.env" ]]; then
Expand Down Expand Up @@ -517,23 +578,24 @@ for unit in imp-relay.service imp-feed.service imp-gateway.service; do
"$SYSTEMD_DIR/$unit"
done

LOAD_LINE='/load ~/.config/imp/capture.tf'
TF_CONFIGURED=0

if grep -Fxq "$LOAD_LINE" "$TF_STARTUP"; then
printf 'TinyFugue startup already contains the Imp hook: %s\n' "$TF_STARTUP"
if ((TF_EXACT_HOOK)); then
TF_STATUS='already exact; preserved without moving or rewriting'
elif ((TF_DIRECT_HOOKS)); then
TF_STATUS='existing hook preserved; no duplicate added'
elif ((TF_NESTED_HOOKS)); then
TF_STATUS='nested hook preserved; no duplicate added'
elif ((TF_UNCERTAIN)); then
TF_STATUS='load order not established; manual integration required'
else
BACKUP="${TF_STARTUP}.imp-backup.$(date +%Y%m%d%H%M%S)"
cp -p -- "$TF_STARTUP" "$BACKUP"
TF_STARTUP_BACKUP="$BACKUP"
TF_STARTUP_BACKUP="$(mktemp "${TF_STARTUP}.imp-backup.XXXXXXXX")"
cp -p -- "$TF_STARTUP" "$TF_STARTUP_BACKUP"
TF_STARTUP_CHANGED=1
printf '\n%s\n' "$LOAD_LINE" >> "$TF_STARTUP"
printf 'Updated TinyFugue startup file: %s\n' "$TF_STARTUP"
printf 'Backup: %s\n' "$BACKUP"
{ printf '%s%s' "$LOAD_LINE" "$TF_NEWLINE"; cat -- "$TF_STARTUP_BACKUP"; } > "$TF_STARTUP"
chmod --reference="$TF_STARTUP_BACKUP" -- "$TF_STARTUP"
TF_STATUS='inserted before original startup commands'
printf 'Backup: %s\n' "$TF_STARTUP_BACKUP"
fi

TF_CONFIGURED=1

if ((START_SERVICES)); then
printf 'Installing Imp systemd user services...\n'

Expand Down Expand Up @@ -635,13 +697,27 @@ else
printf 'Services: installed but not started (--no-start).\n'
fi

if ((TF_CONFIGURED)); then
printf 'TinyFugue startup integration: configured.\n'
else
printf '\nTinyFugue startup integration still requires this line:\n'
printf ' %s\n' "$LOAD_LINE"
printf 'Re-run with --tf-startup FILE to install it automatically.\n'
printf 'TinyFugue startup: %s\n' "$TF_STARTUP"
printf 'TinyFugue integration: %s.\n' "$TF_STATUS"
if ((TF_DIRECT_HOOKS || TF_NESTED_HOOKS || TF_UNCERTAIN)); then
printf 'warning: TinyFugue load order not established; inspect startup commands and nested loads.\n'
fi
if ((TF_BOM)); then
printf 'warning: BOM-prefixed startup preserved; native TinyFugue BOM handling is not established. Review encoding and hook execution manually.\n'
fi
if ((TF_DIRECT_HOOKS + TF_NESTED_HOOKS > 1)); then
printf 'warning: multiple Imp hook loads found; duplicate loading is possible; preserved unchanged.\n'
fi
if [[ "$TF_STATUS" == 'load order not established; manual integration required' ]]; then
printf 'No new hook inserted: legacy syntax, unsupported loads, or a BOM require manual review; preserve existing file encoding when editing.\n'
printf 'Manually arrange this before connection/login commands: %s\n' "$LOAD_LINE"
fi
if ((TF_LEGACY)); then
printf 'Possible legacy world-list startup preserved: place the hook after legacy world definitions but before the first connection/login command, or convert world definitions to /addworld before using an early /load. Do not blindly prepend the hook.\n'
fi
printf 'Confirm your actual TinyFugue invocation (for example, tf -f FILE) loads this selected file and the hook before connection/login commands.\n'
printf 'For the current TF session, manually run: %s\n' "$LOAD_LINE"
printf 'A matching normalized checkpoint can recover prior authoritative identity without reconnecting; without that identity, recovery is not guaranteed.\n'

if command -v loginctl >/dev/null 2>&1; then
LINGER="$(loginctl show-user "$USER" -p Linger --value 2>/dev/null || true)"
Expand Down
24 changes: 22 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ claims belong in `status.md` and the relevant architecture documents.
| Slice 23 - ActionBroker timeout validation | Complete | `actionbroker-timeout-validation` |
| Slice 24 - Direct WSS provisioning and onboarding | Complete | No slice marker; accepted in `v0.3.2` |
| Slice 25 - Connection-oriented server installation | Complete | No slice marker; accepted in `v0.4.1` |
| Slice 26 - Installation reliability and onboarding | In progress | No slice marker |

Slice numbers and milestone tags are development-history markers. They have no
relationship to release versions. Release versions are derived independently
Expand All @@ -54,8 +55,8 @@ The Slices 17–23 remediation program is complete; see the separate
PR identities, verification, and retained limitations. Slice 24 is complete
with non-blocking follow-ups. Slice 25 is complete: its implementation shipped
in `v0.4.0` and was accepted on published artifacts, with a DNS preflight fix in
`v0.4.1`, plus retained follow-ups. Other candidates remain provisional; no next
slice is assigned.
`v0.4.1`, plus retained follow-ups. Slice 26 is in progress with the bounded
scope below. Other candidates remain provisional.

## ✅ Slice 1 - Bootstrap baseline

Expand Down Expand Up @@ -765,6 +766,25 @@ These are retained priorities, not failed acceptance checks or a new slice:
- **Real-runtime CI:** run the real Caddy routing matrix and native TinyFugue
restart regression in CI rather than relying only on deterministic substitutes.

## In progress

### Slice 26 - Installation reliability and onboarding

In progress. Bounded scope:

1. TinyFugue onboarding/startup integration (**only this PR**): explicit
existing-file selection, early hook loading, one-level literal absolute/`~/`
include duplicate detection, manual setup when inspection is unsafe, and
honest operator confirmation of actual startup ordering.
2. Warning before discarding saved Direct credentials.
3. Correct NAT preflight error classification.
4. Recovery of an exact-owned inactive legacy edge when `gateway.env` is missing.
5. Installation documentation and completion-message corrections.

This PR does not change Direct provisioning, credentials, or the protocol, and
does not claim live infrastructure acceptance. The remaining Slice 26 items are
not implemented by this PR.

## Candidate work

Unordered, deliberately without slice numbers, and provisional. Live evidence
Expand Down
Loading