4 Commits

Author SHA1 Message Date
c4ch3c4d3 56305680e0 Fix terminal deployment regressions 2026-04-13 21:22:16 -06:00
c4ch3c4d3 5576142aec Use host-managed SSH accounts for browser terminal 2026-04-13 19:40:38 -06:00
c4ch3c4d3 13ce59d901 Restore executable bits on deployment scripts 2026-04-13 16:40:35 -06:00
c4ch3c4d3 cbf6c3fad3 Add managed Lab 3 browser terminal deployment 2026-04-13 16:40:14 -06:00
14 changed files with 213 additions and 198 deletions
+10 -2
View File
@@ -79,8 +79,7 @@ If CUDA is already mounted or preinstalled outside `PATH`, the installer now det
- The host-side install path assumes modern local tooling, but TransformerLab itself is provisioned from a pinned classic single-user layout.
- TransformerLab is intentionally pinned to the older single-user `v0.28.2` release because newer upstream releases changed the project structure and behavior in ways that break this courseware.
- This project does not rely on TransformerLab's upstream `install.sh`; the Ansible role provisions the pinned release directly so web assets, env layout, and runtime behavior stay reproducible.
- The courseware repairs the pinned TransformerLab install for symlink-aware plugin file lookups and refreshes installed Fastchat plugin manifests so Fastchat-gated features such as Model Architecture, activations, and Visualize Logprobs stay available on pinned installs.
- The managed default TransformerLab student account is also seeded with the courseware Fastchat plugin plus the starter experiments and model metadata that `labctl up` depends on.
- The courseware repairs installed TransformerLab Fastchat plugin manifests so Fastchat-gated features such as Model Architecture and Visualize Logprobs stay available on pinned installs.
- No Ollama models are pulled during `./labctl up`; students pull models manually as part of the courseware.
- WhiteRabbitNeo assets are handled separately from `./labctl up` and `./labctl preflight`.
- Run `./labctl assets lab2` when you want to populate repo-local lab 2 assets in `assets/lab2/` from Hugging Face.
@@ -104,6 +103,15 @@ Default endpoints:
- Unsloth Studio: `http://127.0.0.1:8888`
- Promptfoo UI: `http://127.0.0.1:15500`
- Wiki: `http://127.0.0.1:80`
- Lab 3 Terminal: `http://127.0.0.1:7681/wetty`
## Lab 3 Browser Terminal
The deployment will:
- bind `sshd` to `127.0.0.1:22` only
- install WeTTY and expose it at `http://127.0.0.1:7681/wetty`
- leave login identity management to the host, so any existing local account with password-based SSH access can sign in through the browser terminal
## Notes
+6
View File
@@ -18,8 +18,10 @@ courseware_unsloth_home: "{{ courseware_state_dir }}/unsloth-home"
courseware_ollama_models_dir: "{{ courseware_models_dir }}/ollama"
courseware_node_runtime_dir: "{{ courseware_tools_dir }}/node-runtime"
courseware_node_runtime_bin_dir: "{{ courseware_node_runtime_dir }}/node_modules/node/bin"
courseware_wetty_dir: "{{ courseware_tools_dir }}/wetty"
courseware_promptfoo_dir: "{{ courseware_lab6_dir }}"
courseware_wiki_repo_dir: "{{ courseware_repos_dir }}/LLM-Labs"
courseware_wiki_runtime_config_path: "{{ courseware_wiki_repo_dir }}/public/courseware-runtime.json"
courseware_llama_cpp_bin_dir: "{{ courseware_repos_dir }}/llama.cpp/build/bin"
courseware_bind_host: "0.0.0.0"
@@ -33,6 +35,7 @@ courseware_ports:
unsloth: 8888
promptfoo: 15500
wiki: 80
wetty: 7681
courseware_transformerlab_install_mode: "single-user-pinned"
courseware_transformerlab_version: "v0.28.2"
@@ -63,6 +66,8 @@ courseware_chunkviz_commit: "a891eacafda1f28a12373ad3b00102e68f07c57f"
courseware_promptfoo_version: "0.119.0"
courseware_kiln_release_tag: "v0.18.1"
courseware_node_runtime_version: "20.20.2"
courseware_wetty_spec: "wetty@2.5.0"
courseware_wetty_base_path: "/wetty"
courseware_wiki_repo: "https://git.zuccaro.me/bzuccaro/LLM-Labs.git"
courseware_open_webui_spec: "open-webui"
@@ -151,3 +156,4 @@ courseware_services:
- "unsloth"
- "promptfoo"
- "wiki"
- "wetty"
+1
View File
@@ -8,6 +8,7 @@
- packages
- lab_assets
- node_runtime
- { role: terminal, when: ansible_system == "Linux" }
- llama_cpp
- transformerlab
- open_webui
+114
View File
@@ -0,0 +1,114 @@
- name: Install terminal prerequisites
become: true
apt:
name:
- openssh-server
state: present
update_cache: true
- name: Ensure sshd drop-in directory exists
become: true
file:
path: /etc/ssh/sshd_config.d
state: directory
mode: "0755"
- name: Configure courseware loopback-only sshd policy
become: true
template:
src: sshd-courseware-terminal.conf.j2
dest: /etc/ssh/sshd_config.d/50-courseware-terminal.conf
mode: "0644"
register: courseware_terminal_sshd_config
- name: Ensure sshd runtime directory exists
become: true
file:
path: /run/sshd
state: directory
mode: "0755"
- name: Validate sshd configuration
become: true
command:
argv:
- /usr/sbin/sshd
- -t
- -f
- /etc/ssh/sshd_config
changed_when: false
- name: Start and enable sshd with systemd when available
become: true
systemd:
name: ssh
state: started
enabled: true
when: ansible_service_mgr == "systemd"
- name: Check for running sshd when systemd is unavailable
become: true
command: pgrep -x sshd
register: courseware_terminal_sshd_pid
changed_when: false
failed_when: false
when: ansible_service_mgr != "systemd"
- name: Reload running sshd when config changed outside systemd
become: true
command: pkill -HUP -x sshd
when:
- ansible_service_mgr != "systemd"
- courseware_terminal_sshd_pid.rc == 0
- courseware_terminal_sshd_config.changed
- name: Start sshd when it is not already running outside systemd
become: true
command:
argv:
- /usr/sbin/sshd
when:
- ansible_service_mgr != "systemd"
- courseware_terminal_sshd_pid.rc != 0
- name: Create contained WeTTY directory
file:
path: "{{ courseware_wetty_dir }}"
state: directory
mode: "0755"
- name: Install contained WeTTY runtime
command:
argv:
- npm
- install
- "{{ courseware_wetty_spec }}"
args:
chdir: "{{ courseware_wetty_dir }}"
creates: "{{ courseware_wetty_dir }}/node_modules/.bin/wetty"
environment:
PATH: "{{ courseware_node_runtime_bin_dir }}:{{ ansible_env.PATH }}"
- name: Check loopback sshd listener
become: true
command: ss -ltn
register: courseware_terminal_ss_listeners
changed_when: false
- name: Assert sshd is loopback-only
assert:
that:
- "'127.0.0.1:22' in courseware_terminal_ss_listeners.stdout"
- "'0.0.0.0:22' not in courseware_terminal_ss_listeners.stdout"
- "'[::]:22' not in courseware_terminal_ss_listeners.stdout"
fail_msg: "sshd must listen only on 127.0.0.1:22 for the browser terminal deployment."
- name: Assert WeTTY binary exists
stat:
path: "{{ courseware_wetty_dir }}/node_modules/.bin/wetty"
register: courseware_wetty_bin_stat
- name: Fail when WeTTY installation is incomplete
fail:
msg: "WeTTY was not installed under {{ courseware_wetty_dir }}."
when: not courseware_wetty_bin_stat.stat.exists
@@ -0,0 +1,11 @@
# Managed by Local Courseware Deployment.
ListenAddress 127.0.0.1
AddressFamily inet
PermitRootLogin no
PasswordAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
AllowTcpForwarding no
X11Forwarding no
PrintMotd no
@@ -233,27 +233,12 @@
dest: "{{ courseware_state_dir }}/repair_transformerlab_plugin_supports.py"
mode: "0755"
- name: Install TransformerLab source repair helper
copy:
src: "{{ playbook_dir }}/../../scripts/repair_transformerlab_symlink_paths.py"
dest: "{{ courseware_state_dir }}/repair_transformerlab_symlink_paths.py"
mode: "0755"
- name: Install TransformerLab default-user helper
copy:
src: "{{ playbook_dir }}/../../scripts/ensure_transformerlab_user.py"
dest: "{{ courseware_state_dir }}/ensure_transformerlab_user.py"
mode: "0755"
- name: Repair pinned TransformerLab symlink-aware plugin file lookups
command:
argv:
- python3
- "{{ courseware_state_dir }}/repair_transformerlab_symlink_paths.py"
- --transformerlab-dir
- "{{ courseware_transformerlab_home }}"
changed_when: false
- name: Repair installed Fastchat plugin supports
command:
argv:
+8 -13
View File
@@ -1,5 +1,5 @@
diff --git a/src/app/labs/[slug]/page.tsx b/src/app/labs/[slug]/page.tsx
index f67308f..a6aac38 100644
index eb949ae..bb3d51c 100644
--- a/src/app/labs/[slug]/page.tsx
+++ b/src/app/labs/[slug]/page.tsx
@@ -462,6 +462,19 @@ function markdownToHtml(markdown: string) {
@@ -41,20 +41,15 @@ index f67308f..a6aac38 100644
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
diff --git a/src/components/labs/LabContent.tsx b/src/components/labs/LabContent.tsx
index 7a7ce52..8778a23 100644
index 6addccf..afdd12f 100644
--- a/src/components/labs/LabContent.tsx
+++ b/src/components/labs/LabContent.tsx
@@ -277,7 +277,12 @@ export function LabContent({ className, html }: LabContentProps) {
>
<div className="lab-image-modal__surface" onClick={(event) => event.stopPropagation()}>
{/* eslint-disable-next-line @next/next/no-img-element */}
- <img className="lab-image-modal__image" src={zoomedImage.src} alt={zoomedImage.alt} />
+ <img
+ className="lab-image-modal__image"
+ src={zoomedImage.src}
+ alt={zoomedImage.alt}
@@ -346,6 +346,7 @@ export function LabContent({ className, html }: LabContentProps) {
<img
className="lab-image-modal__image"
src={zoomedImage.src}
alt={zoomedImage.alt}
+ referrerPolicy="no-referrer"
+ />
/>
</div>
</div>
) : null}
+6
View File
@@ -36,6 +36,12 @@
environment:
PATH: "{{ courseware_node_runtime_bin_dir }}:{{ ansible_env.PATH }}"
- name: Render wiki runtime config
template:
src: courseware-runtime.json.j2
dest: "{{ courseware_wiki_runtime_config_path }}"
mode: "0644"
- name: Stat wiki build output
stat:
path: "{{ courseware_wiki_repo_dir }}/.next/BUILD_ID"
@@ -0,0 +1,3 @@
{
"lab3TerminalUrl": "http://{{ courseware_url_host }}:{{ courseware_ports.wetty }}{{ courseware_wetty_base_path }}"
}
+4
View File
@@ -11,9 +11,12 @@ COURSEWARE_EMBEDDING_ATLAS_PORT="{{ courseware_ports.embedding_atlas }}"
COURSEWARE_UNSLOTH_PORT="{{ courseware_ports.unsloth }}"
COURSEWARE_PROMPTFOO_PORT="{{ courseware_ports.promptfoo }}"
COURSEWARE_WIKI_PORT="{{ courseware_ports.wiki }}"
COURSEWARE_WETTY_PORT="{{ courseware_ports.wetty }}"
OLLAMA_BIN="{{ courseware_ollama_bin }}"
OLLAMA_MODELS_DIR="{{ courseware_ollama_models_dir }}"
NODE_RUNTIME_BIN_DIR="{{ courseware_node_runtime_bin_dir }}"
WETTY_BIN="{{ courseware_wetty_dir }}/node_modules/.bin/wetty"
COURSEWARE_WETTY_BASE_PATH="{{ courseware_wetty_base_path }}"
OPEN_WEBUI_VENV="{{ courseware_venvs_dir }}/open-webui"
OPEN_WEBUI_DATA_DIR="{{ courseware_state_dir }}/open-webui"
CHUNKVIZ_DIR="{{ courseware_repos_dir }}/ChunkViz"
@@ -29,6 +32,7 @@ UNSLOTH_BIN="{{ ansible_env.HOME }}/.local/bin/unsloth"
PROMPTFOO_DIR="{{ courseware_promptfoo_dir }}"
PROMPTFOO_BIN="{{ courseware_tools_dir }}/promptfoo/node_modules/.bin/promptfoo"
WIKI_DIR="{{ courseware_wiki_repo_dir }}"
WIKI_RUNTIME_CONFIG_PATH="{{ courseware_wiki_runtime_config_path }}"
LLAMA_CPP_BIN_DIR="{{ courseware_llama_cpp_bin_dir }}"
KILN_LINUX_BIN="{{ courseware_apps_dir }}/kiln/Kiln"
KILN_MAC_APP="{{ courseware_apps_dir }}/Kiln.app"
+17 -1
View File
@@ -17,9 +17,13 @@ load_runtime_env() {
: "${COURSEWARE_URL_HOST:=127.0.0.1}"
: "${COURSEWARE_PROMPTFOO_PORT:=15500}"
: "${COURSEWARE_WIKI_PORT:=80}"
: "${COURSEWARE_WETTY_PORT:=7681}"
: "${COURSEWARE_WETTY_BASE_PATH:=/wetty}"
: "${NODE_RUNTIME_BIN_DIR:=$COURSEWARE_STATE_DIR/tools/node-runtime/node_modules/node/bin}"
: "${WETTY_BIN:=$COURSEWARE_STATE_DIR/tools/wetty/node_modules/.bin/wetty}"
: "${PROMPTFOO_DIR:=$COURSEWARE_STATE_DIR/lab6}"
: "${WIKI_DIR:=$COURSEWARE_STATE_DIR/repos/LLM-Labs}"
: "${WIKI_RUNTIME_CONFIG_PATH:=$WIKI_DIR/public/courseware-runtime.json}"
: "${LLAMA_CPP_BIN_DIR:=$COURSEWARE_STATE_DIR/repos/llama.cpp/build/bin}"
if [ -n "${OLLAMA_BIN:-}" ] && [[ "$OLLAMA_BIN" != */* ]] && command -v "$OLLAMA_BIN" >/dev/null 2>&1; then
@@ -43,7 +47,8 @@ service_list() {
"embedding-atlas" \
"unsloth" \
"promptfoo" \
"wiki"
"wiki" \
"wetty"
}
service_pid_file() {
@@ -64,6 +69,7 @@ service_port() {
unsloth) printf '%s\n' "${COURSEWARE_UNSLOTH_PORT}" ;;
promptfoo) printf '%s\n' "${COURSEWARE_PROMPTFOO_PORT}" ;;
wiki) printf '%s\n' "${COURSEWARE_WIKI_PORT}" ;;
wetty) printf '%s\n' "${COURSEWARE_WETTY_PORT}" ;;
*) return 1 ;;
esac
}
@@ -78,6 +84,7 @@ service_url() {
unsloth) printf 'http://%s:%s\n' "$COURSEWARE_URL_HOST" "$COURSEWARE_UNSLOTH_PORT" ;;
promptfoo) printf 'http://%s:%s\n' "$COURSEWARE_URL_HOST" "$COURSEWARE_PROMPTFOO_PORT" ;;
wiki) printf 'http://%s:%s\n' "$COURSEWARE_URL_HOST" "$COURSEWARE_WIKI_PORT" ;;
wetty) printf 'http://%s:%s%s\n' "$COURSEWARE_URL_HOST" "$COURSEWARE_WETTY_PORT" "$COURSEWARE_WETTY_BASE_PATH" ;;
*) return 1 ;;
esac
}
@@ -144,6 +151,15 @@ service_command() {
"$COURSEWARE_BIND_HOST" \
"$COURSEWARE_WIKI_PORT"
;;
wetty)
printf 'cd "%s" && PATH="%s:$PATH" exec "%s" --host %s --port %s --base %s --allow-iframe --ssh-host 127.0.0.1 --ssh-port 22 --ssh-auth password' \
"$COURSEWARE_ROOT" \
"$NODE_RUNTIME_BIN_DIR" \
"$WETTY_BIN" \
"$COURSEWARE_BIND_HOST" \
"$COURSEWARE_WETTY_PORT" \
"$COURSEWARE_WETTY_BASE_PATH"
;;
*)
return 1
;;
-94
View File
@@ -4,15 +4,9 @@ from __future__ import annotations
import argparse
import asyncio
import os
import shutil
import sys
from pathlib import Path
DEFAULT_WORKSPACE_PLUGINS = ("fastchat_server",)
DEFAULT_WORKSPACE_EXPERIMENTS = ("alpha", "beta", "gamma")
DEFAULT_WORKSPACE_MODELS = ("unsloth_Llama-3.2-1B-Instruct",)
DEFAULT_MODEL_METADATA_FILES = ("_tlab_complete_provenance.json",)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
@@ -42,92 +36,6 @@ def bootstrap_source(transformerlab_dir: Path) -> None:
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
def target_workspace(transformerlab_dir: Path, team_id: str) -> Path:
return transformerlab_dir / "orgs" / team_id / "workspace"
def workspace_team_id(workspace: Path, transformerlab_dir: Path) -> str | None:
orgs_dir = transformerlab_dir / "orgs"
try:
relative = workspace.relative_to(orgs_dir)
except ValueError:
return None
if len(relative.parts) >= 2 and relative.parts[1] == "workspace":
return relative.parts[0]
return None
def candidate_workspaces(transformerlab_dir: Path, excluded_team_id: str) -> list[Path]:
candidates: list[Path] = []
root_workspace = transformerlab_dir / "workspace"
if root_workspace.is_dir():
candidates.append(root_workspace)
orgs_dir = transformerlab_dir / "orgs"
if not orgs_dir.is_dir():
return candidates
for workspace in sorted(orgs_dir.glob("*/workspace")):
if not workspace.is_dir():
continue
if workspace_team_id(workspace, transformerlab_dir) == excluded_team_id:
continue
candidates.append(workspace)
return candidates
def copy_dir_if_missing(source: Path | None, target: Path, label: str) -> bool:
if source is None or not source.is_dir() or target.exists():
return False
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(source, target)
print(f"Seeded {label} from {source}.")
return True
def copy_file_if_missing(source: Path | None, target: Path, label: str) -> bool:
if source is None or not source.is_file() or target.exists():
return False
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
print(f"Seeded {label} from {source}.")
return True
def find_workspace_seed(transformerlab_dir: Path, category: str, name: str, excluded_team_id: str) -> Path | None:
for workspace in candidate_workspaces(transformerlab_dir, excluded_team_id):
candidate = workspace / category / name
if candidate.exists():
return candidate
return None
def seed_workspace(transformerlab_dir: Path, team_id: str) -> None:
workspace = target_workspace(transformerlab_dir, team_id)
workspace.mkdir(parents=True, exist_ok=True)
for plugin in DEFAULT_WORKSPACE_PLUGINS:
source = transformerlab_dir / "src" / "transformerlab" / "plugins" / plugin
copy_dir_if_missing(source, workspace / "plugins" / plugin, f"plugin '{plugin}'")
for experiment in DEFAULT_WORKSPACE_EXPERIMENTS:
source = find_workspace_seed(transformerlab_dir, "experiments", experiment, team_id)
copy_dir_if_missing(source, workspace / "experiments" / experiment, f"experiment '{experiment}'")
copied_model = False
for model in DEFAULT_WORKSPACE_MODELS:
source = find_workspace_seed(transformerlab_dir, "models", model, team_id)
copied_model = copy_dir_if_missing(source, workspace / "models" / model, f"model '{model}'") or copied_model
for metadata_name in DEFAULT_MODEL_METADATA_FILES:
source = find_workspace_seed(transformerlab_dir, "models", metadata_name, team_id)
if copied_model or source is not None:
copy_file_if_missing(source, workspace / "models" / metadata_name, f"model metadata '{metadata_name}'")
async def ensure_user(args: argparse.Namespace) -> int:
from sqlalchemy import select
from transformerlab.db.constants import DATABASE_FILE_NAME
@@ -221,8 +129,6 @@ async def ensure_user(args: argparse.Namespace) -> int:
await session.commit()
print(f"Updated team role to owner for {args.email}.")
seed_workspace(Path(args.transformerlab_dir), str(user_team.team_id))
action = "Created" if created else "Verified"
print(f"{action} default TransformerLab user {args.email}.")
return 0
@@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""Patch pinned TransformerLab source to tolerate symlinked home directories."""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
PATCH_MARKER = "with symlinked TransformerLab home directories."
TARGET_BLOCK = re.compile(
r"(?P<indent>[ \t]+)# The following prevents path traversal attacks:.*?"
r"(?P=indent)# now get the file contents",
re.DOTALL,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--transformerlab-dir", required=True)
return parser.parse_args()
def repair_plugins_router(path: Path) -> bool:
source = path.read_text(encoding="utf-8")
if PATCH_MARKER in source:
return False
replacement = (
" # The following prevents path traversal attacks while remaining compatible\n"
" # with symlinked TransformerLab home directories.\n"
" plugin_dir = Path(await lab_dirs.plugin_dir_by_name((pluginId)))\n"
" resolved_plugin_dir = plugin_dir.resolve()\n"
' final_path = (plugin_dir / f"{filename}{file_ext}").resolve()\n'
"\n"
" try:\n"
" final_path.relative_to(resolved_plugin_dir)\n"
" except ValueError:\n"
' return {"message": f"File {filename}{file_ext} is outside plugin directory"}\n'
"\n"
" # now get the file contents"
)
updated, count = TARGET_BLOCK.subn(replacement, source, count=1)
if count != 1:
raise RuntimeError(f"Could not find path traversal block in {path}")
path.write_text(updated, encoding="utf-8")
return True
def main() -> int:
args = parse_args()
root = Path(args.transformerlab_dir).expanduser().resolve()
plugins_router = root / "src" / "transformerlab" / "routers" / "experiment" / "plugins.py"
if not plugins_router.exists():
print(f"missing TransformerLab plugins router: {plugins_router}", file=sys.stderr)
return 1
changed = repair_plugins_router(plugins_router)
if changed:
print(f"patched {plugins_router}")
else:
print(f"already patched {plugins_router}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+33 -3
View File
@@ -28,6 +28,34 @@ ensure_transformerlab_default_user() {
--last-name "${TRANSFORMERLAB_DEFAULT_USER_LAST_NAME:-}" >>"$STATE_DIR/logs/transformerlab_default_user.log" 2>&1 || true
}
check_wetty_prereqs() {
if [ ! -x "$WETTY_BIN" ]; then
echo "Missing WeTTY binary at $WETTY_BIN. Re-run ./labctl up." >&2
exit 1
fi
if [ ! -f "$WIKI_RUNTIME_CONFIG_PATH" ]; then
echo "Missing wiki runtime config at $WIKI_RUNTIME_CONFIG_PATH. Re-run ./labctl up." >&2
exit 1
fi
if ! python3 - <<'PY'
import socket, sys
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
try:
sock.connect(("127.0.0.1", 22))
except OSError:
sys.exit(1)
finally:
sock.close()
PY
then
echo "Loopback sshd is not reachable on 127.0.0.1:22." >&2
exit 1
fi
}
resolve_targets() {
if [ $# -eq 0 ]; then
echo "No target specified." >&2
@@ -82,7 +110,7 @@ service_ready() {
promptfoo)
curl -fsS "$(service_url "$service")/health" >/dev/null 2>&1
;;
open-webui|chunkviz|embedding-atlas|unsloth|wiki)
open-webui|chunkviz|embedding-atlas|unsloth|wiki|wetty)
curl -fsS "$(service_url "$service")" >/dev/null 2>&1
;;
*)
@@ -160,8 +188,6 @@ start_one() {
;;
transformerlab)
if command -v python3 >/dev/null 2>&1; then
python3 "$SCRIPT_DIR/repair_transformerlab_symlink_paths.py" \
--transformerlab-dir "$TRANSFORMERLAB_DIR" >>"$STATE_DIR/logs/transformerlab_source_repairs.log" 2>&1 || true
python3 "$SCRIPT_DIR/repair_transformerlab_plugin_supports.py" \
--transformerlab-dir "$TRANSFORMERLAB_DIR" \
--plugin "fastchat_server" \
@@ -178,6 +204,9 @@ start_one() {
--required-support "batched" >>"$STATE_DIR/logs/transformerlab_plugin_supports.log" 2>&1 || true
fi
;;
wetty)
check_wetty_prereqs
;;
*)
;;
esac
@@ -291,6 +320,7 @@ Unsloth Studio: $(service_url unsloth)
Promptfoo CLI: $PROMPTFOO_BIN
Promptfoo UI: $(service_url promptfoo)
Wiki: $(service_url wiki)
Lab 3 Terminal: $(service_url wetty)
Kiln app: ${KILN_LAUNCH_PATH:-not installed}
EOF
}