# Remote Hosts and Mobile Access Run Kiro Crew on an always-on host (a VPS, a cloud VM, a Linux desktop, a spare box) so the chat bot, cron jobs, and task runner keep working while your laptop sleeps. Then reach the dashboard from wherever you are: an SSH tunnel for a laptop, a named HTTPS tunnel for a phone. Four parts, in the order you will need them: 1. [Run 24/7 on a remote host](#1-run-247-on-a-remote-host): host setup and install 2. [Reach it from a laptop or phone](#2-reach-it-from-a-laptop-or-phone): SSH tunnels, HTTPS tunnels, session lifetimes 3. [Keep it alive as a service](#3-keep-it-alive-as-a-service): systemd / launchd, plus the awkward-host recipe 4. [Troubleshooting](#4-troubleshooting) --- ## 1. Run 24/7 on a remote host ### Host requirements - **OS**: any modern Linux distribution (Ubuntu 22.04+, Debian 12+, Fedora, Amazon Linux 2023). macOS works too, with launchd instead of systemd. - **Python**: 3.10 or newer (`setup.cfg` sets `python_requires = >=3.10`). - **Node.js**: needed to build the dashboard bundle. `website/package.json` declares `"node": "20 || >=22"`; `kirocrew doctor` warns below Node 16. - **RAM**: there is no single published floor, because the footprint scales with concurrent sessions, spawned subagents, and MCP servers. Two figures from the code give you the shape of it: `acp/runtime.py` recycles a long-lived multiplexed backend once it passes 500 MiB RSS (`_DEFAULT_MAX_RSS_MB = 500.0`), and the embedding model is loaded in-process from a ~610MB GGUF file. Measure your own steady state, then leave headroom: MCP cold starts and heavy tool calls spike well above it. - **CPU**: a couple of vCPUs is fine for a single user; extra cores help with CPU-intensive tool calls and parallel subagent execution. - **Architecture**: x86_64 or arm64. ### Install the basics ```bash # Debian / Ubuntu sudo apt-get update && sudo apt-get install -y git tmux python3 python3-pip python3-venv # Fedora / Amazon Linux 2023 sudo dnf install -y git tmux python3 python3-pip ``` Install Node.js from your distro, [nodejs.org](https://nodejs.org/), or a version manager such as [nvm](https://github.com/nvm-sh/nvm). `tmux` is handy for a first smoke test before you install the service. Set your git identity if you plan to let the agent work on repos on this host: ```bash git config --global user.name "Your Name" git config --global user.email "you@example.com" ``` ### Install the agent backend Kiro Crew drives the `kiro-cli` agent over ACP, and it is the only provider (`agent.provider = acp`). Install `kiro-cli` per its own docs, put it on your `PATH`, and log in: ```bash kiro-cli login ``` The credentials Kiro Crew itself reads from `~/.kiro/crew/.env` are chat-platform and owner-identity credentials (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `KIROCREW_OWNER_ID`, and the equivalents for Discord / Telegram / Teams / WeCom / Webex). There is no model API key to configure: `kiro-cli` owns the model credential, and `kiro-cli login` is where it is set. ### Install Kiro Crew Same two steps as a local machine (Python backend plus the React dashboard bundle). See the [install guide](install.md) for the full walkthrough: ```bash git clone https://github.com/kirodotdev/KiroCrew.git cd KiroCrew # Build the frontend bundle and stage it into the package cd website && npm install && npm run build && cd .. cp -R website/dist src/kiro_crew/static/dist # Install the backend (ships the bundled dashboard) pip install . # Configure kirocrew setup kirocrew doctor # verify everything, including kiro-cli and Node ``` `kirocrew setup` writes the resolved project directory to `~/.kiro/crew/project_dir` so the CLI works from any working directory. Embeddings need no setup step at all: they are always on and run in-process from a bundled llama-cpp-python, and the model file downloads in the background on first gateway start. There is no separate embedding service to install. ### Smoke test in tmux Before you commit to a service unit, confirm the gateway actually boots: ```bash tmux new -s kirocrew kirocrew gateway # Ctrl+B, D to detach; reattach with: tmux attach -t kirocrew ``` tmux survives an SSH disconnect but does **not** auto-restart on crash or auto-start on reboot. Move to [a real service](#3-keep-it-alive-as-a-service) once the smoke test passes, and **kill the tmux session first**: only one gateway can own the dashboard port, which defaults to `5476` (`config/loader.py` `_DEFAULT_PORT`, overridable with the `KIROCREW_PORT` environment variable). Two gateways on one port means the second one fails to bind. ```bash tmux kill-session -t kirocrew ``` ### Move your state to the new host A fresh install starts with no memories, preferences, lessons, or agent config. `scripts/sync-to-remote.sh` copies them from your local machine. Run it **from the local machine**: ```bash scripts/sync-to-remote.sh user@your-host.example.com # Non-default dashboard port on the remote (multi-host setups) scripts/sync-to-remote.sh user@your-host.example.com 7779 # Preview without transferring scripts/sync-to-remote.sh --dry-run scripts/sync-to-remote.sh --help ``` It is one-way (local overwrites remote). It takes an atomic SQLite `.backup` of `memory.db` rather than copying a live WAL, which would land a torn database; patches the remote `config.json` to `dashboard.url = http://localhost:` with `auto_open_browser` off (a headless host has no browser to open); and syncs `sessions/` so your chat history shows up on the remote dashboard. `--dry-run` prints every transfer without performing it. Set `DEFAULT_HOST` at the top of the script if you do not want to pass the target every time. What matters, and where it lives: | Category | Path | Why | |---|---|---| | Structured memory | `~/.kiro/crew/workspace/memory/` | `preferences.md`, `projects.md`, daily `history/` | | Vector + FTS databases | `~/.kiro/crew/memory.db`, `memory_index.db` | Semantic/episodic memory (`vector_memory.py`) and the FTS5 index (`memory.py`) | | Lessons | `~/.kiro/crew/memory.db`, falling back to `lessons.jsonl` | See the note below | | Config | `~/.kiro/crew/config.json` | Chat-platform tokens, model prefs, dashboard settings | | Skills | `~/.kiro/crew/skills/` | Custom skill definitions | | Webhook hooks | `~/.kiro/crew/hooks.json` | Script-hook definitions (`hooks.py` `ScriptHookStore`) | | Cron jobs | `~/.kiro/crew/crons.json` | Scheduled recurring jobs (`cron.py`) | > **Where lessons actually live.** Both stores exist and the vector store wins > when it holds anything. `learn.py` appends to `/lessons.jsonl`, > but `vector_memory.write_lesson()` stores lessons as semantic entries in > `memory.db`, and `ContextBuilder` reads `get_lessons_context()` from the vector > store first, only falling back to the JSONL when that comes back empty. So > sync **both**: `memory.db` is authoritative on any host that has ever written > a lesson natively, and the JSONL still carries lessons written before that. What NOT to carry over: - `~/.kiro/crew/kiro_pids.txt`, `kiro_session_pids.txt`, and the `session_pid_.txt` / `.sig` sidecars: live PID tracking (`session_pid.py`), meaningless on another host and actively misleading if copied. - `~/.kiro/crew/security_events.jsonl`: the tamper-evident SEL audit chain (`sel.py`); it belongs to the host that wrote it. - `~/.kiro/crew/.env`, `.local_secret`, `sel_hmac.key`: secrets. Re-enter the `.env` credentials with `kirocrew setup`; the other two are regenerated. Keeping two hosts loosely in sync afterwards is just rsync (replace the SSH target): ```bash alias mc-sync='rsync -avz ~/.kiro/crew/workspace/memory/ user@your-host.example.com:~/.kiro/crew/workspace/memory/ \ && rsync -avz ~/.kiro/crew/memory.db ~/.kiro/crew/memory_index.db user@your-host.example.com:~/.kiro/crew/' ``` Run it only while both gateways are stopped, or you will copy a live WAL-mode database out from under a writer. --- ## 2. Reach it from a laptop or phone The gateway binds `127.0.0.1` and stays there. `is_local_only()` in `dashboard/urls.py` always returns `True` in this build, so **setting `dashboard.url` does not widen the bind**: it only changes the host used in generated links and adds that origin to the CSRF/WebSocket allowlist. The one bind override is `KIROCREW_BIND` (an IP address, validated by `bind_address_for()`), which exists for containers where published ports map to a bridge interface; the official Docker image sets it. Both server paths mount the token-auth middleware unconditionally, so a wider bind never exposes an unauthenticated surface beyond the three token-free liveness probes (`/api/health`, `/api/live`, `/api/ready`). Everything below therefore works by putting something in front of loopback. ### SSH tunnel (laptop) ```bash ssh -L 5476:localhost:5476 user@your-host.example.com ``` Then open `http://localhost:5476` locally. For a non-default remote port, match both sides: ```bash ssh -N -L 7779:localhost:7779 user@your-host.example.com # foreground, no shell ssh -fN -L 7779:localhost:7779 user@your-host.example.com # background ``` To get the tunnel on every connection, add to your local `~/.ssh/config`: ``` Host your-host.example.com LocalForward 5476 localhost:5476 ``` Works on macOS, Linux, and Windows (OpenSSH ships with Windows 10+; the config file is at `%USERPROFILE%\.ssh\config`). If your browser reaches the dashboard on a *different* local port than the remote one (`ssh -L 8777:localhost:5476`), the browser sends Origin `http://localhost:8777`, which is not in the default allowlist. Opt that port in with `KIROCREW_ALLOWED_LOOPBACK_PORTS` on the gateway host; the CSRF check deliberately does not blanket-trust every loopback port, because a malicious local page on an arbitrary port would otherwise pass it. ### Named HTTPS tunnel (phone) A phone cannot open an SSH port-forward, so put a tunnel provider in front of the loopback port and point the bot's links at it. Use a **named** tunnel, not an ephemeral one: a named tunnel keeps the same URL across restarts, so `dashboard.url` stays correct and you do not have to re-edit config and restart the gateway every time the tunnel reconnects. An ephemeral tunnel mints a fresh random hostname on each start, which silently invalidates both `dashboard.url` and the origin allowlist derived from it. With [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/): ```bash cloudflared tunnel login cloudflared tunnel create kirocrew cloudflared tunnel route dns kirocrew kirocrew.example.com cloudflared tunnel --url http://localhost:5476 run kirocrew ``` With [ngrok](https://ngrok.com/docs) the equivalent is `ngrok http --domain=kirocrew.example.com 5476`. **If you use Tailscale, prefer `tailscale serve` over Funnel.** The two are not the same and the difference is the whole security story: - [`tailscale serve`](https://tailscale.com/kb/1242/tailscale-serve) publishes the dashboard **only inside your tailnet** — nothing is reachable from the public internet, you get TLS and a stable MagicDNS hostname, and who can reach it is governed by your tailnet ACLs. This is the better answer for the phone case: ```bash tailscale serve --bg --https 443 http://127.0.0.1:5476 ``` Then set `dashboard.url` to `https://..ts.net` (below) so the origin allowlist and the `Host` barrier accept it. - [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) does the opposite: it puts the service **on the public internet**, like cloudflared and ngrok. Use it only if you actually want public ingress. A shared corporate tailnet is not a private network — every member who can reach the Serve endpoint is inside your trust boundary, so keep the ACL narrow. Then set the URL in `~/.kiro/crew/config.json` and restart: ```json { "dashboard": { "url": "https://kirocrew.example.com" } } ``` ```bash kirocrew restart ``` `dashboard.url` does two things: chat-platform links are generated against that host instead of `localhost`, and its origin is added to the allowed-origin set so the CSRF check and the WebSocket handshake accept requests arriving through the tunnel. Keep the tunnel process running (tmux, or the provider's own service installer such as `cloudflared service install`). > **A tunnel puts your dashboard on the public internet.** Token auth is always > mounted, tokens are HMAC-signed, and the DNS-rebinding `Host` barrier applies to > every non-probe request. > > **Do not count on IP pinning here.** A token is pinned to the address that first > used it — but every tunnel above runs on *this* host and connects to the gateway > from loopback, so the address it pins to is the tunnel process, not your phone. > One pin is then satisfied by anyone who reaches the dashboard through that same > tunnel, for the life of the session (up to 20 hours; `kirocrew token` defaults > straight to `20h`). For the same reason the audit trail records the caller as > `127.0.0.1` rather than a client address. Security Posture → Dashboard token > auth reports which of the two states you are actually in. > > So the real controls for a tunnelled dashboard are the provider's own auth layer > (Cloudflare Access, or `tailscale serve`, which keeps the service inside your > tailnet instead of publishing it) plus short session lifetimes — not the pin. > Note also that config-write and secret-reveal endpoints refuse tunnelled requests: > `is_direct_local_request()` treats any request carrying `Forwarded` / > `X-Forwarded-*` / `X-Real-IP` as remote, and every standard tunnel and reverse > proxy attaches those. ### Getting a link on your phone 1. In your Kiro Crew DM, send `/kirocrew dashboard` (or `/kirocrew dashboard 6h`). 2. The bot DMs you `https:///?token=...`. 3. Tap it. The link exchanges the token for a session cookie. `kirocrew token` does the same thing from a shell on the gateway host. ### Session duration Two independent clocks, both signed into the token payload (`dashboard/token_auth.py`): | Clock | Value | What it governs | |---|---|---| | Link click window (`exp`) | 5 minutes (`LINK_WINDOW_SECS = 300`) | The presigned URL must be **opened** within this window | | Session TTL (`session_exp`) | 1 hour by default, 20 hours maximum (`MAX_SESSION_TTL_SECS = 20 * 3600`) | How long the cookie the link mints stays valid | The session TTL is the one you feel. The chat-command default is 1 hour (`ttl = 3600` in `slack/events.py` and `slack/handler.py`); pass a duration to raise it (`/kirocrew dashboard 6h`, `/kirocrew dashboard 20h`). `parse_duration` accepts `h` or `m` and clamps to the 20-hour ceiling, so asking for more silently gets you 20 hours rather than an error. `kirocrew token` defaults straight to `20h`. When the session expires, generate a new link. The 5-minute click window is not the session length: it only means a link left sitting in a DM overnight is dead and you need a fresh one. ### Persistent SSH tunnel on macOS (LaunchAgent) A terminal-held tunnel dies with the terminal. A LaunchAgent survives reboots and reconnects after sleep. A ready-made plist is at [`assets/com.kirocrew.tunnel.plist`](assets/com.kirocrew.tunnel.plist): ```bash cp docs/guides/assets/com.kirocrew.tunnel.plist ~/Library/LaunchAgents/ sed -i '' 's|ALIAS@DEV_DESKTOP_HOSTNAME|user@your-host.example.com|g' \ ~/Library/LaunchAgents/com.kirocrew.tunnel.plist launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kirocrew.tunnel.plist ``` Verify with the token-free liveness probe (`/api/status` needs a token, `/api/health` does not): ```bash curl -s http://localhost:5476/api/health # {"ok": true, ...} ``` Manage it: ```bash tail -f /tmp/kirocrew-tunnel.log launchctl kickstart -k gui/$(id -u)/com.kirocrew.tunnel # restart launchctl bootout gui/$(id -u)/com.kirocrew.tunnel # stop launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kirocrew.tunnel.plist # start ``` The plist sets `ServerAliveInterval=30`, `ServerAliveCountMax=3`, `ExitOnForwardFailure=yes`, and `KeepAlive`, so macOS restarts the tunnel after a sleep/wake or network change; reconnect takes roughly 30 seconds. Use SSH key-based authentication so the reconnect is passwordless. ### Raycast script (macOS, zsh) Opens the tunnel if needed, mints a token on the remote, and opens the browser. ```zsh #!/bin/zsh -e # Required parameters: # @raycast.schemaVersion 1 # @raycast.title Open KiroCrew # @raycast.mode compact # Optional parameters: # @raycast.packageName KiroCrew Utils # Documentation: # @raycast.description Get a token and open the dashboard REMOTE_HOST="user@your-host.example.com" REMOTE_CMD='source ~/.zshrc; kirocrew token' DEBUG_LOG="/tmp/kirocrew_debug.log" if lsof -i :5476 -sTCP:LISTEN > /dev/null 2>&1; then echo "Tunnel already open" else ssh -fNT -L 5476:localhost:5476 "${REMOTE_HOST}" echo "Tunnel opened" fi # `kirocrew token` prints only URL(s) on stdout (failures go to stderr). Keep the # localhost one: that is what routes through the SSH tunnel opened above. URL="$(ssh -o ConnectTimeout=10 "${REMOTE_HOST}" "${REMOTE_CMD}" 2>"${DEBUG_LOG}" | grep -m1 'localhost:5476')" if [[ -z "$URL" ]]; then echo "ERROR: no token URL. Check ${DEBUG_LOG}" exit 1 fi open "$URL" ``` --- ## 3. Keep it alive as a service ### The built-in installer ```bash kirocrew service install ``` On Linux this writes a **system-level** systemd unit at `/etc/systemd/system/kirocrew.service` and enables it, so the gateway survives SSH disconnects, restarts on failure, and starts on boot (`WantedBy=multi-user.target`). On macOS it writes a launchd LaunchAgent at `~/Library/LaunchAgents/dev.kirocrew.gateway.plist` with `RunAtLoad`, `KeepAlive=true`, and a finite `ExitTimeOut`, so it starts at login, relaunches after exit, and force-kills only after the graceful stop deadline. An explicit `kirocrew stop` unloads the agent so it stays down for the current login session. ```bash kirocrew service status # service state kirocrew logs -f # tail live logs kirocrew stop # stop kirocrew restart # restart (service-aware) kirocrew service uninstall # remove the unit / plist ``` **Sudo scope on Linux:** the install shells out to `sudo install` (to place the unit as root-owned `0644`) and `sudo systemctl` (daemon-reload, enable, restart). No kirocrew, MCP, or LLM code path runs under sudo. Once started the gateway runs as `User=$USER Group=$(id -gn)`, not root. The unit also caps crash-looping with `StartLimitBurst=3` / `StartLimitIntervalSec=300` and pins `LimitNOFILE=65536`, because a stock 1024 FD limit fails the frontend production build with `EMFILE`. **Why system-level rather than `systemctl --user`:** older distros (systemd 219 era) have no working per-user manager, and `systemctl --user` there fails with `Failed to get D-Bus connection`. A system unit behaves the same on every systemd since 2015. ### Hosts without a working `systemd --user` If you specifically want a **user** unit and `systemctl --user status` errors out, the per-user manager is not running. Enable it once (needs sudo), then install the user unit: ```bash sudo tee /etc/systemd/system/user@$(id -u).service << 'EOF' [Unit] Description=User Manager for UID %i After=systemd-user-sessions.service After=user-runtime-dir@%i.service Wants=user-runtime-dir@%i.service [Service] LimitNOFILE=infinity LimitNPROC=infinity User=%i PAMName=systemd-user Type=notify PermissionsStartOnly=true ExecStartPre=/bin/loginctl enable-linger %i ExecStart=/usr/lib/systemd/systemd --user Slice=user-%i.slice KillMode=mixed Delegate=yes TasksMax=infinity Restart=always RestartSec=15 [Install] WantedBy=default.target EOF sudo systemctl daemon-reload sudo systemctl enable user@$(id -u).service sudo systemctl start user@$(id -u).service ``` `ExecStartPre=/bin/loginctl enable-linger` is the load-bearing line: without lingering, the user manager (and everything under it) is torn down when your last login session ends, which defeats the entire point of running 24/7. Verify with `systemctl --user status`, then install the user unit with the helper script [`assets/setup.sh`](assets/setup.sh). It resolves the `kirocrew` binary and the Node version, renders [`assets/kirocrew.service`](assets/kirocrew.service) into `~/.config/systemd/user/`, and enables it. It reads the template from its own directory, so run it from there, and it refuses to proceed if a gateway is already running (that port conflict again): ```bash cd docs/guides/assets && ./setup.sh ``` Either way, read the rendered `Environment=PATH=` line before you start the service and drop any entry that does not exist on your host, then add whatever does: the unit gets exactly this `PATH` and nothing from your shell profile. Or do it by hand: ```bash mkdir -p ~/.config/systemd/user cp docs/guides/assets/kirocrew.service ~/.config/systemd/user/ sed -i "s|KIROCREW_BIN|$(command -v kirocrew)|g" ~/.config/systemd/user/kirocrew.service sed -i "s/%u/$(whoami)/g" ~/.config/systemd/user/kirocrew.service sed -i "s/NVM_NODE_VERSION/$(node --version)/g" ~/.config/systemd/user/kirocrew.service systemctl --user daemon-reload systemctl --user enable kirocrew systemctl --user start kirocrew ``` `systemctl --user status kirocrew` should report `active (running)`. > **`Failed to get D-Bus connection` while running `systemctl --user`?** Your > shell has no `XDG_RUNTIME_DIR`, which is how the client finds the per-user bus > socket. This is normal in a non-login shell, in a `cron` job, and inside an > agent-spawned subprocess. Export it and retry: > `export XDG_RUNTIME_DIR=/run/user/$(id -u)`. Manage a user unit: | Action | Command | |---|---| | Status | `systemctl --user status kirocrew` | | Restart | `systemctl --user restart kirocrew` | | Logs | `journalctl --user -u kirocrew -f` | | Uninstall | `systemctl --user disable --now kirocrew` | ### Hand-rolled system unit If you want to customize the unit rather than let `kirocrew service install` generate it: ```bash KIROCREW_BIN=$(command -v kirocrew 2>/dev/null || echo "$HOME/.local/bin/kirocrew") sudo tee /etc/systemd/system/kirocrew.service << EOF [Unit] Description=KiroCrew AI Agent Gateway After=network-online.target Wants=network-online.target StartLimitBurst=3 StartLimitIntervalSec=300 [Service] Type=simple User=$(whoami) ExecStart=$KIROCREW_BIN gateway Restart=on-failure RestartSec=10 LimitNOFILE=65536 WorkingDirectory=$HOME Environment=HOME=$HOME Environment=PATH=$(dirname $KIROCREW_BIN):$HOME/.local/bin:$HOME/.nvm/versions/node/$(node -v 2>/dev/null || echo v20.0.0)/bin:/usr/local/bin:/usr/bin [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now kirocrew ``` Tail it with `sudo journalctl -u kirocrew -f`. Getting `PATH` right matters: the unit does not inherit your interactive shell's environment, so `kiro-cli`, `node`, and `npx` must all be reachable from the `PATH` you set here or MCP servers and tool calls fail with ENOENT. --- ## 4. Troubleshooting | Symptom | Fix | |---|---| | `kirocrew: command not found` after install | Put pip's script dir on `PATH` (often `~/.local/bin`), then `source ~/.bashrc` or re-login | | Agent backend errors or timeouts | Confirm `kiro-cli` is on `PATH` and logged in (`kiro-cli login`); `kirocrew doctor` reports its status | | Service will not start | `sudo journalctl -u kirocrew -n 50` (system unit) or `journalctl --user -u kirocrew -n 50` (user unit) | | Service restart-loops then gives up | `StartLimitBurst=3` within 5 minutes stops the loop on purpose. Read the logs, fix the cause, then `sudo systemctl reset-failed kirocrew` | | `systemctl --user` says `Failed to get D-Bus connection` | `export XDG_RUNTIME_DIR=/run/user/$(id -u)` | | Gateway will not bind the port | Something else already owns it, usually a tmux gateway. `tmux kill-session -t kirocrew`, then `ss -ltnp \| grep 5476` | | SSH tunnel connection refused | Confirm the gateway is running and listening: `ss -ltnp \| grep 5476` on the remote host | | Dashboard loads over the tunnel but the live view flaps online/offline | The TLS-terminating proxy must forward `X-Forwarded-Proto: https`; without it the auth cookie is set without `Secure` and mobile browsers withhold it from the `wss://` upgrade | | Chat link still points at `localhost` | Set `dashboard.url` in `config.json` and restart the gateway | | Link opens to "token expired" | The presigned URL must be opened within 5 minutes. Request a fresh link | | Session drops sooner than you expect | The default is 1 hour. Ask for longer: `/kirocrew dashboard 6h`, up to 20h | | Phone cannot reach the tunnel URL | Verify the tunnel process is running and connected on the gateway host | | Settings will not save over the tunnel | By design. Config-write and secret-reveal endpoints require a direct-local request, and forwarding headers mark a tunnelled request as remote. Change these over an SSH session on the host | | "Embeddings not ready" in the dashboard | The ~610MB model downloads in the background over HTTPS on gateway start. `kirocrew doctor` probes the resolved URL; set `KIROCREW_EMBED_MODEL_URL` for a mirror. Memory falls back to keyword search until it lands, and the agent keeps working | ## See also - [install.md](install.md): all build and install methods - [docker.md](docker.md): container deployment, including `KIROCREW_BIND` - [slack-setup.md](slack-setup.md): chat app creation and configuration - [../architecture/security-deep-dive.md](../architecture/security-deep-dive.md): token auth, origin checks, the local-request gate