Skip to content

Latest commit

 

History

History
232 lines (184 loc) · 12.6 KB

File metadata and controls

232 lines (184 loc) · 12.6 KB

Install from source

The manual path — four root steps a checkout installs with, no RPM. The package install (see the README) automates all of it; sudo ai-tools-bootstrap automates steps 2–3 once install.sh has deployed it, and install.sh automates everything from step 4 on.

Set the recurring identities once, in the shell you run these steps in, so every command pastes verbatim (the full naming spec is in naming-conventions.md):

export PROJECTS_USER="$(id -un)"
export PROJECTS_GROUP="$(id -gn)"
export PROJECTS_HOME="${HOME}"
export SANDBOX_USER=ai-tools
export SANDBOX_GROUP=ai-tools

Each critical step also re-states the sandbox name inline, so a step pasted on its own still works.

1. Install PATH dedup fragment (root, once)

sudo install -d -o root -g root -m 751 /usr/local/lib/ai-tools
sudo install -o root -g root -m 644 \
    src/usr/local/lib/ai-tools/path-dedup.sh /usr/local/lib/ai-tools/path-dedup.sh

(The lib directory's group becomes ai-tools once the account exists — install.sh and the RPM re-assert root:ai-tools 0751.)

path-dedup deduplicates the shell's existing $PATH and orders it root-owned-first, so /usr/local/bin/claude — the wrapper that launches claude restricted — always resolves ahead of the nvm-managed claude. It is sourced per-account: only the operator shells wired for it get the ordering, and every other account on the host keeps its stock PATH.

sudo ai-tools-admin operators add <user> offers to wire the source line into your ~/.bashrc and ~/.bash_profile. To wire it by hand, add it to both files (non-login interactive shells read only ~/.bashrc, login shells ~/.bash_profile), after your nvm init:

export NVM_DIR="${HOME}/.nvm"
[ -s "${NVM_DIR}/nvm.sh" ] && source "${NVM_DIR}/nvm.sh"

# ai-tools PATH dedup (must follow nvm init)
[[ -f /usr/local/lib/ai-tools/path-dedup.sh ]] && source /usr/local/lib/ai-tools/path-dedup.sh

Those two files are bash's, and operators add names your login shell when it reads something else. The fragment sources cleanly under zsh, so the same line goes in ~/.zshrc and ~/.zprofile; a shell that reads no bash (fish) takes the same tier ordering in its own syntax.

nvm must be sourced before path-dedup: nvm prepends its versioned bin dir to $PATH, and path-dedup then restructures it into Tier 4, behind the T1 system bins (which include the wrapper) and T2 ~/.local/bin. path-dedup.sh is idempotent — sourcing it again in the same shell produces the same PATH.

2. Create the SANDBOX_USER OS account at /opt (root, once)

# The sandbox account name is fixed at ai-tools (see "Identities and naming" in the
# README). Set it here so this block works even pasted on its own -- an unset
# SANDBOX_USER makes useradd fail with "invalid user name ''".
SANDBOX_USER=ai-tools
SANDBOX_GROUP=ai-tools

sudo useradd \
    --system \
    --shell /sbin/nologin \
    --home-dir /opt/ai-tools \
    --no-create-home \
    --comment "AI tools sandbox user" \
    "${SANDBOX_USER}"
sudo install -d -o "${SANDBOX_USER}" -g "${SANDBOX_GROUP}" -m 755 /opt/ai-tools

# Lock password (system users have no password by default, but be explicit)
sudo passwd -l "${SANDBOX_USER}"

The install -d creates /opt/ai-tools owned by the account with +x for all, so ${PROJECTS_USER} can traverse into bin/. The RPM ships this account via sysusers.d, so this step applies only to the from-source path.

/home is mounted nosuid, which would prevent the sudo UID-switch from taking effect. /opt/ai-tools has no nosuid restriction, so the switch to ${SANDBOX_USER} actually takes effect.

3. Install nvm + Node + claude as SANDBOX_USER (root, once)

ai-tools-bootstrap does steps 2 and 3 in one idempotent command once the package is installed — it creates the account, installs the toolchain, seeds the symlink, and enables the nvm-update.timer. The manual equivalent:

# cd first: the block runs as ${SANDBOX_USER}, which cannot occupy your home as cwd
sudo -u "${SANDBOX_USER}" bash -c '
  cd /opt/ai-tools
  export NVM_DIR=/opt/ai-tools/.nvm
  export HOME=/opt/ai-tools
  curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
  source /opt/ai-tools/.nvm/nvm.sh
  nvm install 22
  nvm alias default 22
  npm install -g @anthropic-ai/claude-code
'

# Create bin dir and initial claude symlink (nvm-update.sh maintains it going forward)
sudo -u "${SANDBOX_USER}" bash -c '
  cd /opt/ai-tools
  source /opt/ai-tools/.nvm/nvm.sh
  mkdir -p /opt/ai-tools/bin
  ln -sf "/opt/ai-tools/.nvm/versions/node/$(nvm version default)/bin/claude" \
         /opt/ai-tools/bin/claude
'

Once install.sh (step 4) has run, /opt/ai-tools/bin is locked 0551 root:ai-tools and only root maintains the symlink: instead of the ln above, run sudo ai-tools-bootstrap (idempotent -- it provisions whatever is missing and seeds the symlink through the root helper), or re-run sudo ./install.sh install.

4. Run the install script (root, once)

Everything from here on is fully automated by install.sh. Complete steps 2 and 3 first — the account must exist (else the script stops with ai-tools user not found) and /opt/ai-tools/bin must exist (step 3 creates it; the script writes nvm-update.sh into it). sudo ai-tools-bootstrap does both in one idempotent command. Then run:

sudo ./install.sh install

The script asks which account to enrol as the operator, offering the invoking SUDO_USER as the default — answer No to name another. A non-interactive run and a plain Enter both take SUDO_USER. root is refused at either route, including the one that reaches it by accident: sudo from a root shell sets SUDO_USER=root, and the resulting host has an operator the CLI refuses every project verb.

Enrolling your own login account is the usual choice. Agent-written files are handed back to it, so an editor or IDE working in a claimed project keeps seeing its own files, and a login that can already sudo holds what claiming needs. Enrol a different account when this host is being set up for someone else, or when a dedicated provisioning account owns the projects. Create one before installing:

sudo useradd -m -s /bin/bash op && sudo usermod -aG wheel op

useradd creates the login account the script enrols; usermod -aG wheel is this host's general sudo grant, which nothing in this project writes (see below). Naming an account that does not exist yet refuses the install and prints this same command.

The question is asked once per account. A re-install whose invoking account already holds both facts — a name in OPERATORS and ai-ops membership — reports whose host it is working on and re-asserts that enrolment without prompting.

Name the account up front to skip the question — what an unattended install uses, and how to enrol a different account on a host that already has one:

sudo ./install.sh install --operator op

The name is refused on the same terms as a typed one (root, the ai-tools sandbox account, an account that does not exist or has no home), and it decides only who is enrolled: the script still runs as sudo, and its verification suite still runs as the invoking SUDO_USER.

Enrolment writes the two facts that make an operator — ai-ops membership and a name in OPERATORS. Claiming a project needs a general sudo grant as well, which nothing here writes; the host's own sudoers decides it. An operator without one launches agent sessions, and another operator claims for it with ai-tools --project-claim --for <operator>. A host needs at least one operator holding the grant, so enrol one that does — a service account holding none is enrolled after the install with ai-tools-admin, rather than named at this prompt.

The script deploys the static %ai-ops sudoers drop-in, the helpers and the system units, creates the approved-projects allowlist with format documentation, installs the ai-tools project CLI and the /var/opt/ai-tools sandbox area, enables the nvm-update.timer in ${SANDBOX_USER}'s --user instance, and enables the ai-tools-relabel.path watcher. It is idempotent — safe to re-run after updates. The install directory is never auto-registered as a project.

Enrol each further login user as an operator (ai-ops membership, allowlist seed):

sudo ai-tools-admin operators add <user>     # defaults to $SUDO_USER

It reports which shape the enrolment produced — whether the account can claim projects, or only launch sessions and have them claimed for it — by asking sudo about that account.

Register projects with the ai-tools CLI, run as your own user (no sudo):

ai-tools --project-claim /path/to/project     # an existing directory, claimed in place
ai-tools --project-create /path/to/new        # a new project directory, created and claimed
ai-tools --sandbox-create /path/to/repo       # an isolated shallow clone
ai-tools --lockdown /path/to/project          # revoke agent access to secrets (sudo)

project-lifecycle.md covers registering in depth — claim vs sandbox clone, what each consent prompt grants (including the traverse-only parent grant a home-nested project needs), and every recovery/reversal path.

To remove everything installed by this script:

sudo ./install.sh uninstall

Files

The source→deploy map install.sh applies (the authoritative per-artifact owner/group/mode list is tests/integration/perms.sh, which sudo ./install.sh check-perms runs):

File Deploy path
src/usr/local/lib/ai-tools/path-dedup.sh /usr/local/lib/ai-tools/path-dedup.sh (root)
src/opt/ai-tools/bin/nvm-update.sh /opt/ai-tools/bin/nvm-update.sh
src/usr/local/libexec/ai-tools/ai-tools-chown.sh /usr/local/libexec/ai-tools/ai-tools-chown (root)
src/usr/local/libexec/ai-tools/ai-tools-setgid.sh /usr/local/libexec/ai-tools/ai-tools-setgid (root)
src/usr/local/libexec/ai-tools/ai-tools-launcher-symlink.sh /usr/local/libexec/ai-tools/ai-tools-launcher-symlink (root)
src/usr/local/libexec/ai-tools/ai-tools-relabel-agent.sh /usr/local/libexec/ai-tools/ai-tools-relabel-agent (root)
src/usr/local/libexec/ai-tools/ai-tools-bootstrap.sh /usr/local/libexec/ai-tools/ai-tools-bootstrap (root)
src/usr/local/libexec/ai-tools/ai-tools-admin.sh /usr/local/libexec/ai-tools/ai-tools-admin (root)
src/usr/local/libexec/ai-tools/ai-tools-lockdown.sh /usr/local/libexec/ai-tools/ai-tools-lockdown (root)
src/usr/local/libexec/ai-tools/ai-tools-handback.py /usr/local/libexec/ai-tools/ai-tools-handback (root)
src/usr/local/bin/ai-tools-handback-client.py /usr/local/bin/ai-tools-handback-client (root:ai-tools)
src/usr/lib/systemd/system/ai-tools-handback.socket /usr/lib/systemd/system/ai-tools-handback.socket (root)
src/usr/lib/systemd/system/ai-tools-handback@.service /usr/lib/systemd/system/ai-tools-handback@.service (root)
src/usr/local/lib/ai-tools/secret-patterns.lib.sh /usr/local/lib/ai-tools/secret-patterns.lib.sh (root)
src/usr/local/lib/ai-tools/skip-dirs.lib.sh /usr/local/lib/ai-tools/skip-dirs.lib.sh (root)
src/usr/local/lib/ai-tools/filters.lib.sh /usr/local/lib/ai-tools/filters.lib.sh (root)
src/usr/local/lib/ai-tools/filters.d/core.rules /usr/local/lib/ai-tools/filters.d/core.rules (root)
src/usr/local/lib/ai-tools/filters.d/dotnet.rules /usr/local/lib/ai-tools/filters.d/dotnet.rules (root)
src/usr/local/bin/claude.sh /usr/local/bin/claude (root)
src/opt/ai-tools/bin/ai-tools-run.sh /opt/ai-tools/bin/ai-tools-run
src/opt/ai-tools/agents/claude-code/post-tool-hook.sh /opt/ai-tools/.claude/post-tool-hook.sh
src/opt/ai-tools/agents/claude-code/session-hook.sh /opt/ai-tools/.claude/session-hook.sh
src/opt/ai-tools/agents/claude-code/filter-hook.sh /opt/ai-tools/.claude/filter-hook.sh
src/opt/ai-tools/agents/claude-code/settings.json /opt/ai-tools/.claude/settings.json
src/usr/lib/systemd/user/nvm-update.service /usr/lib/systemd/user/nvm-update.service (root)
src/usr/lib/systemd/user/nvm-update.timer /usr/lib/systemd/user/nvm-update.timer (root)
src/usr/lib/systemd/system/ai-tools-relabel.path /usr/lib/systemd/system/ai-tools-relabel.path (root)
src/usr/lib/systemd/system/ai-tools-relabel.service /usr/lib/systemd/system/ai-tools-relabel.service (root)
src/etc/sudoers.d/ai-tools /etc/sudoers.d/ai-tools (root)
src/etc/ai-tools/operator.conf /etc/ai-tools/operator.conf (root; seeded once, then operator-maintained)
install.sh run in place via sudo