#!/bin/sh
#
# coding - Enter the SliTaz AI coding jail as yourself, then run opencode.
# The jail is created by root with: tazdev gen-jail
#
# This script runs as root through sudo, so it takes no argument and reads
# no user config, only /etc/slitaz/tazdev.conf. The sudoers rule is:
#
#     %coding ALL=(root) NOPASSWD: /usr/bin/coding
#
# (c) 2026 SliTaz GNU/Linux - GNU GPL v3
#

JAIL_ROOT="/home/slitaz/coding"
JAIL_LOG="/var/log/coding.log"
[ -f /etc/slitaz/tazdev.conf ] && . /etc/slitaz/tazdev.conf
jail="$JAIL_ROOT"

die() {
	echo "coding: $@" >&2
	exit 1
}

# Who used the jail and when, read by admins with: tazdev jail-log
log() {
	echo "$(date '+%Y-%m-%d %H:%M:%S') $user $1" >> $JAIL_LOG
}

# Become root, sudo tells us who we really are
if [ "$(id -u)" != 0 ]; then
	sudo -n -l "$0" >/dev/null 2>&1 ||
		die "you may not enter the coding jail, ask an admin"
	exec sudo "$0"
fi

user="$SUDO_USER"
[ -n "$user" ] || die "run me as a normal user, not as root"
case "$user" in
	*[!a-z0-9_.-]*) die "invalid user name: $user" ;;
esac
uid=$(id -u $user)
gid=$(id -g $user)
group=$(id -gn $user)
[ "$uid" -ge 1000 ] || die "system accounts do not code in the jail"

[ -x "$jail/usr/bin/opencode" ] ||
	die "no coding jail in $jail, ask an admin for: tazdev gen-jail"
# Each session gets its own pid namespace: without it the dev's host
# processes (ssh, shell) show in /proc and their /proc/<pid>/root leads
# out of the jail.
command -v unshare >/dev/null ||
	die "unshare is missing, ask an admin for: util-linux-unshare"

# Root only lock: a dev holding it could block everybody
lock=/var/lock/coding.lock
touch $lock && chmod 0600 $lock

# Mount once for everybody and keep it mounted: other devs may be inside.
# With hidepid=2 each dev only sees their own processes in the jail. The
# lock also covers the accounts: two first visits may come at once.
(
set -e
flock 9
if ! mountpoint -q $jail/proc; then
	mount -t proc -o nosuid,nodev,noexec,hidepid=2 proc $jail/proc
	mount -t sysfs -o ro,nosuid,nodev,noexec sysfs $jail/sys
	mount -t tmpfs -o nosuid,mode=0755 none $jail/dev
	mknod -m 666 $jail/dev/null    c 1 3
	mknod -m 666 $jail/dev/zero    c 1 5
	mknod -m 666 $jail/dev/tty     c 5 0
	mknod -m 666 $jail/dev/ptmx    c 5 2
	mknod -m 444 $jail/dev/random  c 1 8
	mknod -m 444 $jail/dev/urandom c 1 9
	ln -s /proc/self/fd   $jail/dev/fd
	ln -s /proc/self/fd/0 $jail/dev/stdin
	ln -s /proc/self/fd/1 $jail/dev/stdout
	ln -s /proc/self/fd/2 $jail/dev/stderr
	mkdir $jail/dev/pts $jail/dev/shm
	mount -t devpts -o gid=4,mode=620 none $jail/dev/pts
	mount -t tmpfs -o nosuid,nodev,size=256m none $jail/dev/shm
fi

# Same uid and gid as on the host, with a locked password
has() { awk -F: -v k="$1" -v f=$2 '$f == k { found=1 } END { exit !found }' $3; }
has $user 1 $jail/etc/passwd ||
	echo "$user:x:$uid:$gid:$user:/home/$user:/bin/bash" >> $jail/etc/passwd
has $user 1 $jail/etc/shadow ||
	echo "$user:!:0:0:99999:7:::" >> $jail/etc/shadow
has $gid 3 $jail/etc/group ||
	echo "$group:x:$gid:" >> $jail/etc/group

# First visit: a private home with the default opencode config
if [ ! -d "$jail/home/$user" ]; then
	mkdir -p $jail/home/$user
	cp -a $jail/etc/skel/. $jail/home/$user
	# The opencode wrapper avoids the background server, but if another
	# subcommand starts it, it listens on a fixed port: one port per dev
	echo "{ \"port\": $((20000 + uid)) }" \
		> $jail/home/$user/.config/opencode/service.json
	chmod 0600 $jail/home/$user/.config/opencode/service.json
	chown -R $uid:$gid $jail/home/$user
	chmod 0700 $jail/home/$user
fi
) 9> $lock || die "jail setup failed"

sed "s|<user>|$user|g" $jail/etc/motd
log enter
# A lost ssh connection is logged as a hangup, not a leave
trap 'log hangup; exit' HUP TERM
# Private pid and mount namespaces with a fresh /proc, then drop root
# in the jail: --nnp means nothing can give it back (setuid, caps)
unshare --pid --mount --fork --mount-proc=$jail/proc \
	chroot $jail /bin/busybox setpriv --nnp /bin/busybox su -l $user
log leave
